mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
* fix(resolution): distinguish name guesses and preserve export visibility * test(go): keep method enrichment fixture in one package * fix(resolution): address split review edge cases and evidence reporting * fix(exports): recognize imported and expression-local receivers * fix(resolution): align export and target evidence with language scope * fix(ingestion): preserve lexical import provenance through resolution * test(ci): rebalance Windows shards from measured slow suites * Address PR review feedback (#3190) - Label constructor unique-name guesses as global-name-fallback and run language vetoes - Tighten Go qualified, Rust crate::, and Ruby class-reopen fallback guards - Ignore for-loop shadowed CommonJS receivers and exclude guesses from the resolved-call census - Refresh FinalizeOutput and hook docs for lexical binding scopes Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten review-feedback leftovers on fallback visibility. Qualified Go calls still respect export and test-package rules, nested Rust src/ stays a module segment, and top-level conditional this.x is treated as CommonJS. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(autofix): apply prettier + eslint fixes via /autofix command * Address PR review feedback (#3190) - Distinguish Swift package prefixes when comparing target modules - Document lexical import binding and handledSites refusal marking - Drop the stale ci-scope-parity workflow claim and prototype-safe export verdicts Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3190) Supply caller source on Ruby visibility cases so they exercise the named allow branches instead of the missing-text bypass. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(resolution): label unique constructor types as name guesses A workspace-unique class hit in findClassBindingInScope was treated as an in-scope bind, so Go/JS constructor-form sites skipped the guess label and the Go unexported veto. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(resolution): keep qualified constructors precise after unique-name split Bare constructor unique-name hits stay guesses so Go can veto an unexported type. A written qualifier is now carried as rawQualifiedName so `new pkg.Foo()` and `models.Box[T]{}` can still recover the unique class without that veto. Co-authored-by: Cursor <cursoragent@cursor.com> * test(bench): rebaseline Go/Java scope-capture fingerprints for constructor qualifiers Generic Go composite literals and qualified Java `new pkg.Foo()` now carry @reference.qualified-name on existing constructor matches. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(resolution): treat import-reached unique constructors as precise C++ #include and Rust re-exports do not mint a lexical class binding. A unique type in an imported file (or imported directory) is therefore a real bind, not a name guess, so those CALLS edges stay import-resolved. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(resolution): require named or resolved imports for constructor precision Bare Go Box[T]{} is not package-qualified, and a sibling-file import of a different name is not constructor visibility. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
325 lines
12 KiB
TypeScript
325 lines
12 KiB
TypeScript
/**
|
|
* `@declaration.is-exported` — the export-evidence marker the TypeScript and
|
|
* JavaScript capture emitters synthesize (`ts-js-export-marker.ts`), and its
|
|
* landing on `SymbolDefinition.isExported` through the central extractor.
|
|
* Review findings on #3182 (typescript/scope-resolver.ts:138).
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { emitTsScopeCaptures } from '../../../src/core/ingestion/languages/typescript/captures.js';
|
|
import { emitJsScopeCaptures } from '../../../src/core/ingestion/languages/javascript/captures.js';
|
|
import { extract } from '../../../src/core/ingestion/scope-extractor.js';
|
|
import { typescriptScopeResolver } from '../../../src/core/ingestion/languages/typescript/scope-resolver.js';
|
|
|
|
type Emit = typeof emitTsScopeCaptures;
|
|
|
|
function verdicts(emit: Emit, src: string, filePath: string): Record<string, string | undefined> {
|
|
const out = Object.create(null) as Record<string, string | undefined>;
|
|
for (const m of emit(src, filePath)) {
|
|
const name = m['@declaration.name']?.text;
|
|
if (name === undefined) continue;
|
|
if (Object.hasOwn(out, name) && out[name] !== undefined) continue;
|
|
out[name] = m['@declaration.is-exported']?.text;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const ESM = `
|
|
export function a() {}
|
|
function b() {}
|
|
const c = () => 1;
|
|
export const d = 2;
|
|
function e() {}
|
|
export { c, e as renamed };
|
|
function f() {}
|
|
export default f;
|
|
function g() { function inner() {} }
|
|
`;
|
|
|
|
describe('@declaration.is-exported (TypeScript emitter)', () => {
|
|
it('marks direct, clause and default exports true and everything else false in an ESM file', () => {
|
|
const v = verdicts(emitTsScopeCaptures, ESM, 'test.ts');
|
|
expect(v.a).toBe('true');
|
|
expect(v.b).toBe('false');
|
|
expect(v.c).toBe('true');
|
|
expect(v.d).toBe('true');
|
|
expect(v.e).toBe('true');
|
|
expect(v.f).toBe('true');
|
|
expect(v.g).toBe('false');
|
|
expect(v.inner).toBe('false');
|
|
});
|
|
|
|
it('does not skip a declaration named toString as an Object.prototype hit', () => {
|
|
const v = verdicts(emitTsScopeCaptures, 'export function toString() {}\n', 'test.ts');
|
|
expect(v.toString).toBe('true');
|
|
});
|
|
|
|
it('a member of an exported class is NOT itself exported; nested functions never are (magyargergo)', () => {
|
|
const v = verdicts(
|
|
emitTsScopeCaptures,
|
|
'export class Unrelated { clash() {} }\nfunction wrapper() { function selected() {} }\nexport { selected };\nconst selected = 1;\n',
|
|
'test.ts',
|
|
);
|
|
expect(v.Unrelated).toBe('true');
|
|
expect(v.clash).toBe('false');
|
|
expect(v.wrapper).toBe('false');
|
|
// Two `selected`s: the module-level one is exported by the clause, the
|
|
// nested one is not — `verdicts` keeps the first non-undefined per name, so
|
|
// look them up individually.
|
|
const all = emitTsScopeCaptures(
|
|
'function wrapper() { function selected() {} }\nexport { selected };\nconst selected = 1;\n',
|
|
'test.ts',
|
|
).filter((m) => m['@declaration.name']?.text === 'selected');
|
|
expect(all.map((m) => m['@declaration.is-exported']?.text).sort()).toEqual(['false', 'true']);
|
|
});
|
|
|
|
it("a method of the `module.exports = { … }` object literal IS that module's export (magyargergo)", () => {
|
|
const v = verdicts(
|
|
emitJsScopeCaptures,
|
|
'function helper() {}\nmodule.exports = { alpha() { return 1; }, beta: () => 2 };\n',
|
|
'lib.js',
|
|
);
|
|
expect(v.alpha).toBe('true');
|
|
expect(v.beta).toBe('true');
|
|
expect(v.helper).toBeUndefined();
|
|
});
|
|
|
|
it('emits NO verdict for a CommonJS file — `module.exports` is an export surface it cannot read', () => {
|
|
const v = verdicts(
|
|
emitTsScopeCaptures,
|
|
'function a() {}\nfunction b() {}\nmodule.exports = { a };\n',
|
|
'test.ts',
|
|
);
|
|
expect(v.a).toBeUndefined();
|
|
expect(v.b).toBeUndefined();
|
|
});
|
|
|
|
it('emits NO verdict for an ambient .d.ts', () => {
|
|
const v = verdicts(emitTsScopeCaptures, 'declare function a(): void;\n', 'lib.d.ts');
|
|
expect(v.a).toBeUndefined();
|
|
});
|
|
|
|
it('does not let a file that STARTS with `export` mark everything exported (the text-prefix trap)', () => {
|
|
const v = verdicts(
|
|
emitTsScopeCaptures,
|
|
'export const x = 1;\nfunction hidden() {}\n',
|
|
'test.ts',
|
|
);
|
|
expect(v.x).toBe('true');
|
|
expect(v.hidden).toBe('false');
|
|
});
|
|
});
|
|
|
|
describe('@declaration.is-exported — Opus review follow-ups', () => {
|
|
it('a re-export FROM another module never marks a same-named local exported', () => {
|
|
const v = verdicts(
|
|
emitTsScopeCaptures,
|
|
"export { alpha } from './other';\nexport { beta as gamma } from './o';\nexport type { T } from './t';\nfunction alpha() {}\nfunction beta() {}\nfunction gamma() {}\ntype T = number;\nexport const keep = 1;\n",
|
|
'test.ts',
|
|
);
|
|
expect(v.alpha).not.toBe('true');
|
|
expect(v.beta).not.toBe('true');
|
|
expect(v.gamma).not.toBe('true');
|
|
expect(v.T).not.toBe('true');
|
|
expect(v.alpha).toBe('false');
|
|
expect(v.keep).toBe('true');
|
|
});
|
|
|
|
it('exports inside a namespace or ambient module body are not file-level exports', () => {
|
|
const v = verdicts(
|
|
emitTsScopeCaptures,
|
|
"export namespace NS { export function f() {} }\ndeclare module 'x' { export function q(): void; }\nexport function top() {}\n",
|
|
'test.ts',
|
|
);
|
|
expect(v.NS).toBe('true');
|
|
expect(v.f).not.toBe('true');
|
|
expect(v.q).not.toBe('true');
|
|
expect(v.top).toBe('true');
|
|
});
|
|
|
|
it('a comment or string mentioning module.exports does not silence the ESM verdicts', () => {
|
|
const v = verdicts(
|
|
emitTsScopeCaptures,
|
|
"// legacy: module.exports = api\nconst note = 'exports.x = 1';\nexport function a() {}\nfunction b() {}\n",
|
|
'test.ts',
|
|
);
|
|
expect(v.a).toBe('true');
|
|
expect(v.b).toBe('false');
|
|
// ...while a real alias of the export object still does.
|
|
const cjs = verdicts(
|
|
emitJsScopeCaptures,
|
|
'const m = module.exports;\nfunction b() {}\nm.b = b;\n',
|
|
'x.js',
|
|
);
|
|
expect(cjs.b).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('@declaration.is-exported (JavaScript emitter)', () => {
|
|
it('keeps bracket-form top-level this exports undecided like dot-form exports', () => {
|
|
for (const [emit, file] of [
|
|
[emitJsScopeCaptures, 'x.js'],
|
|
[emitTsScopeCaptures, 'x.ts'],
|
|
] as const) {
|
|
const v = verdicts(emit, "function api() {} this['api'] = api;", file);
|
|
expect(v.api).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('treats a top-level conditional this assignment as CommonJS', () => {
|
|
for (const [emit, file] of [
|
|
[emitJsScopeCaptures, 'x.js'],
|
|
[emitTsScopeCaptures, 'x.ts'],
|
|
] as const) {
|
|
for (const src of [
|
|
'function api() {}\nif (enabled) this.api = api;\nfunction hidden() {}',
|
|
'function api() {}\nif (enabled) { this.api = api; }\nfunction hidden() {}',
|
|
]) {
|
|
const v = verdicts(emit, src, file);
|
|
expect(v.api).toBeUndefined();
|
|
expect(v.hidden).toBeUndefined();
|
|
}
|
|
}
|
|
});
|
|
|
|
it('does not treat this assignments inside a function as module exports', () => {
|
|
const v = verdicts(
|
|
emitJsScopeCaptures,
|
|
'function wrapper() { if (enabled) this.api = api; }\nfunction hidden() {}',
|
|
'x.js',
|
|
);
|
|
expect(v.hidden).toBe('false');
|
|
});
|
|
|
|
it('recognizes exported module-scoped var inside a block, but not block let or function-local var', () => {
|
|
for (const [emit, file] of [
|
|
[emitJsScopeCaptures, 'x.js'],
|
|
[emitTsScopeCaptures, 'x.ts'],
|
|
] as const) {
|
|
const v = verdicts(
|
|
emit,
|
|
'if (ok) { var visible = 1; let hidden = 2; } function wrapper() { var nested = 3; } export { visible, hidden, nested };',
|
|
file,
|
|
);
|
|
expect(v.visible).toBe('true');
|
|
expect(v.hidden).toBe('false');
|
|
expect(v.nested).toBe('false');
|
|
}
|
|
});
|
|
|
|
it('marks synthesized default-export HOC declarations in both emitters', () => {
|
|
for (const [emit, file] of [
|
|
[emitJsScopeCaptures, 'Widget.jsx'],
|
|
[emitTsScopeCaptures, 'Widget.tsx'],
|
|
] as const) {
|
|
const captures = emit('export default memo(() => <div />);', file).filter(
|
|
(capture) => capture['@declaration.function'] !== undefined,
|
|
);
|
|
expect(captures.length).toBeGreaterThan(0);
|
|
expect(
|
|
captures.every((capture) => capture['@declaration.is-exported']?.text === 'true'),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
'function wrapper(module) { module.exports = {}; }',
|
|
"function wrapper(module) { module['exports'] = {}; }",
|
|
'function wrapper(exports) { exports.alpha = 1; }',
|
|
"const wrapper = exports => { exports['alpha'] = 1; };",
|
|
'function wrapper() { const module = {}; module.exports = {}; }',
|
|
'function wrapper() { { module.exports = {}; let module; } }',
|
|
'function wrapper({ receiver: module }) { module.exports = {}; }',
|
|
'const module = {}; module.exports = { alpha() {} };',
|
|
"import module from './other'; module.exports = {};",
|
|
"import { receiver as exports } from './other'; exports.alpha = 1;",
|
|
"import * as module from './other'; module.exports = {};",
|
|
'const wrapper = function module() { module.exports = {}; };',
|
|
'for (const exports of values) { exports.x = 1; }',
|
|
'for (const module of values) { module.exports = {}; }',
|
|
])('ignores a shadowed CommonJS receiver: %s', (source) => {
|
|
for (const [emit, file] of [
|
|
[emitJsScopeCaptures, 'x.js'],
|
|
[emitTsScopeCaptures, 'x.ts'],
|
|
] as const) {
|
|
const v = verdicts(
|
|
emit,
|
|
`${source}\nexport function publicApi() {}\nfunction hidden() {}`,
|
|
file,
|
|
);
|
|
expect(v.publicApi).toBe('true');
|
|
expect(v.hidden).toBe('false');
|
|
}
|
|
});
|
|
|
|
it('retains real CommonJS writes inside an unshadowed wrapper', () => {
|
|
const v = verdicts(
|
|
emitJsScopeCaptures,
|
|
'function wrapper() { module.exports = {}; }\nfunction hidden() {}',
|
|
'x.js',
|
|
);
|
|
expect(v.hidden).toBeUndefined();
|
|
});
|
|
|
|
it('an imported source name does not shadow a different local alias', () => {
|
|
const v = verdicts(
|
|
emitJsScopeCaptures,
|
|
"import { module as other } from './other'; module.exports = {}; function hidden() {}",
|
|
'x.js',
|
|
);
|
|
expect(v.hidden).toBeUndefined();
|
|
});
|
|
|
|
it.each(["module['exports']", 'module["exports"]'])(
|
|
'recognizes %s object methods and properties as exports',
|
|
(target) => {
|
|
const v = verdicts(
|
|
emitJsScopeCaptures,
|
|
`function helper() {}\n${target} = { alpha() { return 1; }, beta: () => 2 };\n`,
|
|
'lib.js',
|
|
);
|
|
expect(v.alpha).toBe('true');
|
|
expect(v.beta).toBe('true');
|
|
expect(v.helper).toBeUndefined();
|
|
},
|
|
);
|
|
|
|
it('does not confuse dynamic or unrelated module subscripts with exports', () => {
|
|
for (const target of ['module[key]', "module['other']"]) {
|
|
const v = verdicts(emitJsScopeCaptures, `${target} = {};\nfunction hidden() {}`, 'lib.js');
|
|
expect(v.hidden).toBe('false');
|
|
}
|
|
});
|
|
|
|
it('marks ESM declarations', () => {
|
|
const v = verdicts(emitJsScopeCaptures, ESM, 'test.js');
|
|
expect(v.a).toBe('true');
|
|
expect(v.b).toBe('false');
|
|
expect(v.e).toBe('true');
|
|
expect(v.f).toBe('true');
|
|
});
|
|
|
|
it('stays silent for `exports.x =` files', () => {
|
|
const v = verdicts(emitJsScopeCaptures, 'function a() {}\nexports.a = a;\n', 'test.js');
|
|
expect(v.a).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('SymbolDefinition.isExported through the extractor', () => {
|
|
it('lands as a tri-state field: true / false / absent', () => {
|
|
const esm = extract(
|
|
emitTsScopeCaptures('export function a() {}\nfunction b() {}\n', 'x.ts'),
|
|
'x.ts',
|
|
typescriptScopeResolver,
|
|
);
|
|
const byName = new Map(esm.localDefs.map((d) => [d.qualifiedName, d.isExported]));
|
|
expect(byName.get('a')).toBe(true);
|
|
expect(byName.get('b')).toBe(false);
|
|
const cjs = extract(
|
|
emitTsScopeCaptures('function a() {}\nmodule.exports = a;\n', 'y.ts'),
|
|
'y.ts',
|
|
typescriptScopeResolver,
|
|
);
|
|
expect(cjs.localDefs.find((d) => d.qualifiedName === 'a')?.isExported).toBeUndefined();
|
|
expect('isExported' in cjs.localDefs.find((d) => d.qualifiedName === 'a')!).toBe(false);
|
|
});
|
|
});
|