GitNexus/gitnexus/test/unit/scope-resolution/esm-export-marker.test.ts
Abhinav Pandey 2220f4d851
fix(resolution): label fallback guesses and preserve export visibility (#3190)
* 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>
2026-09-10 11:38:19 +01:00

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);
});
});