Merge reviewed Go stack into workspace split

This commit is contained in:
Abhinav Pandey 2026-09-06 09:37:29 +05:30
commit 5d87d17619
No known key found for this signature in database
16 changed files with 300 additions and 71 deletions

View file

@ -82,19 +82,18 @@ export function goIsGlobalNameFallbackPlausible(ctx: {
const cand = classifyGoFile(ctx.candidate.filePath, ctx.sourceTextOf);
// Non-test files never see test-only declarations.
if (cand.isTest && !caller.isTest) return false;
// An external test package and its tested package are different packages:
// a BARE name cannot cross that boundary in either direction. Undecidable
// (no package clause available) → allow.
// Different clauses require an explicit dot import, even in one directory.
// Fall through to the exported/import checks for external test packages.
// Missing source leaves package identity undecidable.
if (
caller.declared !== undefined &&
cand.declared !== undefined &&
caller.declared !== cand.declared
caller.declared === undefined ||
cand.declared === undefined ||
caller.declared === cand.declared
)
return false;
return true;
return true;
}
// Different directory, so a different package — and a `_test.go` file's
// Different package — and a `_test.go` file's
// declarations are compiled only into ITS OWN package's test binary. No other
// package, test or not, can see them, exported or not. Decidable from the
// path alone, so it comes before every exception below (the module-root

View file

@ -1127,6 +1127,13 @@ export function emitJsScopeCaptures(
fnNode,
deriveDefaultExportHocName(filePath),
);
// This declaration's name is synthetic, so the later query-name
// marker cannot see it. The HOC predicate already proves the export.
grouped['@declaration.is-exported'] = syntheticCapture(
'@declaration.is-exported',
fnNode,
'true',
);
}
}

View file

@ -8,21 +8,15 @@
* bare free call and never reaches this tier it carries a qualified name and
* is resolved earlier by `resolveQualifiedFreeCall`).
*
* One rule therefore covers both halves the visibility question splits into:
*
* - A non-`pub` item cannot be `use`d from outside its module at all, so the
* absence of a covering `use` correctly refuses it.
* - A `pub` item is reachable, but only from a file that actually wrote the
* `use`, which is the same check.
*
* That is why this does not need to read the `pub` marker, which
* `SymbolDefinition` does not carry. It asks the decidable question "did this
* file bring the name's module into scope?" instead of the undecidable one.
* This hook checks import evidence, not Rust item visibility. A child module
* can use private ancestor items, and visibility restrictions such as
* `pub(crate)` require more context than `SymbolDefinition` carries. A matching
* import therefore keeps a labeled guess rather than proving accessibility.
*
* Module paths are matched against the candidate's FILE path (extension
* stripped, and `mod`/`lib`/`main` stem dropped, since `a/b/mod.rs` IS module
* `a::b`). `use` targets are `::`-separated and `crate::`/`super::` prefixes
* contribute no segments, so suffix matching lines the two up.
* `a::b`). `crate::` names the root; `self::` and `super::` resolve relative
* to the caller's module before comparison.
*/
import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared';
@ -39,7 +33,7 @@ const RUST_DIRECTORY_MODULE_STEMS: ReadonlySet<string> = new Set(['mod', 'lib',
const RUST_CRATE_ROOT_DIRS: ReadonlySet<string> = new Set(['src', 'tests', 'benches', 'examples']);
/** Path prefixes of a `use` that name a root rather than a module segment. */
const RUST_USE_ROOT_PREFIXES: ReadonlySet<string> = new Set(['crate', 'self', 'super', '$crate']);
const RUST_USE_ROOT_PREFIXES: ReadonlySet<string> = new Set(['crate', '$crate']);
/**
* The module path a Rust file provides, as a `/`-joined path.
@ -57,9 +51,18 @@ function rustModulePathOf(filePath: string): string {
return segments.join('/');
}
/** A `use` target with its root prefix dropped: `crate::a::b` → `a::b`. */
function rustUsePathOf(targetRaw: string): string {
/** Resolve explicit relative prefixes against the caller's module path. */
function rustUsePathOf(targetRaw: string, callerFilePath: string): string {
const segments = targetRaw.split('::').filter((s) => s !== '');
if (segments[0] === 'self' || segments[0] === 'super') {
const base = rustModulePathOf(callerFilePath).split('/').filter(Boolean);
if (segments[0] === 'self') segments.shift();
while (segments[0] === 'super') {
base.pop();
segments.shift();
}
return [...base, ...segments].join('::');
}
while (segments.length > 0 && RUST_USE_ROOT_PREFIXES.has(segments[0]!)) segments.shift();
return segments.join('::');
}
@ -84,7 +87,7 @@ export function rustIsGlobalNameFallbackPlausible(ctx: {
const candidateName = rustSimpleNameOf(ctx.candidate);
for (const imp of ctx.callerParsed.parsedImports) {
const usePath = rustUsePathOf(imp.targetRaw);
const usePath = rustUsePathOf(imp.targetRaw, ctx.callerParsed.filePath);
// Only a glob introduces every bare item of a module. A named import must
// match both the candidate's original name and the call's local spelling.
if (imp.kind === 'wildcard') {

View file

@ -10,8 +10,9 @@
* 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>/…`. Files
* outside that layout fall back to their top-level directory.
* package manifest produces: `Sources/<Target>/…` and `Tests/<Target>/…`.
* `src/<Target>/…` is also recognized by the package configuration loader.
* Files outside these layouts have unknown module identity.
*
* The `private` / `fileprivate` half of the rule is NOT implemented, because
* neither marker is recoverable from the parse model this hook sees
@ -24,21 +25,26 @@ 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']);
const SWIFT_TARGET_ROOTS: ReadonlySet<string> = new Set([
'Sources',
'Tests',
'sources',
'tests',
'src',
]);
/**
* The module (target) a Swift file belongs to.
*
* `Sources/Core/User.swift` `Core`. A path with no target root returns its
* first segment, so a flat repository still groups its files together instead
* of putting every file in its own module.
* `Sources/Core/User.swift` `Core`. Arbitrary Xcode folders are not module
* boundaries; without a recognized target layout leave the answer unknown.
*/
function swiftModuleOf(filePath: string): string {
const segments = filePath.split('/').filter((s) => s !== '');
for (let i = 0; i < segments.length - 1; i++) {
for (let i = 0; i < segments.length - 2; i++) {
if (SWIFT_TARGET_ROOTS.has(segments[i]!)) return segments[i + 1]!;
}
return segments.length > 1 ? segments[0]! : '';
return '';
}
export function swiftIsGlobalNameFallbackPlausible(ctx: {

View file

@ -564,6 +564,13 @@ export function emitTsScopeCaptures(
fnNode,
deriveDefaultExportHocName(filePath),
);
// This declaration's name is synthetic, so the later query-name
// marker cannot see it. The HOC predicate already proves the export.
grouped['@declaration.is-exported'] = syntheticCapture(
'@declaration.is-exported',
fnNode,
'true',
);
}
}

View file

@ -1,7 +1,8 @@
/**
* Per-language census of the global-name fallback: how many CALLS edges rest on
* a unique-name guess, and how many guesses each language's visibility rules
* refused.
* Per-language census of global-name fallback decisions: how many call sites
* used a unique-name guess, and how many candidates visibility rules refused.
* Sites are counted before edge coalescing: a precisely bound site can prove
* the same caller/target dependency, so this is not a count of heuristic edges.
*
* Both halves are needed and neither is meaningful alone. A guess count with no
* refusal count cannot distinguish a language with genuinely few impossible
@ -60,7 +61,7 @@ export function countCallsByLanguage(
const UNKNOWN_LANGUAGE = 'unknown';
export interface NameFallbackLanguageCounts {
/** Labeled `global-name-fallback` edges emitted for this language — CALL SITES. */
/** Call sites resolved by a unique-name guess, before edge coalescing. */
readonly guessed: number;
/**
* Distinct (caller file, callee name) pairs among those sites the unit
@ -118,9 +119,9 @@ export function summarizeNameFallback(
const guessedPairsByLanguage = new Map<string, number>();
const refused = new Map<string, number>();
const ambiguousNames = new Set<string>();
// Two units, both kept. `guessed` counts call SITES — the number of emitted
// guessed edges, which is what the log line has always reported and what
// earlier persisted summaries hold. `guessedPairs` dedupes by (caller file,
// Two units, both kept. `guessed` counts call SITES, not final graph edges:
// a precise site may prove the same dependency during edge coalescing.
// Preserve the unit earlier summaries hold. `guessedPairs` dedupes by (caller file,
// callee name), the unit `callsByLanguage` is counted in: ten guessed `foo()`
// calls in one file are one pair against a denominator that counts `foo`
// once, so the guessy RATIO uses pairs and is bounded by 1. Changing the unit
@ -194,7 +195,7 @@ export function formatNameFallbackSummary(
summary.distinctGuessedPairs !== undefined
? ` (${summary.distinctGuessedPairs} distinct caller-file/name pairs)`
: '';
return `name-guessed CALLS edges: ${summary.totalGuessed} call sites${pairs}, ${summary.totalRefused} refused as impossible (guessed/refused by language: ${languages})${ambiguous}`;
return `name-fallback resolution: ${summary.totalGuessed} call sites${pairs}, ${summary.totalRefused} refused as impossible (guessed/refused by language: ${languages})${ambiguous}`;
}
/**

View file

@ -46,10 +46,65 @@ export interface EsmExportEvidence {
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;
if (node.type === 'identifier' || node.type === 'shorthand_property_identifier_pattern') {
return node.text === name;
}
if (node.type === 'variable_declarator')
return bindsReceiver(node.childForFieldName('name'), name);
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 === '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))
);
}
/** 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 (
bindsReceiver(scope.childForFieldName('parameters'), name) ||
bindsReceiver(scope.childForFieldName('parameter'), name)
)
return true;
if (scope.type !== 'program' && scope.type !== 'statement_block') continue;
for (const statement of scope.namedChildren) {
const declaration =
statement.type === 'export_statement'
? statement.childForFieldName('declaration')
: statement;
if (declaration === null) continue;
if (
declaration.type === 'lexical_declaration' ||
declaration.type === 'variable_declaration'
) {
if (declaration.namedChildren.some((child) => bindsReceiver(child, name))) return true;
} else if (
['function_declaration', 'class_declaration'].includes(declaration.type) &&
declaration.childForFieldName('name')?.text === name
)
return true;
}
}
return false;
}
/** Static dot and bracket spellings of the same CommonJS export object. */
function isModuleExportsReference(node: SyntaxNode): boolean {
const object = node.childForFieldName('object');
if (object?.type !== 'identifier' || object.text !== 'module') return false;
if (
object?.type !== 'identifier' ||
object.text !== 'module' ||
isExportReceiverShadowed(node, 'module')
)
return false;
if (node.type === 'member_expression') {
return node.childForFieldName('property')?.text === 'exports';
}
@ -68,12 +123,22 @@ function hasCommonJsExportSurface(root: SyntaxNode): boolean {
for (const member of root.descendantsOfType('member_expression')) {
const object = member.childForFieldName('object');
if (object === null) continue;
if (object.type === 'identifier' && object.text === 'exports') return true;
if (
object.type === 'identifier' &&
object.text === 'exports' &&
!isExportReceiverShadowed(member, 'exports')
)
return true;
if (isModuleExportsReference(member)) return true;
}
for (const sub of root.descendantsOfType('subscript_expression')) {
const object = sub.childForFieldName('object');
if (object?.type === 'identifier' && object.text === 'exports') return true;
if (
object?.type === 'identifier' &&
object.text === 'exports' &&
!isExportReceiverShadowed(sub, 'exports')
)
return true;
if (isModuleExportsReference(sub)) return true;
}
return false;

View file

@ -3870,9 +3870,9 @@ async function runFullAnalysisInner(
const resolutionOutcomes = pipelineResult.resolutionOutcomes ?? [];
logUnresolvedReceiverFiles(resolutionOutcomes);
// Census of name-guessed CALLS edges (labeled `global-name-fallback`), refused
// impossibles and ambiguous `export *` names — the honesty readout for this
// run's resolution. Logged, and persisted below as `nameFallbackEdges`.
// Census of guessed call sites (before edge coalescing), refused candidates
// and ambiguous `export *` names. The legacy `nameFallbackEdges` metadata
// key stores site counts, not the final population of heuristic edges.
const nameFallbackSummary = summarizeNameFallback(
resolutionOutcomes,
countCallsByLanguage(pipelineResult.resolvedCalleeNamesByCaller, pipelineResult.graph),

View file

@ -313,10 +313,12 @@ export interface RepoMeta {
*/
undecidedInterfaceSatisfaction?: UndecidedSatisfactionSummary;
/**
* Census of the name-guessed CALLS edges the run emitted (labeled
* `global-name-fallback`), the impossible ones it refused, and the ambiguous
* Census of name-guessed call sites before edge coalescing, the impossible
* candidates the run refused, and the ambiguous
* `export *` names it declined to publish. Absent on indexes built before the
* census existed. See `scope-resolution/name-fallback-summary.ts`.
* census existed. The legacy key does not imply final edge counts: a precise
* site may prove a dependency shared with a guessed site.
* See `scope-resolution/name-fallback-summary.ts`.
*/
nameFallbackEdges?: NameFallbackSummary;
/**

View file

@ -127,9 +127,10 @@ export function render() {
// — so the label, not just the name, is the assertion that actually catches
// a regression here.
it('every arrow-const winner through the wildcard chain is the Function def, not the Variable shadow', () => {
const byTarget = new Map(callsFromMain().map((e) => [e.target, e.targetLabel]));
expect(byTarget.get('Button')).toBe('Function');
expect(byTarget.get('LinkButton')).toBe('Function');
expect(byTarget.get('clearButtonStyles')).toBe('Function');
const calls = callsFromMain();
for (const name of ['Button', 'LinkButton', 'clearButtonStyles']) {
const labels = calls.filter((edge) => edge.target === name).map((edge) => edge.targetLabel);
expect(labels, name).toEqual(['Function']);
}
});
});

View file

@ -17,14 +17,16 @@ const memberOnly = `export function alpha(s: string) { return s; }\nexport class
async function run(name: string, files: Record<string, string>) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `gn-named-member-${name}-`));
writeFixtureRepo(dir, files);
const result = await runPipelineFromRepo(dir, () => {});
const targets = getRelationships(result, 'CALLS')
.filter((e) => e.sourceFilePath.includes('src/main'))
.map((e) => e.target)
.sort();
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
return targets;
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'])(

View file

@ -149,7 +149,7 @@ end
// 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.confidence === 0.5 && c.rel.reason === 'import-resolved',
(c) => c.rel.reason === 'import-resolved',
);
expect(mislabeled).toEqual([]);
});

View file

@ -150,6 +150,54 @@ describe('@declaration.is-exported — Opus review follow-ups', () => {
});
describe('@declaration.is-exported (JavaScript emitter)', () => {
it('marks synthesized default-export HOC declarations in both emitters', () => {
for (const [emit, file] of [
[emitJsScopeCaptures, 'Widget.jsx'],
[emitTsScopeCaptures, 'Widget.tsx'],
] as const) {
const captures = emit('export default memo(() => <div />);', file).filter(
(capture) => capture['@declaration.function'] !== undefined,
);
expect(captures.length).toBeGreaterThan(0);
expect(
captures.every((capture) => capture['@declaration.is-exported']?.text === 'true'),
).toBe(true);
}
});
it.each([
'function wrapper(module) { module.exports = {}; }',
"function wrapper(module) { module['exports'] = {}; }",
'function wrapper(exports) { exports.alpha = 1; }',
"const wrapper = exports => { exports['alpha'] = 1; };",
'function wrapper() { const module = {}; module.exports = {}; }',
'function wrapper() { { module.exports = {}; let module; } }',
'function wrapper({ receiver: module }) { module.exports = {}; }',
'const module = {}; module.exports = { alpha() {} };',
])('ignores a shadowed CommonJS receiver: %s', (source) => {
for (const [emit, file] of [
[emitJsScopeCaptures, 'x.js'],
[emitTsScopeCaptures, 'x.ts'],
] as const) {
const v = verdicts(
emit,
`${source}\nexport function publicApi() {}\nfunction hidden() {}`,
file,
);
expect(v.publicApi).toBe('true');
expect(v.hidden).toBe('false');
}
});
it('retains real CommonJS writes inside an unshadowed wrapper', () => {
const v = verdicts(
emitJsScopeCaptures,
'function wrapper() { module.exports = {}; }\nfunction hidden() {}',
'x.js',
);
expect(v.hidden).toBeUndefined();
});
it.each(["module['exports']", 'module["exports"]'])(
'recognizes %s object methods and properties as exports',
(target) => {

View file

@ -31,6 +31,11 @@ import { emitFreeCallFallback } from '../../../src/core/ingestion/scope-resoluti
import { buildWorkspaceResolutionIndex } from '../../../src/core/ingestion/scope-resolution/workspace-index.js';
import { createSemanticModel } from '../../../src/core/ingestion/model/semantic-model.js';
import { GLOBAL_NAME_FALLBACK_REASON } from '../../../src/core/graph/edge-reasons.js';
import {
formatNameFallbackSummary,
summarizeNameFallback,
} from '../../../src/core/ingestion/scope-resolution/name-fallback-summary.js';
import type { ResolutionOutcome } from '../../../src/core/ingestion/scope-resolution/resolution-outcome.js';
const CALLER_FILE = 'caller.ts';
const TARGET_FILE = 'target.ts';
@ -162,7 +167,7 @@ function run(sites: readonly ReferenceSite[]) {
const graph = createKnowledgeGraph();
fnNode(graph, 'fn:main', 'main', CALLER_FILE);
fnNode(graph, 'fn:helper', 'helper', TARGET_FILE);
const outcomes: { kind: string }[] = [];
const outcomes: ResolutionOutcome[] = [];
emitFreeCallFallback(
graph,
indexes,
@ -214,4 +219,20 @@ describe('free-call dedup: the label is decided from every collapsed site, never
expect(calls[0]!.confidence).toBe(0.5);
expect(calls[0]!.reason).toBe(GLOBAL_NAME_FALLBACK_REASON);
});
it('reports guessed sites without claiming they are final guessed edges', () => {
for (const sites of [
[guessedSite(3), preciseSite(4)],
[preciseSite(3), guessedSite(4)],
]) {
const { calls, outcomes } = run(sites);
expect(calls).toHaveLength(1);
expect(calls[0]!.reason).toBe('import-resolved');
const summary = summarizeNameFallback(outcomes);
expect(summary?.totalGuessed).toBe(1);
const line = formatNameFallbackSummary(summary);
expect(line).toContain('name-fallback resolution: 1 call sites');
expect(line).not.toContain('CALLS edges');
}
});
});

View file

@ -97,6 +97,27 @@ describe('shared path arithmetic', () => {
});
describe('Go: isGlobalNameFallbackPlausible', () => {
it('allows an external test package to dot-import exported production functions', () => {
const callerParsed = mkCaller('foo/caller_test.go', [
{ kind: 'wildcard', targetRaw: 'example.com/mod/foo' },
]);
const sourceTextOf = (file: string) =>
file.endsWith('_test.go') ? 'package foo_test' : 'package foo';
expect(
goIsGlobalNameFallbackPlausible({
callerParsed,
sourceTextOf,
candidate: mkCandidate('foo/helper.go', 'Helper'),
}),
).toBe(true);
expect(
goIsGlobalNameFallbackPlausible({
callerParsed,
sourceTextOf,
candidate: mkCandidate('foo/helper.go', 'helper'),
}),
).toBe(false);
});
it('REFUSES an unexported identifier from another package', () => {
// The headline case: `a.uniqueHelperXyz` is invisible to package `b`, and no
// import can make it visible, so the guess is impossible rather than weak.
@ -337,6 +358,19 @@ describe('Dart: isGlobalNameFallbackPlausible', () => {
});
describe('Rust: isGlobalNameFallbackPlausible', () => {
it.each([
['src/a/b.rs', 'super::unique_helper_xyz', 'src/a.rs'],
['src/a/b/c.rs', 'super::super::unique_helper_xyz', 'src/a/mod.rs'],
['src/a/b.rs', 'self::child::unique_helper_xyz', 'src/a/b/child.rs'],
])('resolves relative imports from %s', (caller, target, candidate) => {
expect(
rustIsGlobalNameFallbackPlausible({
site: BARE_SITE,
callerParsed: mkCaller(caller, [namedImport(target, BARE_SITE.name)]),
candidate: mkCandidate(candidate, BARE_SITE.name),
}),
).toBe(true);
});
it('REFUSES a cross-module item with no covering `use`', () => {
expect(
rustIsGlobalNameFallbackPlausible({
@ -388,15 +422,24 @@ describe('Rust: isGlobalNameFallbackPlausible', () => {
});
it('allows an item whose `use` names the ITEM rather than only its module', () => {
// `use crate::user::User` may arrive with the item name still on the path.
// Matching only the full path missed the module and refused `User::new`.
// A bare call exercises the import matcher, not the qualified-site bypass.
const candidate = mkCandidate('src/user.rs', 'build_user');
expect(
rustIsGlobalNameFallbackPlausible({
site: { name: 'new', rawQualifiedName: 'User::new' },
callerParsed: mkCaller('src/main.rs', [namedImport('crate::user::User', 'User')]),
candidate: mkCandidate('src/user.rs', 'User.new'),
site: { name: 'build_user' },
callerParsed: mkCaller('src/main.rs', [
namedImport('crate::user::build_user', 'build_user'),
]),
candidate,
}),
).toBe(true);
expect(
rustIsGlobalNameFallbackPlausible({
site: { name: 'build_user' },
callerParsed: mkCaller('src/main.rs'),
candidate,
}),
).toBe(false);
});
it('REFUSES when the only `use` of the module names a DIFFERENT item', () => {
@ -466,6 +509,30 @@ describe('Rust: isGlobalNameFallbackPlausible', () => {
});
describe('Swift: isGlobalNameFallbackPlausible', () => {
it('does not invent module boundaries between arbitrary Xcode directories', () => {
expect(
swiftIsGlobalNameFallbackPlausible({
callerParsed: mkCaller('App/Caller.swift'),
candidate: mkCandidate('Shared/Helper.swift', 'helper'),
}),
).toBe(true);
});
it('recognizes distinct src targets and requires a matching import', () => {
const candidate = mkCandidate('src/Core/Helper.swift', 'helper');
expect(
swiftIsGlobalNameFallbackPlausible({
callerParsed: mkCaller('src/App/Caller.swift'),
candidate,
}),
).toBe(false);
expect(
swiftIsGlobalNameFallbackPlausible({
callerParsed: mkCaller('src/App/Caller.swift', [namedImport('Core')]),
candidate,
}),
).toBe(true);
});
it('allows a cross-file candidate in the same target (whole-module internal)', () => {
expect(
swiftIsGlobalNameFallbackPlausible({

View file

@ -15,8 +15,8 @@
* `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 is the one migrated
* resolver that opts in for real see `languages/vue/scope-resolver.ts`).
* 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.