GitNexus/gitnexus/test/integration/resolvers/barrel-named-import-class-method.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

107 lines
4.5 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import path from 'path';
import fs from 'node:fs';
import os from 'node:os';
import { getRelationships, runPipelineFromRepo, writeFixtureRepo } from './helpers.js';
/**
* `namedImportsBindTopLevelOnly` (ECMAScript): `import { beta }` can never reach a
* class member. Before the hook, `findExportByName`'s callable preference let
* `Foo.beta()` outrank the top-level `const beta = 42` — or bind on its own when no
* top-level `beta` existed — and the import produced a confident CALLS edge to a
* symbol the module cannot export. Incorrect context is worse than missing: the
* Variable shadow must win and emit no edge; the member must never bind.
*/
const impl = `export const beta = 42;\nexport function alpha(s: string) { return s; }\nexport class Foo { beta() { return 1; } }\n`;
const memberOnly = `export function alpha(s: string) { return s; }\nexport class Foo { beta() { return 1; } }\n`;
async function run(name: string, files: Record<string, string>) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `gn-named-member-${name}-`));
try {
writeFixtureRepo(dir, files);
const result = await runPipelineFromRepo(dir, () => {});
return getRelationships(result, 'CALLS')
.filter((e) => e.sourceFilePath.includes('src/main'))
.map((e) => e.target)
.sort();
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
}
describe.each(['ts', 'js'])(
'named imports bind module-level declarations only (%s)',
(extension) => {
// Keep identical scenarios while actually selecting each language's parser
// and separately registered resolver.
const fixture = (files: Record<string, string>) =>
Object.fromEntries(
Object.entries(files).map(([file, content]) => [
file.replace(/\.ts$/, `.${extension}`),
extension === 'js' ? content.replaceAll(': string', '') : content,
]),
);
it('direct named import: a class method sharing a top-level value name emits no edge', async () => {
const targets = await run(
`direct-${extension}`,
fixture({
'package.json': '{ "name": "root", "private": true }\n',
'src/Impl.ts': impl,
'src/main.ts': `import { alpha, beta } from './Impl';\nexport function render() { alpha('a'); beta(); }\n`,
}),
);
expect(targets).toEqual(['alpha']);
}, 60000);
it('direct named import: a class method with NO top-level declaration does not bind', async () => {
const targets = await run(
`member-only-${extension}`,
fixture({
'package.json': '{ "name": "root", "private": true }\n',
'src/Impl.ts': memberOnly,
'src/main.ts': `import { alpha, beta } from './Impl';\nexport function render() { alpha('a'); beta(); }\n`,
}),
);
expect(targets).toEqual(['alpha']);
}, 60000);
it('named re-export through a barrel: same rule', async () => {
const targets = await run(
`barrel-${extension}`,
fixture({
'package.json': '{ "name": "root", "private": true }\n',
'src/Impl.ts': impl,
'src/index.ts': `export { alpha, beta } from './Impl';\n`,
'src/main.ts': `import { alpha, beta } from './index';\nexport function render() { alpha('a'); beta(); }\n`,
}),
);
expect(targets).toEqual(['alpha']);
}, 60000);
it('control: a top-level arrow-const behind the same barrel still binds', async () => {
const targets = await run(
`control-${extension}`,
fixture({
'package.json': '{ "name": "root", "private": true }\n',
'src/Impl.ts': `export const beta = () => 1;\nexport function alpha(s: string) { return s; }\n`,
'src/index.ts': `export { alpha, beta } from './Impl';\n`,
'src/main.ts': `import { alpha, beta } from './index';\nexport function render() { alpha('a'); beta(); }\n`,
}),
);
expect(targets).toEqual(['alpha', 'beta']);
}, 60000);
it('an aliased private import cannot guess a different module-private declaration', async () => {
const targets = await run(
`alias-${extension}`,
fixture({
'package.json': '{ "name": "root", "private": true }\n',
'src/Impl.ts': 'export class Foo { beta() {} }\nfunction renamed() {}\n',
'src/main.ts':
"import { beta as renamed } from './Impl';\nexport function render() { renamed(); }\n",
}),
);
expect(targets).toEqual([]);
}, 60000);
},
);