mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-12 23:02:45 +00:00
Merge reviewed fallback corrections into Go split
This commit is contained in:
commit
fd7272bc24
16 changed files with 300 additions and 71 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -3866,9 +3866,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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'])(
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue