Merge final Go stack into workspace split

This commit is contained in:
Abhinav Pandey 2026-09-06 09:46:13 +05:30
commit c13e44854b
No known key found for this signature in database
9 changed files with 142 additions and 25 deletions

View file

@ -10,7 +10,7 @@
* then only if the declaration is `public`.
*
* A module is approximated by its source directory, the layout every Swift
* package manifest produces: `Sources/<Target>/…` and `Tests/<Target>/…`.
* package configuration discovers: `Sources/<Target>/…` (also under `Package/`).
* `src/<Target>/…` is also recognized by the package configuration loader.
* Files outside these layouts have unknown module identity.
*
@ -24,14 +24,8 @@
import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared';
import { modulePathReaches } from '../../scope-resolution/utils/name-fallback-visibility.js';
/** Directory names that hold one subdirectory PER TARGET rather than sources. */
const SWIFT_TARGET_ROOTS: ReadonlySet<string> = new Set([
'Sources',
'Tests',
'sources',
'tests',
'src',
]);
/** Keep aligned with loadSwiftPackageConfig: other layouts are unconfigured. */
const SWIFT_TARGET_ROOTS: ReadonlySet<string> = new Set(['Sources', 'src']);
/**
* The module (target) a Swift file belongs to.

View file

@ -195,7 +195,8 @@ export function formatNameFallbackSummary(
summary.distinctGuessedPairs !== undefined
? ` (${summary.distinctGuessedPairs} distinct caller-file/name pairs)`
: '';
return `name-fallback resolution: ${summary.totalGuessed} call sites${pairs}, ${summary.totalRefused} refused as impossible (guessed/refused by language: ${languages})${ambiguous}`;
const siteUnit = summary.totalGuessed === 1 ? 'call site' : 'call sites';
return `name-fallback resolution: ${summary.totalGuessed} ${siteUnit}${pairs}, ${summary.totalRefused} refused as impossible (guessed/refused by language: ${languages})${ambiguous}`;
}
/**

View file

@ -44,8 +44,6 @@ export interface EsmExportEvidence {
readonly commonJs: boolean;
}
const CJS_EXPORT_ASSIGNMENT = /^\s*(this\.[A-Za-z_$][\w$]*\s*=)/;
/** Read binding patterns, never initializer expressions or property keys. */
function bindsReceiver(node: SyntaxNode | null, name: string): boolean {
if (node === null) return false;
@ -57,18 +55,33 @@ function bindsReceiver(node: SyntaxNode | null, name: string): boolean {
if (node.type === 'assignment_pattern')
return bindsReceiver(node.childForFieldName('left'), name);
if (node.type === 'pair_pattern') return bindsReceiver(node.childForFieldName('value'), name);
if (node.type === 'import_specifier') {
return bindsReceiver(node.childForFieldName('alias') ?? node.childForFieldName('name'), name);
}
if (node.type === 'required_parameter' || node.type === 'optional_parameter') {
return bindsReceiver(node.childForFieldName('pattern'), name);
}
return (
['formal_parameters', 'object_pattern', 'array_pattern', 'rest_pattern'].includes(node.type) &&
node.namedChildren.some((child) => bindsReceiver(child, name))
[
'formal_parameters',
'object_pattern',
'array_pattern',
'rest_pattern',
'import_clause',
'named_imports',
'namespace_import',
].includes(node.type) && node.namedChildren.some((child) => bindsReceiver(child, name))
);
}
/** A locally bound `module`/`exports` is not Node's export receiver. */
function isExportReceiverShadowed(node: SyntaxNode, name: string): boolean {
for (let scope = node.parent; scope !== null; scope = scope.parent) {
if (
['function_expression', 'generator_function', 'class'].includes(scope.type) &&
scope.childForFieldName('name')?.text === name
)
return true;
if (
bindsReceiver(scope.childForFieldName('parameters'), name) ||
bindsReceiver(scope.childForFieldName('parameter'), name)
@ -81,6 +94,13 @@ function isExportReceiverShadowed(node: SyntaxNode, name: string): boolean {
? statement.childForFieldName('declaration')
: statement;
if (declaration === null) continue;
if (
declaration.type === 'import_statement' &&
declaration.namedChildren.some(
(child) => child.type === 'import_clause' && bindsReceiver(child, name),
)
)
return true;
if (
declaration.type === 'lexical_declaration' ||
declaration.type === 'variable_declaration'
@ -197,8 +217,16 @@ export function collectEsmExportEvidence(
namedLocals.add(child.text);
}
}
} else if (stmt.type === 'expression_statement' && CJS_EXPORT_ASSIGNMENT.test(stmt.text)) {
commonJs = true;
} else if (stmt.type === 'expression_statement') {
const assignment = stmt.namedChildren[0];
const left =
assignment?.type === 'assignment_expression' ? assignment.childForFieldName('left') : null;
if (
left !== null &&
(left.type === 'member_expression' || left.type === 'subscript_expression') &&
left.childForFieldName('object')?.type === 'this'
)
commonJs = true;
}
}
return { namedLocals, commonJs };
@ -235,7 +263,9 @@ export function esmExportVerdict(
// program without crossing a nesting boundary — one inside a namespace or
// ambient-module body is that container's export (see NESTING_BOUNDARIES).
let underExport = false;
let functionScopedVariable = false;
while (current !== null && current.type !== 'program') {
if (current.type === 'variable_declaration') functionScopedVariable = true;
if (current.type === 'export_statement') {
underExport = true;
current = current.parent;
@ -248,6 +278,12 @@ export function esmExportVerdict(
return evidence.commonJs ? undefined : false;
}
if (NESTING_BOUNDARIES.has(current.type) && current.id !== nameNode.parent?.id) {
// `var` crosses blocks but never a function/namespace boundary. A
// module-scoped `var` can therefore be exported by a later clause.
if (current.type === 'statement_block' && functionScopedVariable) {
current = current.parent;
continue;
}
return evidence.commonJs ? undefined : false;
}
current = current.parent;

View file

@ -11,8 +11,8 @@
* import produced, so no consumer could discount it.
*
* These tests pin both. The Go arm proves the impossible edge is now REFUSED,
* with the same-package call kept as the control that shows the refusal is
* targeted rather than a blanket disabling of the tier. The Ruby arm proves a
* 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`.
@ -71,10 +71,11 @@ func CallItRemotely() int {
afterAll(() => rmRepo(repoDir));
it('keeps the same-package call (control: the tier still works)', () => {
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', () => {

View file

@ -150,6 +150,32 @@ describe('@declaration.is-exported — Opus review follow-ups', () => {
});
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('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'],
@ -174,6 +200,10 @@ describe('@declaration.is-exported (JavaScript emitter)', () => {
'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 = {}; };',
])('ignores a shadowed CommonJS receiver: %s', (source) => {
for (const [emit, file] of [
[emitJsScopeCaptures, 'x.js'],
@ -198,6 +228,15 @@ describe('@declaration.is-exported (JavaScript emitter)', () => {
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) => {

View file

@ -231,7 +231,7 @@ describe('free-call dedup: the label is decided from every collapsed site, never
const summary = summarizeNameFallback(outcomes);
expect(summary?.totalGuessed).toBe(1);
const line = formatNameFallbackSummary(summary);
expect(line).toContain('name-fallback resolution: 1 call sites');
expect(line).toContain('name-fallback resolution: 1 call site (');
expect(line).not.toContain('CALLS edges');
}
});

View file

@ -224,9 +224,15 @@ describe('formatNameFallbackSummary', () => {
const line = formatNameFallbackSummary(
summarizeNameFallback([guessed('ruby'), refused('go'), refused('go'), refused('go')]),
);
expect(line).toContain('1 call sites (1 distinct caller-file/name pairs)');
expect(line).toContain('1 call site (1 distinct caller-file/name pairs)');
expect(line).toContain('3 refused as impossible');
// `go` has more total activity, so it leads.
expect(line).toMatch(/go 0\/3.*ruby 1\/0/);
});
it('uses plural call sites for multiple guesses', () => {
expect(
formatNameFallbackSummary(summarizeNameFallback([guessed('go'), guessed('go')])),
).toContain('2 call sites (');
});
});

View file

@ -509,6 +509,17 @@ describe('Rust: isGlobalNameFallbackPlausible', () => {
});
describe('Swift: isGlobalNameFallbackPlausible', () => {
it.each(['sources', 'tests', 'Tests'])(
'does not invent target modules under unconfigured %s folders',
(root) => {
expect(
swiftIsGlobalNameFallbackPlausible({
callerParsed: mkCaller(`${root}/App/Caller.swift`),
candidate: mkCandidate(`${root}/Core/Helper.swift`, 'helper'),
}),
).toBe(true);
},
);
it('does not invent module boundaries between arbitrary Xcode directories', () => {
expect(
swiftIsGlobalNameFallbackPlausible({

View file

@ -20,17 +20,46 @@ describe('collectResolvedCalleeNames', () => {
fn('b', 'b', 'src/b.go');
fn('c', 'c', 'src/c.ts');
g.addNode({ id: 'file', label: 'File' as NodeLabel, properties: { filePath: 'src/a.go' } });
g.addRelationship({ id: 'r1', sourceId: 'a', targetId: 'b', type: 'CALLS', confidence: 0.85 });
g.addRelationship({ id: 'r2', sourceId: 'a', targetId: 'c', type: 'CALLS', confidence: 0.5 });
g.addRelationship({ id: 'r3', sourceId: 'c', targetId: 'b', type: 'CALLS', confidence: 0.85 });
g.addRelationship({
id: 'r1',
sourceId: 'a',
targetId: 'b',
type: 'CALLS',
confidence: 0.85,
reason: '',
});
g.addRelationship({
id: 'r2',
sourceId: 'a',
targetId: 'c',
type: 'CALLS',
confidence: 0.5,
reason: '',
});
g.addRelationship({
id: 'r3',
sourceId: 'c',
targetId: 'b',
type: 'CALLS',
confidence: 0.85,
reason: '',
});
g.addRelationship({
id: 'r4',
sourceId: 'file',
targetId: 'a',
type: 'DEFINES',
confidence: 1,
reason: '',
});
g.addRelationship({
id: 'r5',
sourceId: 'a',
targetId: 'file',
type: 'CALLS',
confidence: 1,
reason: '',
});
g.addRelationship({ id: 'r5', sourceId: 'a', targetId: 'file', type: 'CALLS', confidence: 1 });
const index = collectResolvedCalleeNames(g, g);
expect([...index.keys()].sort()).toEqual(['a', 'c']);