Merge branch 'main' into fix/fts-non-fatal-in-analyze

This commit is contained in:
Gergő Magyar 2026-05-24 07:36:22 +01:00 • committed by GitHub
commit 62a5774909
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 336 additions and 10 deletions

View file

@ -34,6 +34,7 @@ import {
mroPhase,
communitiesPhase,
processesPhase,
type ScopeResolutionOutput,
type PipelinePhase,
type CommunitiesOutput,
type ProcessesOutput,
@ -182,6 +183,10 @@ export const runPipelineFromRepo = async (
let communityResult: CommunitiesOutput['communityResult'] | undefined;
let processResult: ProcessesOutput['processResult'] | undefined;
const resolutionOutcomes = getPhaseOutput<ScopeResolutionOutput>(
results,
'scopeResolution',
).resolutionOutcomes;
if (!options?.skipGraphPhases) {
communityResult = getPhaseOutput<CommunitiesOutput>(results, 'communities').communityResult;
@ -208,6 +213,7 @@ export const runPipelineFromRepo = async (
totalFileCount: totalFiles,
communityResult,
processResult,
resolutionOutcomes,
usedWorkerPool,
};
};

View file

@ -30,6 +30,10 @@ import type { SemanticModel } from '../../model/semantic-model.js';
import type { WorkspaceResolutionIndex } from '../workspace-index.js';
import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
import type { ScopeResolver } from '../contract/scope-resolver.js';
import type {
ResolutionOutcomeRecorder,
ResolutionSuppressionReason,
} from '../resolution-outcome.js';
import { resolveCallerGraphId, resolveDefGraphId } from '../graph-bridge/ids.js';
import {
findAllCallableBindingsInScope,
@ -79,6 +83,7 @@ export function emitFreeCallFallback(
* fail at the call site. Three-valued; `'unknown'` keeps the
* candidate (monotonicity). */
readonly constraintCompatibility?: ScopeResolver['constraintCompatibility'];
readonly recordResolutionOutcome?: ResolutionOutcomeRecorder;
} = {},
): number {
let emitted = 0;
@ -152,9 +157,18 @@ export function emitFreeCallFallback(
// Cross-file candidates are shadowing; keep first-match.
const sameFile = narrowed.every((d) => d.filePath === narrowed[0]!.filePath);
if (sameFile) {
handledSites.add(
`${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`,
);
recordSuppressedOutcome(options.recordResolutionOutcome, {
phase: 'free-call-fallback',
filePath: parsed.filePath,
name: site.name,
range: site.atRange,
reason: suppressionReasonForOverload(narrowed, site.arity, {
conversionRankFn: options.conversionRankFn,
argumentTypes: site.argumentTypes,
}),
candidates: narrowed,
});
handledSites.add(siteKey(parsed.filePath, site));
continue;
}
}
@ -186,7 +200,19 @@ export function emitFreeCallFallback(
parsedFiles,
);
const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
const key = siteKey(parsed.filePath, site);
if (adlSuppressed && ordinary.length === 0) {
recordSuppressedOutcome(options.recordResolutionOutcome, {
phase: 'free-call-fallback',
filePath: parsed.filePath,
name: site.name,
range: site.atRange,
reason: 'adl-ordinary-lookup-blocked',
candidates: ordinary,
});
handledSites.add(key);
continue;
}
if (adl === undefined || adl.length === 0) {
// No ADL contribution. Default behavior: `ordinary[0]` —
// scope-chain walk preserves local-shadows-import precedence.
@ -210,7 +236,7 @@ export function emitFreeCallFallback(
if (narrowed.length === 1) {
fnDef = narrowed[0];
} else if (narrowed.length === 0) {
handledSites.add(siteKey);
handledSites.add(key);
continue;
} else {
// >1 survivors: same-file → suppress (true overloads,
@ -219,7 +245,18 @@ export function emitFreeCallFallback(
// first-match (shadowing semantics).
const sameFile = narrowed.every((d) => d.filePath === narrowed[0]!.filePath);
if (sameFile) {
handledSites.add(siteKey);
recordSuppressedOutcome(options.recordResolutionOutcome, {
phase: 'free-call-fallback',
filePath: parsed.filePath,
name: site.name,
range: site.atRange,
reason: suppressionReasonForOverload(narrowed, site.arity, {
conversionRankFn: options.conversionRankFn,
argumentTypes: site.argumentTypes,
}),
candidates: narrowed,
});
handledSites.add(key);
continue;
}
fnDef = ordinary[0];
@ -246,16 +283,27 @@ export function emitFreeCallFallback(
if (narrowed.length === 1) {
fnDef = narrowed[0];
} else if (narrowed.length === 0) {
handledSites.add(siteKey);
handledSites.add(key);
continue;
} else if (narrowed.length > 1) {
recordSuppressedOutcome(options.recordResolutionOutcome, {
phase: 'free-call-fallback',
filePath: parsed.filePath,
name: site.name,
range: site.atRange,
reason: suppressionReasonForOverload(narrowed, site.arity, {
conversionRankFn: options.conversionRankFn,
argumentTypes: site.argumentTypes,
}),
candidates: narrowed,
});
if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) {
handledSites.add(siteKey);
handledSites.add(key);
continue;
}
// Multiple survivors remain after conversion-rank scoring;
// suppress instead of picking arbitrarily.
handledSites.add(siteKey);
handledSites.add(key);
continue;
}
}
@ -295,7 +343,7 @@ export function emitFreeCallFallback(
// Always mark the site as handled — even when the dedup-collapse
// means we don't add a new edge — so `emit-references` skips its
// potentially-wrong fallback for the same site.
handledSites.add(`${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`);
handledSites.add(siteKey(parsed.filePath, site));
const relId = `rel:CALLS:${callerGraphId}->${tgtGraphId}`;
if (seen.has(relId)) continue;
seen.add(relId);
@ -315,6 +363,61 @@ export function emitFreeCallFallback(
return emitted;
}
function siteKey(
filePath: string,
site: { readonly atRange: { readonly startLine: number; readonly startCol: number } },
): string {
return `${filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
}
function suppressionReasonForOverload(
candidates: readonly SymbolDefinition[],
arity: number | undefined,
ctx: {
readonly conversionRankFn?: ConversionRankFn;
readonly argumentTypes?: readonly string[];
},
): ResolutionSuppressionReason {
if (isOverloadAmbiguousAfterNormalization(candidates, arity)) {
return 'overload-ambiguous-normalization';
}
if (
ctx.conversionRankFn !== undefined &&
ctx.argumentTypes !== undefined &&
ctx.argumentTypes.length > 0
) {
return 'conversion-rank-tied';
}
return 'overload-ambiguous';
}
function recordSuppressedOutcome(
record: ResolutionOutcomeRecorder | undefined,
input: {
readonly phase: string;
readonly filePath: string;
readonly name: string;
readonly range: {
readonly startLine: number;
readonly startCol: number;
readonly endLine: number;
readonly endCol: number;
};
readonly reason: ResolutionSuppressionReason;
readonly candidates: readonly SymbolDefinition[];
},
): void {
record?.({
kind: 'suppressed',
phase: input.phase,
filePath: input.filePath,
name: input.name,
range: input.range,
reason: input.reason,
candidateIds: input.candidates.map((d) => d.nodeId),
});
}
/**
* Build a `simpleName -> callable defs` index from `scopes.defs` once per
* pass. Mirrors the filter the old per-site scan applied: Function /

View file

@ -65,6 +65,10 @@ import {
extractTemplateArguments,
stripTemplateArguments,
} from '../../utils/template-arguments.js';
import type {
ResolutionOutcomeRecorder,
ResolutionSuppressionReason,
} from '../resolution-outcome.js';
/** Subset of `ScopeResolver` consumed by this pass. Accepting the
* subset rather than the full provider keeps tests and partial
@ -140,6 +144,9 @@ export function emitReceiverBoundCalls(
provider: ReceiverBoundProviderSubset,
index: WorkspaceResolutionIndex,
model: SemanticModel,
options: {
readonly recordResolutionOutcome?: ResolutionOutcomeRecorder;
} = {},
): number {
let emitted = 0;
// Per-pass dedup so the multiple cases don't double-emit if two of
@ -498,6 +505,15 @@ export function emitReceiverBoundCalls(
if (memberDef === 'ambiguous') {
// Same-name ambiguity across inline-namespace children (#1564):
// suppress edge emission, mark site handled.
options.recordResolutionOutcome?.({
kind: 'suppressed',
phase: 'receiver-bound-calls',
filePath: parsed.filePath,
name: site.name,
range: site.atRange,
reason: 'inline-ns-ambiguous',
candidateIds: [],
});
handledSites.add(siteKey);
continue;
}
@ -702,6 +718,7 @@ export function emitReceiverBoundCalls(
const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
let memberDef: SymbolDefinition | undefined;
let ambiguous = false;
let ambiguousOwnerId: string | undefined;
// Track whether the chain walk filtered out any static-only
// candidates. When it did and the chain ended with no
// legitimate instance member, we mark the site as handled so
@ -733,6 +750,7 @@ export function emitReceiverBoundCalls(
const picked = pickFirstNonStaticOnly(ownerId, memberName, site, model, provider);
if (picked === OVERLOAD_AMBIGUOUS) {
ambiguous = true;
ambiguousOwnerId = ownerId;
break;
}
if (picked === STATIC_ONLY_FILTERED) {
@ -754,6 +772,15 @@ export function emitReceiverBoundCalls(
// Suppress and mark handled so `emitReferencesViaLookup`
// doesn't re-emit the pre-resolved reference. See
// OVERLOAD_AMBIGUOUS docstring for the upstream cause.
recordReceiverOverloadSuppression(
options.recordResolutionOutcome,
parsed.filePath,
site,
ambiguousOwnerId ?? ownerDef.nodeId,
memberName,
model,
provider,
);
handledSites.add(siteKey);
continue;
}
@ -830,6 +857,15 @@ export function emitReceiverBoundCalls(
resolveDefGraphId(valueDef.filePath, valueDef, nodeLookup) ?? valueDef.nodeId;
const picked = pickOverload(ownerGraphId, memberName, site, model, provider);
if (picked === OVERLOAD_AMBIGUOUS) {
recordReceiverOverloadSuppression(
options.recordResolutionOutcome,
parsed.filePath,
site,
ownerGraphId,
memberName,
model,
provider,
);
handledSites.add(siteKey);
continue;
}
@ -1017,3 +1053,49 @@ function pickFirstNonStaticOnly(
if (candidates.length > 1) return OVERLOAD_AMBIGUOUS;
return candidates[0] ?? overloads[0];
}
function recordReceiverOverloadSuppression(
record: ResolutionOutcomeRecorder | undefined,
filePath: string,
site: ParsedFile['referenceSites'][number],
ownerId: string,
memberName: string,
model: SemanticModel,
provider: ReceiverBoundProviderSubset,
): void {
if (record === undefined) return;
const overloads = model.methods.lookupAllByOwner(ownerId, memberName);
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
argumentTypeClasses: site.argumentTypeClasses,
conversionRankFn: provider.conversionRankFn,
constraintCompatibility: provider.constraintCompatibility,
});
const reason: ResolutionSuppressionReason = isOverloadAmbiguousAfterNormalization(
candidates,
site.arity,
)
? 'overload-ambiguous-normalization'
: hasConversionRankingSignal(site, provider)
? 'conversion-rank-tied'
: 'overload-ambiguous';
record({
kind: 'suppressed',
phase: 'receiver-bound-calls',
filePath,
name: site.name,
range: site.atRange,
reason,
candidateIds: candidates.map((d) => d.nodeId),
});
}
function hasConversionRankingSignal(
site: ParsedFile['referenceSites'][number],
provider: ReceiverBoundProviderSubset,
): boolean {
return (
provider.conversionRankFn !== undefined &&
site.argumentTypes !== undefined &&
site.argumentTypes.length > 0
);
}

View file

@ -37,6 +37,7 @@ import { readFileContents } from '../../filesystem-walker.js';
import { runScopeResolution } from './run.js';
import { SCOPE_RESOLVERS } from './registry.js';
import { isDev, isSemanticModelValidatorEnabled } from '../../utils/env.js';
import type { ResolutionOutcome } from '../resolution-outcome.js';
import { logger } from '../../../logger.js';
export interface ScopeResolutionOutput {
@ -48,6 +49,8 @@ export interface ScopeResolutionOutput {
readonly importsEmitted: number;
/** Reference (CALLS / ACCESSES / INHERITS / USES) edges emitted. */
readonly referenceEdgesEmitted: number;
/** Additive stream of resolver diagnostics; does not affect graph edges. */
readonly resolutionOutcomes: readonly ResolutionOutcome[];
/** Per-language breakdown for telemetry / shadow-parity. */
readonly perLanguage: ReadonlyMap<
SupportedLanguages,
@ -64,6 +67,7 @@ const NOOP_OUTPUT: ScopeResolutionOutput = Object.freeze({
filesProcessed: 0,
importsEmitted: 0,
referenceEdgesEmitted: 0,
resolutionOutcomes: [],
perLanguage: new Map(),
});
@ -116,6 +120,7 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
let totalImports = 0;
let totalRefs = 0;
let anyRan = false;
const resolutionOutcomes: ResolutionOutcome[] = [];
const perLanguage = new Map<
SupportedLanguages,
{
@ -156,6 +161,9 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
treeCache: scopeTreeCache,
resolutionConfig,
preExtractedParsedFiles: preExtractedByPath,
recordResolutionOutcome: (outcome) => {
resolutionOutcomes.push(outcome);
},
onWarn: (msg) => {
if (isSemanticModelValidatorEnabled()) {
logger.warn(`[scope-resolution:${lang}] ${msg}`);
@ -197,6 +205,7 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
filesProcessed: totalFiles,
importsEmitted: totalImports,
referenceEdgesEmitted: totalRefs,
resolutionOutcomes,
perLanguage,
};
},

View file

@ -44,6 +44,7 @@ import { emitImportEdges } from '../graph-bridge/imports-to-edges.js';
import type { ScopeResolver } from '../contract/scope-resolver.js';
import { findClassBindingInScope, findEnclosingClassDef } from '../scope/walkers.js';
import { buildWorkspaceResolutionIndex } from '../workspace-index.js';
import type { ResolutionOutcome, ResolutionOutcomeRecorder } from '../resolution-outcome.js';
import { logger } from '../../../logger.js';
@ -161,6 +162,11 @@ interface RunScopeResolutionInput {
* Cache miss is safe — falls back to fresh extract.
*/
readonly preExtractedParsedFiles?: ReadonlyMap<string, ParsedFile>;
/**
* Optional additive diagnostics sink. Resolver passes call this when they
* intentionally suppress an edge; the graph remains unchanged.
*/
readonly recordResolutionOutcome?: ResolutionOutcomeRecorder;
}
interface RunScopeResolutionStats {
@ -170,6 +176,7 @@ interface RunScopeResolutionStats {
readonly resolve: ResolveStats;
readonly referenceEdgesEmitted: number;
readonly referenceSkipped: number;
readonly resolutionOutcomes: readonly ResolutionOutcome[];
}
export function runScopeResolution(
@ -178,6 +185,11 @@ export function runScopeResolution(
): RunScopeResolutionStats {
const { graph, files } = input;
const onWarn = input.onWarn ?? (() => {});
const resolutionOutcomes: ResolutionOutcome[] = [];
const recordResolutionOutcome: ResolutionOutcomeRecorder = (outcome) => {
resolutionOutcomes.push(outcome);
input.recordResolutionOutcome?.(outcome);
};
const PROF = process.env.PROF_SCOPE_RESOLUTION === '1';
const tStart = PROF ? process.hrtime.bigint() : 0n;
let fileContents: Map<string, string> | undefined;
@ -248,6 +260,7 @@ export function runScopeResolution(
resolve: { sitesProcessed: 0, referencesEmitted: 0, unresolved: 0 },
referenceEdgesEmitted: 0,
referenceSkipped: 0,
resolutionOutcomes,
};
}
@ -359,6 +372,9 @@ export function runScopeResolution(
provider,
workspaceIndex,
readonlyModel,
{
recordResolutionOutcome,
},
);
const unresolvedReceiverExtras =
provider.emitUnresolvedReceiverEdges !== undefined
@ -387,6 +403,7 @@ export function runScopeResolution(
resolveAdlCandidates: provider.resolveAdlCandidates,
conversionRankFn: provider.conversionRankFn,
constraintCompatibility: provider.constraintCompatibility,
recordResolutionOutcome,
},
);
const { emitted, skipped } = emitReferencesViaLookup(
@ -424,5 +441,6 @@ export function runScopeResolution(
resolve: resolveStats,
referenceEdgesEmitted: emitted + receiverExtras + unresolvedReceiverExtras + freeCallExtras,
referenceSkipped: skipped,
resolutionOutcomes,
};
}

View file

@ -0,0 +1,34 @@
import type { Range } from 'gitnexus-shared';
export type ResolutionSuppressionReason =
| 'adl-ordinary-lookup-blocked'
| 'conversion-rank-tied'
| 'inline-ns-ambiguous'
| 'overload-ambiguous'
| 'overload-ambiguous-normalization';
export type ResolutionOutcome =
| {
readonly kind: 'resolved';
readonly targetId: string;
readonly phase: string;
readonly filePath: string;
readonly name: string;
readonly range: Range;
}
| {
readonly kind: 'suppressed';
readonly reason: ResolutionSuppressionReason;
/**
* Scope-resolution definition IDs considered by the suppression decision.
* For `inline-ns-ambiguous` this is currently empty because the
* qualified namespace resolver returns only an `ambiguous` sentinel.
*/
readonly candidateIds: readonly string[];
readonly phase: string;
readonly filePath: string;
readonly name: string;
readonly range: Range;
};
export type ResolutionOutcomeRecorder = (outcome: ResolutionOutcome) => void;

View file

@ -1,6 +1,7 @@
import type { KnowledgeGraph } from '../core/graph/types.js';
import { CommunityDetectionResult } from '../core/ingestion/community-processor.js';
import { ProcessDetectionResult } from '../core/ingestion/process-processor.js';
import type { ResolutionOutcome } from '../core/ingestion/scope-resolution/resolution-outcome.js';
// CLI-specific: in-memory result with graph + detection results
export interface PipelineResult {
@ -11,6 +12,12 @@ export interface PipelineResult {
totalFileCount: number;
communityResult?: CommunityDetectionResult;
processResult?: ProcessDetectionResult;
/**
* Additive diagnostics for registry-primary resolution decisions that
* deliberately suppress edge emission. Empty means no diagnostic was
* produced; graph edge semantics are unchanged.
*/
resolutionOutcomes: readonly ResolutionOutcome[];
/**
* True if the parse phase spawned a worker pool for this run. False means
* the sequential fallback handled every chunk. Primarily a test affordance

View file

@ -9,6 +9,7 @@ import {
getRelationships,
getNodesByLabel,
getNodesByLabelFull,
getResolutionOutcomes,
edgeSet,
runPipelineFromRepo,
createResolverParityIt,
@ -1819,6 +1820,21 @@ describe('C++ ambiguous integer-width overloads', () => {
// GitNexus does not have. The resolver must suppress entirely.
expect(processCalls.length).toBe(0);
});
it('records a structured suppression reason for normalization ambiguity', () => {
const outcomes = getResolutionOutcomes(result).filter(
(o) =>
o.kind === 'suppressed' &&
o.name === 'process' &&
o.phase === 'receiver-bound-calls' &&
o.filePath.endsWith('caller.cpp') &&
o.reason === 'overload-ambiguous-normalization',
);
expect(outcomes.length).toBeGreaterThan(0);
expect(outcomes[0]?.candidateIds.length).toBe(2);
expect(outcomes[0]?.range.startLine).toBeGreaterThan(0);
});
});
// ---------------------------------------------------------------------------
@ -1893,6 +1909,20 @@ describe('C++ overload resolution — conversion-rank disambiguation (#1578)', (
// Contract: zero edges for ALL h() call sites combined (dedup).
expect(hCalls.length).toBe(0);
});
it('records a structured suppression reason for conversion-rank ties', () => {
const outcomes = getResolutionOutcomes(result).filter(
(o) =>
o.kind === 'suppressed' &&
o.name === 'h' &&
o.phase === 'free-call-fallback' &&
o.reason === 'conversion-rank-tied',
);
expect(outcomes.length).toBeGreaterThan(0);
expect(outcomes[0]?.candidateIds.length).toBe(2);
expect(outcomes[0]?.range.startLine).toBeGreaterThan(0);
});
});
// C++ overload resolution: pointer/nullptr/ellipsis conversion ranks (#1637)
@ -2722,6 +2752,20 @@ describe('C++ ADL — non-function ordinary lookup suppresses ADL', () => {
// `e` is audit::Event, audit::record should NOT be discovered.
expect(recordCalls.length).toBe(0);
});
it('records a structured suppression reason for ADL blocker lookup', () => {
const outcomes = getResolutionOutcomes(result).filter(
(o) =>
o.kind === 'suppressed' &&
o.name === 'record' &&
o.phase === 'free-call-fallback' &&
o.reason === 'adl-ordinary-lookup-blocked',
);
expect(outcomes.length).toBeGreaterThan(0);
expect(outcomes[0]?.candidateIds.length).toBe(0);
expect(outcomes[0]?.range.startLine).toBeGreaterThan(0);
});
});
describe('C++ ADL — inner callable + outer non-callable: ADL not suppressed', () => {
@ -2984,6 +3028,20 @@ describe('C++ inline namespace — ambiguous same-name across inline children (#
// the same name. The resolver must suppress rather than pick arbitrarily.
expect(fooCalls.length).toBe(0);
});
it('records a structured suppression reason for inline namespace ambiguity', () => {
const outcomes = getResolutionOutcomes(result).filter(
(o) =>
o.kind === 'suppressed' &&
o.name === 'foo' &&
o.phase === 'receiver-bound-calls' &&
o.reason === 'inline-ns-ambiguous',
);
expect(outcomes.length).toBeGreaterThan(0);
expect(outcomes[0]?.candidateIds.length).toBe(0);
expect(outcomes[0]?.range.startLine).toBeGreaterThan(0);
});
});
describe('C++ inline namespace — ambiguous distinct signatures (conservative suppress)', () => {

View file

@ -218,6 +218,7 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
// resolver-only correctness win (PR #1520 review follow-up plan U2 /
// Claude review Finding 5); backporting to legacy is out of scope.
'emits zero CALLS edges when process(int)/process(long) collide after normalization',
'records a structured suppression reason for normalization ambiguity',
// The legacy DAG path resolves `using namespace a; using namespace b; foo()`
// by walking the workspace registry by simple name and binding to
// the first match — same shape as the integer-width collision, just
@ -299,6 +300,7 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
// Multi-arg incomparable overloads: pairwise dominance check finds
// neither h(int,int) nor h(double,double) dominates. Scope-resolver-only.
'h(42, 2.5) emits zero CALLS edges — incomparable multi-arg overloads, ambiguous',
'records a structured suppression reason for conversion-rank ties',
// Pointer/nullptr/ellipsis conversion ranks (#1637) need C++ type-class
// sidecars plus conversion-rank scoring. The legacy DAG has neither.
'f(nullptr) and f(p) resolve to f(int*) while f(42) resolves to f(bool)',
@ -328,6 +330,7 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
// 'ambiguous' and suppresses edge emission. Scope-resolver-only
// correctness win (#1564); backporting to legacy is out of scope.
'outer::foo() emits zero CALLS edges when v1 and v2 both declare foo',
'records a structured suppression reason for inline namespace ambiguity',
// Distinct-signature inline-namespace ambiguity: `foo(int)` in v1 and
// `foo(double)` in v2. The scope-resolver conservatively suppresses
// because `resolveQualifiedReceiverMember` lacks call-site argument
@ -348,6 +351,8 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
// scope-resolver-only. The legacy DAG has no scope-aware ADL blocker
// detection; it falls back to `pickUniqueGlobalCallable`. Scope-
// resolver-only correctness wins; backporting is out of scope.
'record(e) emits zero CALLS when a variable named record exists in scope',
'records a structured suppression reason for ADL blocker lookup',
'swap(a,b) resolves to data::swap when inner scope has callable swap and outer has variable',
'record(e) emits zero CALLS when a block-scope function declaration exists',
]),
@ -429,6 +434,10 @@ export function getRelationships(result: PipelineResult, type: string): RelEdge[
return edges;
}
export function getResolutionOutcomes(result: PipelineResult) {
return result.resolutionOutcomes ?? [];
}
export function getNodesByLabel(result: PipelineResult, label: string): string[] {
const names: string[] = [];
result.graph.forEachNode((n) => {