mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-04 02:31:36 +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>
207 lines
7.6 KiB
TypeScript
207 lines
7.6 KiB
TypeScript
/**
|
|
* End-to-end behaviour of the global-name fallback after WS1-A.
|
|
*
|
|
* Two things used to be true at once and both were wrong:
|
|
*
|
|
* 1. A call to a name that happens to be unique in the repository acquired a
|
|
* CALLS edge even when the language forbids the call outright — Go's
|
|
* unexported identifiers being the clearest case.
|
|
* 2. Every such edge was emitted with `confidence: 0.85` and
|
|
* `reason: 'import-resolved'`, i.e. spelled exactly like an edge a real
|
|
* import produced, so no consumer could discount it.
|
|
*
|
|
* These tests pin both. The Go arm proves the impossible edge is now REFUSED,
|
|
* with a same-file call confirming ordinary local resolution is preserved.
|
|
* That control does not exercise the global-name tier. The Ruby arm proves a
|
|
* surviving guess is LABELED, since Ruby deliberately keeps the tier for
|
|
* autoload. The last test is the regression that matters most: no edge from
|
|
* this tier may ever again carry `import-resolved`.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import path from 'path';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import {
|
|
getRelationships,
|
|
getResolutionOutcomes,
|
|
runPipelineFromRepo,
|
|
writeFixtureRepo,
|
|
type PipelineResult,
|
|
} from './helpers.js';
|
|
import { GLOBAL_NAME_FALLBACK_REASON } from '../../../src/core/graph/edge-reasons.js';
|
|
|
|
const rmRepo = (dir: string | undefined): void => {
|
|
if (dir !== undefined) {
|
|
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
|
}
|
|
};
|
|
|
|
describe('Go: an unexported identifier is not callable from another package', () => {
|
|
let result: PipelineResult;
|
|
let repoDir: string | undefined;
|
|
|
|
beforeAll(async () => {
|
|
repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-ws1-go-fallback-'));
|
|
writeFixtureRepo(repoDir, {
|
|
'go.mod': 'module example.com/mod\n\ngo 1.22\n',
|
|
// Package `a` owns `uniqueHelperXyz`. The name is unique repo-wide, which
|
|
// is the ONLY reason the old fallback matched it from package `b`.
|
|
'a/helper.go': `package a
|
|
|
|
func uniqueHelperXyz() int {
|
|
return 41
|
|
}
|
|
|
|
func UseItLocally() int {
|
|
return uniqueHelperXyz() + 1
|
|
}
|
|
`,
|
|
// Package `b` cannot see `uniqueHelperXyz` under any spelling: Go's
|
|
// lower-case initial makes it package-private, so no import helps.
|
|
'b/caller.go': `package b
|
|
|
|
func CallItRemotely() int {
|
|
return uniqueHelperXyz() + 1
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(repoDir, () => {});
|
|
}, 120000);
|
|
|
|
afterAll(() => rmRepo(repoDir));
|
|
|
|
it('keeps ordinary same-file resolution as a control', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const local = calls.find((c) => c.source === 'UseItLocally' && c.target === 'uniqueHelperXyz');
|
|
expect(local).toBeDefined();
|
|
expect(local!.rel.reason).not.toBe(GLOBAL_NAME_FALLBACK_REASON);
|
|
});
|
|
|
|
it('emits NO caller edge from the other package', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const crossPackage = calls.filter(
|
|
(c) => c.source === 'CallItRemotely' && c.target === 'uniqueHelperXyz',
|
|
);
|
|
expect(crossPackage).toEqual([]);
|
|
});
|
|
|
|
it('records the drop as a refusal rather than losing it silently', () => {
|
|
const refusals = getResolutionOutcomes(result).filter(
|
|
(o) => o.kind === 'fallback-refused' && o.name === 'uniqueHelperXyz',
|
|
);
|
|
expect(refusals.length).toBeGreaterThan(0);
|
|
expect(refusals.every((o) => o.kind === 'fallback-refused' && o.language === 'go')).toBe(true);
|
|
});
|
|
|
|
it('lists no cross-package caller for the unexported helper at all', () => {
|
|
// The shape an `impact --direction upstream` answer is built from: every
|
|
// CALLS edge whose target is the helper. Package `b` must not appear.
|
|
const callers = getRelationships(result, 'CALLS')
|
|
.filter((c) => c.target === 'uniqueHelperXyz')
|
|
.map((c) => c.sourceFilePath);
|
|
expect(callers.some((filePath) => filePath.includes('b/caller.go'))).toBe(false);
|
|
expect(callers.some((filePath) => filePath.includes('a/helper.go'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Go/JS: constructor-form unique-name guesses are labeled, not import-resolved', () => {
|
|
it('refuses an unexported Go composite literal from another package', async () => {
|
|
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-ws1-go-ctor-'));
|
|
try {
|
|
writeFixtureRepo(repoDir, {
|
|
'go.mod': 'module example.com/mod\n\ngo 1.22\n',
|
|
'a/t.go': `package a
|
|
|
|
type uniqueWidget struct{}
|
|
`,
|
|
'b/c.go': `package b
|
|
|
|
func F() { _ = uniqueWidget{} }
|
|
`,
|
|
});
|
|
const result = await runPipelineFromRepo(repoDir, () => {});
|
|
const cross = getRelationships(result, 'CALLS').filter(
|
|
(c) => c.source === 'F' && c.target === 'uniqueWidget',
|
|
);
|
|
expect(cross).toEqual([]);
|
|
const refusals = getResolutionOutcomes(result).filter(
|
|
(o) => o.kind === 'fallback-refused' && o.name === 'uniqueWidget',
|
|
);
|
|
expect(refusals.length).toBeGreaterThan(0);
|
|
} finally {
|
|
rmRepo(repoDir);
|
|
}
|
|
}, 120000);
|
|
|
|
it('labels a JS `new UniqueWidget()` guess instead of import-resolved', async () => {
|
|
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-ws1-js-ctor-'));
|
|
try {
|
|
writeFixtureRepo(repoDir, {
|
|
'package.json': '{ "name": "ctor-fallback", "private": true }\n',
|
|
'b/widget.js': `export class UniqueWidget {}\n`,
|
|
'a/consumer.js': `export function make() { return new UniqueWidget(); }\n`,
|
|
});
|
|
const result = await runPipelineFromRepo(repoDir, () => {});
|
|
const edge = getRelationships(result, 'CALLS').find(
|
|
(c) => c.source === 'make' && c.target === 'UniqueWidget',
|
|
);
|
|
expect(edge).toBeDefined();
|
|
expect(edge!.rel.reason).toBe(GLOBAL_NAME_FALLBACK_REASON);
|
|
expect(edge!.rel.confidence).toBe(0.5);
|
|
} finally {
|
|
rmRepo(repoDir);
|
|
}
|
|
}, 120000);
|
|
});
|
|
|
|
describe('Ruby: a surviving name guess is labeled as a guess', () => {
|
|
let result: PipelineResult;
|
|
let repoDir: string | undefined;
|
|
|
|
beforeAll(async () => {
|
|
repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-ws1-ruby-fallback-'));
|
|
writeFixtureRepo(repoDir, {
|
|
// A top-level method in one file, called from another with no `require` —
|
|
// the autoload shape Ruby keeps the fallback for. The edge is a guess and
|
|
// is allowed to exist, but it must say so.
|
|
'app/a.rb': `def unique_helper_xyz
|
|
41
|
|
end
|
|
`,
|
|
'app/b.rb': `def call_it
|
|
unique_helper_xyz()
|
|
end
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(repoDir, () => {});
|
|
}, 120000);
|
|
|
|
afterAll(() => rmRepo(repoDir));
|
|
|
|
it('emits the edge with the guess reason and 0.5 confidence', () => {
|
|
const edge = getRelationships(result, 'CALLS').find(
|
|
(c) => c.source === 'call_it' && c.target === 'unique_helper_xyz',
|
|
);
|
|
expect(edge).toBeDefined();
|
|
expect(edge!.rel.reason).toBe(GLOBAL_NAME_FALLBACK_REASON);
|
|
expect(edge!.rel.confidence).toBe(0.5);
|
|
});
|
|
|
|
it('counts the guess so a reader can see how much of the graph is guessed', () => {
|
|
const guesses = getResolutionOutcomes(result).filter(
|
|
(o) => o.kind === 'fallback-guessed' && o.name === 'unique_helper_xyz',
|
|
);
|
|
expect(guesses.length).toBeGreaterThan(0);
|
|
expect(guesses.every((o) => o.kind === 'fallback-guessed' && o.language === 'ruby')).toBe(true);
|
|
});
|
|
|
|
it('REGRESSION: no guessed edge is spelled like an import-resolved one', () => {
|
|
// The specific lie this work removed. Asserted over the whole graph, not
|
|
// just the one edge, so a future emitter cannot reintroduce it elsewhere.
|
|
const mislabeled = getRelationships(result, 'CALLS').filter(
|
|
(c) => c.rel.reason === 'import-resolved',
|
|
);
|
|
expect(mislabeled).toEqual([]);
|
|
});
|
|
});
|