mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +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>
159 lines
6.6 KiB
TypeScript
159 lines
6.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { buildScopeTree, finalize, type FinalizeHooks, type ParsedFile } from 'gitnexus-shared';
|
|
import { createKnowledgeGraph } from '../../../src/core/graph/graph.js';
|
|
import { emitImportEdges } from '../../../src/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js';
|
|
import { extractParsedFile } from '../../../src/core/ingestion/scope-extractor-bridge.js';
|
|
import { rustProvider } from '../../../src/core/ingestion/languages/rust.js';
|
|
import { rustIsGlobalNameFallbackPlausible } from '../../../src/core/ingestion/languages/rust/name-fallback-visibility.js';
|
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import {
|
|
persistParsedFileChunk,
|
|
loadParsedFilesForPaths,
|
|
} from '../../../src/storage/parsedfile-store.js';
|
|
|
|
function extract(source: string, filePath = 'src/caller.rs'): ParsedFile {
|
|
const parsed = extractParsedFile(rustProvider, source, filePath);
|
|
if (parsed === undefined) throw new Error(`Failed extraction: ${filePath}`);
|
|
return parsed;
|
|
}
|
|
|
|
const source = `
|
|
mod importing {
|
|
use crate::target::helper;
|
|
pub fn allowed() { helper(); }
|
|
}
|
|
mod sibling {
|
|
pub fn denied() { helper(); }
|
|
}
|
|
`;
|
|
|
|
function link(parsed: ParsedFile, lexical = true) {
|
|
const target = extract('pub fn helper() {}', 'src/target.rs');
|
|
const hooks: FinalizeHooks = {
|
|
resolveImportTarget: (raw: string) =>
|
|
raw.startsWith('crate::target') ? target.filePath : null,
|
|
expandsWildcardTo: () => ['helper'],
|
|
mergeBindings: (existing, incoming) => [...existing, ...incoming],
|
|
importsBindAtLexicalScope: lexical,
|
|
};
|
|
return { target, out: finalize({ files: [parsed, target], workspaceIndex: undefined }, hooks) };
|
|
}
|
|
|
|
describe('import lexical provenance', () => {
|
|
it('keeps file dependency edges when import binding moves into a local scope', () => {
|
|
const parsed = extract(source);
|
|
const { target, out } = link(parsed);
|
|
const graph = createKnowledgeGraph();
|
|
emitImportEdges(
|
|
graph,
|
|
out.imports,
|
|
buildScopeTree([...parsed.scopes, ...target.scopes]),
|
|
'scope import',
|
|
);
|
|
const dependencies = graph.relationships.filter((edge) => edge.type === 'IMPORTS');
|
|
expect(dependencies).toHaveLength(1);
|
|
expect(dependencies[0]!.sourceId).toBe('File:src/caller.rs');
|
|
expect(dependencies[0]!.targetId).toBe('File:src/target.rs');
|
|
});
|
|
|
|
it('does not promote a local wildcard into the importing file export closure', () => {
|
|
const parsed = extract('mod inner { use crate::target::*; }');
|
|
const target = extract('pub fn helper() {}', 'src/target.rs');
|
|
const consumer = extract('use crate::caller::helper;', 'src/consumer.rs');
|
|
const out = finalize(
|
|
{ files: [parsed, target, consumer], workspaceIndex: undefined },
|
|
{
|
|
importsBindAtLexicalScope: true,
|
|
resolveImportTarget: (raw) =>
|
|
raw.startsWith('crate::target') ? target.filePath : parsed.filePath,
|
|
expandsWildcardTo: () => ['helper'],
|
|
mergeBindings: (existing, incoming) => [...existing, ...incoming],
|
|
},
|
|
);
|
|
expect(out.bindings.get(parsed.parsedImports[0]!.declaredAtScope!)?.has('helper')).toBe(true);
|
|
expect(out.imports.get(consumer.moduleScope)?.[0]?.linkStatus).toBe('unresolved');
|
|
expect(out.bindings.get(consumer.moduleScope)?.has('helper')).toBe(false);
|
|
});
|
|
|
|
it('survives the worker/disk ParsedFile round-trip', async () => {
|
|
const parsed = extract(source);
|
|
const dir = await mkdtemp(path.join(tmpdir(), 'gn-import-provenance-'));
|
|
try {
|
|
await persistParsedFileChunk(dir, 'imports', [parsed]);
|
|
const loaded = await loadParsedFilesForPaths(dir, new Set([parsed.filePath]));
|
|
const restored = loaded.get(parsed.filePath)!;
|
|
expect(restored.parsedImports).toEqual(parsed.parsedImports);
|
|
const scopeId = restored.parsedImports[0]!.declaredAtScope!;
|
|
expect(link(restored).out.bindings.get(scopeId)?.has('helper')).toBe(true);
|
|
expect(link(restored).out.bindings.get(restored.moduleScope)?.has('helper')).toBe(false);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
it('retains the declaration scope even for non-executing imports', () => {
|
|
const parsed = extract(source);
|
|
const imported = parsed.parsedImports[0]!;
|
|
expect(imported.declaredAtScope).toBeDefined();
|
|
expect(imported.declaredAtScope).not.toBe(parsed.moduleScope);
|
|
expect(parsed.scopes.find((scope) => scope.id === imported.declaredAtScope)?.kind).toBe(
|
|
'Namespace',
|
|
);
|
|
expect(imported.runsOnlyWhenCalled).toBeUndefined();
|
|
expect(JSON.parse(JSON.stringify(imported)).declaredAtScope).toBe(imported.declaredAtScope);
|
|
});
|
|
|
|
it.each([
|
|
'use crate::target::helper;',
|
|
'use crate::target::helper as alias;',
|
|
'use crate::target::*;',
|
|
])('binds %s only in the importing scope', (declaration) => {
|
|
const parsed = extract(source.replace('use crate::target::helper;', declaration));
|
|
const { out } = link(parsed);
|
|
const scope = parsed.parsedImports[0]!.declaredAtScope!;
|
|
const name = declaration.includes('alias') ? 'alias' : 'helper';
|
|
expect(out.bindings.get(scope)?.get(name)?.[0]?.origin).toMatch(/import|wildcard/);
|
|
expect(out.bindings.get(parsed.moduleScope)?.has(name)).toBe(false);
|
|
expect(out.imports.get(scope)).toHaveLength(1);
|
|
expect(out.stats.totalEdges).toBe(1);
|
|
expect(out.stats.linkedEdges).toBe(1);
|
|
});
|
|
|
|
it('preserves file-level binding for resolvers that have not opted in', () => {
|
|
const parsed = extract(source);
|
|
expect(link(parsed, false).out.bindings.get(parsed.moduleScope)?.has('helper')).toBe(true);
|
|
});
|
|
|
|
it('preserves legacy imports without a scope receipt', () => {
|
|
const parsed = extract(source);
|
|
const legacy = {
|
|
...parsed,
|
|
parsedImports: [
|
|
{
|
|
kind: 'named' as const,
|
|
localName: 'helper',
|
|
importedName: 'helper',
|
|
targetRaw: 'crate::target::helper',
|
|
},
|
|
],
|
|
};
|
|
expect(link(legacy).out.bindings.get(parsed.moduleScope)?.has('helper')).toBe(true);
|
|
});
|
|
|
|
it('does not use a sibling inline module import to authorize a guess', () => {
|
|
const parsed = extract(source);
|
|
const { target } = link(parsed);
|
|
const sites = parsed.referenceSites.filter(
|
|
(site) => site.kind === 'call' && site.name === 'helper',
|
|
);
|
|
expect(sites).toHaveLength(2);
|
|
const candidate = target.localDefs.find((def) => def.qualifiedName === 'helper')!;
|
|
expect(
|
|
rustIsGlobalNameFallbackPlausible({ callerParsed: parsed, candidate, site: sites[0]! }),
|
|
).toBe(true);
|
|
expect(
|
|
rustIsGlobalNameFallbackPlausible({ callerParsed: parsed, candidate, site: sites[1]! }),
|
|
).toBe(false);
|
|
});
|
|
});
|