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>
152 lines
6.6 KiB
TypeScript
152 lines
6.6 KiB
TypeScript
/**
|
|
* M17 — the `export *` wildcard fan-out in `populateFileClosure`
|
|
* (gitnexus-shared/src/scope-resolution/finalize-algorithm.ts) is gated by the
|
|
* SAME `namedImportsBindTopLevelOnly` hook as the named-import path:
|
|
*
|
|
* for (const [name, def] of (topLevelOnly ? indexTopLevelExportsByName : indexExportsByName)(...))
|
|
*
|
|
* Before this fix, the wildcard fan-out ALWAYS used the narrow (top-level-only)
|
|
* index, regardless of the hook — silently adopting ECMAScript's `export *`
|
|
* semantics (a class member can never be published by a bare wildcard
|
|
* re-export) for every language, including ones (Python, Java, ...) whose
|
|
* wildcard/star import legitimately republishes class members by name.
|
|
*
|
|
* This is below the extraction layer (RFC #909 Ring 2 PKG #921) — synthetic
|
|
* `ParsedFile` input against `finalizeScopeModel` with a FAKE resolver
|
|
* (`namedImportsBindTopLevelOnly` toggled directly), same technique as
|
|
* `finalize-orchestrator.test.ts`. No real language parser involved; the
|
|
* fixture below is deliberately language-agnostic (Vue, TypeScript, and
|
|
* JavaScript all opt in through their language-specific scope resolvers).
|
|
*
|
|
* Fixture shape, held constant across both hook settings:
|
|
* B.ts: class Foo with method `beta` — NO top-level `beta` declaration.
|
|
* A.ts: `export * from './B'` (wildcard re-export; populates A's closure).
|
|
* C.ts: `import { beta } from './A'` — resolves through A's closure, which
|
|
* the direct check on A's own (empty) localDefs never satisfies.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import type { ParsedFile, ParsedImport, Scope, ScopeId, SymbolDefinition } from 'gitnexus-shared';
|
|
import { finalizeScopeModel } from '../../../src/core/ingestion/finalize-orchestrator.js';
|
|
import { vueScopeResolver } from '../../../src/core/ingestion/languages/vue/scope-resolver.js';
|
|
import { typescriptScopeResolver } from '../../../src/core/ingestion/languages/typescript/scope-resolver.js';
|
|
import { javascriptScopeResolver } from '../../../src/core/ingestion/languages/javascript/scope-resolver.js';
|
|
|
|
const mkScope = (id: ScopeId, filePath: string): Scope => ({
|
|
id,
|
|
parent: null,
|
|
kind: 'Module',
|
|
range: { startLine: 1, startCol: 0, endLine: 100, endCol: 0 },
|
|
filePath,
|
|
bindings: new Map(),
|
|
ownedDefs: [],
|
|
imports: [],
|
|
typeBindings: new Map(),
|
|
});
|
|
|
|
const mkFile = (filePath: string, overrides: Partial<ParsedFile> = {}): ParsedFile => ({
|
|
filePath,
|
|
moduleScope: `scope:${filePath}#module`,
|
|
scopes: overrides.scopes ?? [mkScope(`scope:${filePath}#module`, filePath)],
|
|
parsedImports: overrides.parsedImports ?? [],
|
|
localDefs: overrides.localDefs ?? [],
|
|
referenceSites: overrides.referenceSites ?? [],
|
|
});
|
|
|
|
function buildFixture(topLevelOnly: boolean) {
|
|
// B.ts: a class with a method `beta`, and NO top-level `beta` of any kind.
|
|
const fooClass: SymbolDefinition = {
|
|
nodeId: 'def:Foo',
|
|
filePath: 'B.ts',
|
|
type: 'Class',
|
|
qualifiedName: 'B.Foo',
|
|
};
|
|
const fooBetaMethod: SymbolDefinition = {
|
|
nodeId: 'def:Foo.beta',
|
|
filePath: 'B.ts',
|
|
type: 'Method',
|
|
ownerId: 'def:Foo',
|
|
qualifiedName: 'B.Foo.beta',
|
|
};
|
|
const fileB = mkFile('B.ts', { localDefs: [fooClass, fooBetaMethod] });
|
|
|
|
// A.ts: `export * from './B'` — a wildcard re-export, no local defs of its own.
|
|
const wildcardImport: ParsedImport = { kind: 'wildcard', targetRaw: 'B.ts' };
|
|
const fileA = mkFile('A.ts', { parsedImports: [wildcardImport] });
|
|
|
|
// C.ts: `import { beta } from './A'`.
|
|
const namedImport: ParsedImport = {
|
|
kind: 'named',
|
|
localName: 'beta',
|
|
importedName: 'beta',
|
|
targetRaw: 'A.ts',
|
|
};
|
|
const fileC = mkFile('C.ts', { parsedImports: [namedImport] });
|
|
|
|
const out = finalizeScopeModel([fileB, fileA, fileC], {
|
|
hooks: {
|
|
resolveImportTarget: (targetRaw) => targetRaw,
|
|
namedImportsBindTopLevelOnly: topLevelOnly,
|
|
},
|
|
});
|
|
|
|
const cImports = out.imports.get(fileC.moduleScope) ?? [];
|
|
return { out, fileC, fooBetaMethod, betaImport: cImports[0] };
|
|
}
|
|
|
|
describe('M17 — export * wildcard fan-out gated by namedImportsBindTopLevelOnly', () => {
|
|
it('a language that does NOT opt in (Python/Java-shaped: hook false) publishes the class method through the wildcard — wide index preserved', () => {
|
|
const { betaImport, fooBetaMethod } = buildFixture(false);
|
|
expect(betaImport).toBeDefined();
|
|
expect(betaImport!.linkStatus).toBeUndefined();
|
|
expect(betaImport!.targetFile).toBe('A.ts');
|
|
expect(betaImport!.targetDefId).toBe(fooBetaMethod.nodeId);
|
|
});
|
|
|
|
it('a language that DOES opt in (ECMAScript-shaped: hook true) refuses — the wildcard fan-out narrows to module-level declarations only', () => {
|
|
const { betaImport } = buildFixture(true);
|
|
expect(betaImport).toBeDefined();
|
|
// Neither A's own (empty) localDefs nor A's wildcard-populated closure
|
|
// (narrowed to MEMBER_LABELS-excluded defs) ever publish `beta` — the
|
|
// import stays unresolved rather than binding a class member no
|
|
// top-level name legitimizes.
|
|
expect(betaImport!.linkStatus).toBe('unresolved');
|
|
expect(betaImport!.targetDefId).toBeUndefined();
|
|
});
|
|
|
|
it('mutation check: a top-level (non-member) def behind the same wildcard still binds under EITHER setting', () => {
|
|
// Control — proves the gate narrows MEMBER labels specifically, not
|
|
// wildcard re-exports wholesale.
|
|
for (const topLevelOnly of [false, true]) {
|
|
const alphaVar: SymbolDefinition = {
|
|
nodeId: 'def:alpha',
|
|
filePath: 'B.ts',
|
|
type: 'Variable',
|
|
qualifiedName: 'B.alpha',
|
|
};
|
|
const fileB = mkFile('B.ts', { localDefs: [alphaVar] });
|
|
const fileA = mkFile('A.ts', {
|
|
parsedImports: [{ kind: 'wildcard', targetRaw: 'B.ts' }],
|
|
});
|
|
const fileC = mkFile('C.ts', {
|
|
parsedImports: [
|
|
{ kind: 'named', localName: 'alpha', importedName: 'alpha', targetRaw: 'A.ts' },
|
|
],
|
|
});
|
|
const out = finalizeScopeModel([fileB, fileA, fileC], {
|
|
hooks: {
|
|
resolveImportTarget: (targetRaw) => targetRaw,
|
|
namedImportsBindTopLevelOnly: topLevelOnly,
|
|
},
|
|
});
|
|
const edge = out.imports.get(fileC.moduleScope)?.[0];
|
|
expect(edge?.linkStatus, `topLevelOnly=${topLevelOnly}`).toBeUndefined();
|
|
expect(edge?.targetDefId, `topLevelOnly=${topLevelOnly}`).toBe('def:alpha');
|
|
}
|
|
});
|
|
|
|
it('Vue opts in (TS semantics); JS/TS themselves already do — every migrated resolver that sets the hook does so as `true`', () => {
|
|
expect(vueScopeResolver.namedImportsBindTopLevelOnly).toBe(true);
|
|
expect(typescriptScopeResolver.namedImportsBindTopLevelOnly).toBe(true);
|
|
expect(javascriptScopeResolver.namedImportsBindTopLevelOnly).toBe(true);
|
|
});
|
|
});
|