GitNexus/gitnexus/test/integration/resolvers/helpers.ts
Sparsh 73a6a5376e
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
fix(cpp): thread call-site types into qualified member lookup (#1632) (#1810)
* fix(cpp): thread call-site types into qualified member lookup (#1632)

Widen Callsite (arity optional, add argumentTypes) and add optional
callsite?: Callsite to ScopeResolver.resolveQualifiedReceiverMember.
receiver-bound-calls.ts passes the ReferenceSite through structurally;
resolveCppQualifiedNamespaceMember forwards it to narrowOverloadCandidates
along with cppConversionRank, enabling exact-type and conversion-rank
disambiguation across inline-namespace children.

Behavior change:
- outer::foo(42) where v1 declares foo(int) and v2 declares foo(double)
  now resolves to v1::foo (was: 0 edges, conservatively suppressed).
- Same-name same-normalized-signature (e.g. foo(int) vs foo(long)) still
  suppresses at 0 edges via isOverloadAmbiguousAfterNormalization.
- ADL using-import path (resolveAdlCandidates) unchanged — passes no
  callsite, narrowing degrades to existing pass-through behavior.

Closes #1632. Part of #1564.

* fix(cpp): update legacy parity expected-failure list for #1632

- Remove stale expected-failure entry for old diff-sigs test name
  (test now expects 1 edge; legacy DAG also emits 1 edge)
- Add entry for normalized-signature ambiguity (int vs long) test
- Rename describe block from 'conservative suppress' to
  'distinct signatures resolved via call-site types'

Verified both modes:
  REGISTRY_PRIMARY_CPP=1: 241/241 passed
  REGISTRY_PRIMARY_CPP=0: 194 passed, 47 skipped, 0 failed
2026-05-25 07:11:47 +01:00

471 lines
27 KiB
TypeScript

/**
* Shared test helpers for language resolution integration tests.
*/
import path from 'path';
import { it as vitestIt } from 'vitest';
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
import type { PipelineOptions } from '../../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../../src/types/pipeline.js';
import type { GraphRelationship } from 'gitnexus-shared';
const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, ReadonlySet<string>>> = {
c: new Set([
// The legacy DAG path does not resolve the main → create_service call
// because the function prototype in the .h file and the definition in
// the .c file create a dedup ambiguity. The registry-primary path
// resolves it via scope-based wildcard import binding.
'emits CALLS edges for cross-file function calls',
// The legacy DAG path does not resolve cross-file calls through
// #include → prototype chains. The scope-based path resolves
// caller.c → b.h → public_b via wildcard import binding +
// isFileLocalDef filtering of static functions.
'caller.c calls b:helper via include, NOT a:static helper',
]),
csharp: new Set([
'emits the using-import edge App/Program.cs -> Models/User.cs through the scope-resolution path',
// Generic type-argument USES edges are emitted by the registry-primary
// resolver only; the legacy DAG path does not synthesize these references.
'emits USES edges for generic type arguments',
]),
go: new Set([
// The legacy DAG path does not resolve method calls when the method is
// defined in a different file from the receiver type (go-split-method-owner
// fixture). This requires scope-based cross-file package-sibling resolution
// which is only available in the registry-primary path.
'resolves user.Save() to the method whose receiver type is declared in another package file',
]),
java: new Set([
// Duplicate-FQN same-module path-affinity ordering is implemented in the
// Java provider hook for the scope-resolution path. Legacy DAG parity runs
// still use legacy owner/type resolution behavior and can bind cross-module.
'resolves Module1App.run calls to module1 UserService, not module2',
'resolves Module2App.run calls to module2 UserService, not module1',
]),
php: new Set([
// Arity-narrowing in `pickUniqueGlobalCallable` rejects free-call
// candidates that are definitively below required-parameter-count. The
// legacy DAG path does not narrow on arity, so it emits over-broad CALLS
// edges for variadic functions invoked with too few args even though
// the only candidate's required count is non-zero. Scope-resolver-only
// correctness win (commit af9af4a9 U1); backporting to legacy is out
// of scope.
'does NOT emit CALLS edge for record() with zero args (below required=1)',
'does NOT emit CALLS edge for pad() with zero args (below required=1)',
// `$this->method()` precedence inside a class that composes a trait AND
// extends a parent both defining the same method requires the augmented
// trait-aware MRO (trait shadows parent). The legacy DAG has no
// trait-aware MRO, so it fails to bind the call to the trait. Scope-
// resolver-only correctness win (commit af9af4a9 U3).
'$this->record() still resolves to Auditable::record (trait shadows parent)',
// Fully-qualified type-hint resolution (`\App\Other\User $u` parameter)
// routes through the scope-resolver's bindingAugmentations channel
// populated by `populatePhpNamespaceSiblings` Step 3b. The legacy DAG
// resolves receiver types via simple-name workspace lookup and has no
// namespace-prefixed binding channel, so it cannot distinguish the FQN
// target from a same-simple-name class reachable via `use`. Scope-
// resolver-only correctness win (Codex PR #1497 review, finding 1).
'\\App\\Other\\User parameter resolves $u->record() to app/Other/User.php (NOT app/Models/User.php)',
// MRO arity-mismatch on class-name receivers (`Child::method(1)` where
// Child::method takes 2 args and Parent::method takes 1): the legacy
// DAG has no arity narrowing on Case 2 (class-name) MRO walk, so it
// emits a false CALLS edge to Parent::method on fallthrough. Scope-
// resolver-only correctness win (PR #1497 review Image 1 / U1).
'arity-incompatible most-derived override does NOT fall through to ParentModel::method',
// Class-name receiver with single-class arity mismatch (no parent in
// the MRO chain): legacy resolves the method by name without arity
// gating, so it emits a CALLS edge even when arity is definitively
// incompatible. The scope-resolver's `narrowOverloadCandidates` check
// in `receiver-bound-calls.ts` Case 2 rejects this post-fix. Scope-
// resolver-only correctness win (PR #1497 / U1).
'arity-incompatible class with no parent emits zero CALLS edges (regression check)',
// `phpEmitUnresolvedReceiverEdges` exact-required-arity gate (PR
// #1497 / U4): the legacy DAG has no equivalent unresolved-receiver
// fallback hook, so it resolves these untyped-receiver sites via a
// different code path that over-emits for default-parameter and
// variadic-required-mismatch shapes. Scope-resolver-only correctness
// wins; backporting to legacy is out of scope.
'argCount > required (2>1) on candidate with default param emits NO edge post-fix',
'variadic candidate, argCount < required (1<2) emits NO edge',
]),
typescript: new Set([
// Issue #1358 sub-cases: class-instance singleton (`export const foo = new Foo()`)
// and factory-pattern singleton (`export const foo = makeFoo()`) cross-file
// CALLS resolution. The scope-resolution path resolves these via
// `@type-binding.constructor` capture (TS query) +
// `propagateImportedReturnTypes` mirror + receiver-bound Case 4 simple
// typeBinding lookup. The legacy DAG's typeEnv does not propagate
// `new Foo()` constructor inference across module boundaries — verified
// by `scope-parity / typescript parity` CI job failure. Node-existence
// and HAS_METHOD edge assertions pass under legacy DAG (parser-level
// emission is intact); only the cross-file CALLS edge resolution
// requires the scope-resolution chain. Scope-resolver-only correctness
// wins; backporting requires constructor-typeBinding cross-file
// propagation in the legacy DAG.
'resolves caller.fooService.getUser() to FooService.getUser via constructor-inferred typeBinding',
'resolves caller.fooService.getUser() through the factory chain to FooService.getUser',
]),
javascript: new Set([
// Mirrors the TypeScript class-instance and factory-pattern singleton
// resolution gates above. JavaScript fails on the same 2 CALLS-edge
// resolution tests under `REGISTRY_PRIMARY_JAVASCRIPT=0` for the same
// reason — no cross-file constructor-typeBinding propagation in the
// legacy DAG path. Verified by `scope-parity / javascript parity` CI
// job failure on the bare singleton tests before this exclusion landed.
'resolves caller.fooService.getUser() to FooService.getUser via constructor-inferred typeBinding',
'resolves caller.fooService.getUser() through the factory chain to FooService.getUser',
]),
python: new Set([
// Suffix-fallback lex tiebreak depends on the registry-primary
// resolver's deterministic sort. The legacy resolver returns the
// first match in `Set` iteration order, which is insertion-order
// dependent and not aligned with this guarantee. Backporting the
// sort to legacy is out of scope.
'picks the lexicographically smaller path on equal-depth ties',
'binds the call to alpha/services/sync.py, not omega',
'lex tiebreak still picks alpha/services/sync.py with reversed file-write order',
]),
kotlin: new Set<string>([
// #1756 companion-vs-instance dispatch: the registry-primary path
// suppresses `instance.companionMethod()` via `ScopeResolver.
// isStaticOnly` (see `isKotlinStaticOnly` + the Case 4 filter in
// `receiver-bound-calls.ts`). The legacy DAG has no equivalent
// static-only gate — companion methods promoted onto the outer
// class are also returned by `lookupMethodByOwner` when the
// receiver is an instance, producing a false `CALLS` edge. Scope-
// resolver-only correctness win; backporting to legacy is out of
// scope per the migration policy (the bug stops mattering once
// Kotlin enters `MIGRATED_LANGUAGES` and legacy stops running).
'crossover() invoking logger.create() on an instance emits NO CALLS edge',
// #1756 / U2 (remediation plan 2026-05-22-002) MRO shadow tests:
// the registry-primary path filters static-only candidates INSIDE
// the Case-4 MRO chain walk (`pickFirstNonStaticOnly` in
// `receiver-bound-calls.ts`), so a derived class whose only
// member is a companion-promoted static method falls through to
// an ancestor's legitimate instance method; if no ancestor has
// an instance method, no CALLS edge is emitted. The legacy DAG
// returns the static-only companion method via
// `lookupMethodByOwner` on the most-derived owner and emits a
// false `CALLS` edge to it. Same scope-resolver-only correctness
// class as the bare `crossover()` test above; backporting is out
// of scope per the migration policy.
'useChild() falls through static-only Child.foo to Base.foo',
'useChild() does NOT emit an edge to the companion-promoted Child.foo',
'useStandalone() emits no CALLS edge (entire chain is static-only)',
// #1757 lambda scopes: the registry-primary path creates a Block
// scope per `lambda_literal` and synthesizes scoped type-bindings
// for the lambda parameter / implicit `it` (see
// `synthesizeKotlinLambdaBindings` in `kotlin/captures.ts` plus
// the `@type-binding.lambda-scoped` gate in
// `kotlinBindingScopeFor`). This lets the body's call-resolution
// chain see the chain-typebinding for the lambda's enclosing
// call (`users.map { it.name }.forEach { name -> println(name) }`)
// and emit the `chained -> println` edge correctly. The legacy DAG
// has no lambda-body scope and no per-lambda type-binding
// synthesis; calls inside lambdas resolve against the enclosing
// function scope only, so the `name` parameter chain inside a
// chained-receiver forEach lambda doesn't carry the right binding
// and the call-extractor never emits the CALLS edge. Scope-
// resolver-only correctness win; backporting requires re-modeling
// lambda bodies as their own scopes in `call-processor.ts`, which
// is out of scope per migration policy.
'chained: println(name) inside forEach resolves to file-scope println',
// #1756 / U4 (remediation plan 2026-05-22-002) named-companion
// crossover: the registry-primary path stamps the static-only
// marker on named-companion methods (via the new `@scope.companion`
// marker capture and the updated `populateCompanionMembersOn
// EnclosingClass` guard), so `instance.namedCompanionMethod()`
// is filtered out at the `isStaticOnly` hook. The legacy DAG has
// no static-only gate AND no named-companion-aware owner
// promotion — it both leaves the named-companion method owned
// by `Helper` AND emits a crossover edge when the call site uses
// an instance receiver. Same scope-resolver-only correctness
// class as the bare `crossover()` test; backporting is out of
// scope per the migration policy.
'useNamedCrossover: o.create() emits NO CALLS edge to create',
// #1756 / U3 (remediation plan 2026-05-22-002) other-receiver
// crossover: the registry-primary path applies the `isStaticOnly`
// filter across Cases 0 (compound receiver), 3b (chain-typebinding),
// and 5 (value-receiver bridge) of `receiver-bound-calls.ts`. For
// the U3 fixture `kotlin-companion-other-cases/App.kt`, the
// chain-typebinding crossover (`services.first().build()` on a
// chain whose receiver type resolves through the legacy DAG's
// unfiltered lookup) and the value-receiver crossover
// (`l.create("nope")` where the legacy DAG binds `l` directly
// via its receiver-resolution path) both emit false `CALLS`
// edges to the companion-promoted static-only members. The
// legacy DAG has no `isStaticOnly`-equivalent hook, so these
// edges leak. Same scope-resolver-only correctness class as the
// bare `crossover()` test and the U2 MRO-shadow tests above;
// backporting is out of scope per the migration policy.
'useChainTypeBindingCrossover: services.first().build() emits NO CALLS edge to build',
'useValueReceiverCrossover: l.create("nope") emits NO CALLS edge to create',
]),
cpp: new Set<string>([
// The legacy DAG path has no scope-aware filtering on the global
// free-call fallback, so `#include`d headers still leak class
// methods (`User::save`) and namespace members (`ns::foo`) as
// resolution targets for unqualified calls. The scope-resolver
// path filters via `populateCppNonGloballyVisible` +
// `isFileLocalDef`. Scope-resolver-only correctness win
// (PR #1520 review follow-up plan U1); backporting to legacy is
// out of scope.
'does NOT resolve unqualified save() to User::save via #include',
'does NOT resolve unqualified foo() to ns::foo via #include',
// The legacy DAG path lacks the OVERLOAD_AMBIGUOUS suppression
// wired through `pickOverload` + `isOverloadAmbiguousAfterNormalization`,
// so it arbitrarily picks the first overload when `f(int)` and
// `f(long)` collide after C++ integer-width normalization. Scope-
// 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
// with namespace-resolution as the ambiguity source. Scope-resolver-
// only correctness win (PR #1520 review follow-up plan U4 / Claude
// review Finding 7); backporting to legacy is out of scope.
'emits zero CALLS edges for ambiguous foo() bound via two using-namespace declarations',
// The legacy DAG path lacks two-phase template lookup. Unqualified
// calls inside a class template body bind to dependent-base members
// there, producing CALLS edges the compiler would reject (ISO C++
// two-phase name lookup). Scope-resolver-only correctness win
// (PR #1520 review follow-up plan 2026-05-13-001 U3); backporting
// is out of scope.
'Derived<T>::g() -> f() does NOT bind to Base<T>::f (dependent base)',
// The legacy DAG path does not apply merged ordinary+ADL narrowing
// with ambiguity suppression.
// When ADL surfaces multiple overloads that collide after C++
// int/long normalization, legacy picks the first match arbitrarily.
// The scope-resolver path suppresses in free-call-fallback after
// merged-candidate overload narrowing. Scope-resolver-only
// correctness win (PR #1520 review follow-up plan
// 2026-05-13-001 U2); backporting is out of scope.
'process(t, 42) emits zero CALLS edges when ADL surfaces process(Token,int)/process(Token,long) (collide after C++ int normalization)',
// Legacy DAG path does not merge ordinary and ADL candidate sets for
// non-empty ordinary lookup, so it misses ADL's better-match overload.
'swap(a, b) prefers data::swap(Pair&, Pair&) over app::swap(int, int)',
// The legacy DAG path has no qualified namespace-member resolver
// and no inline-namespace awareness. For the versioned fixture
// (`outer::v1::foo` inline, `outer::v0::foo` not), the registry-
// primary path resolves `outer::foo()` to v1 via the inline
// exemption; legacy can't see EITHER and emits zero edges. The
// unqualified / nested fixtures coincidentally resolve in legacy
// because their global free-call fallback picks the unique simple-
// name match; the versioned fixture has two `foo`s and legacy can't
// disambiguate. Scope-resolver-only correctness win (PR #1520
// review follow-up plan 2026-05-13-001 U5); backporting is out of
// scope.
'outer::foo() resolves to outer::v1::foo (inline child), NOT outer::v0::foo',
// Phase 5 cross-unit composition tests assert no false positives
// for compositions where the legacy DAG over-resolves. The legacy
// path has no template-arg-stripping qualified-receiver logic and
// no two-phase dependent-base suppression, so it produces CALLS
// edges where the registry-primary path correctly suppresses.
// Scope-resolver-only correctness wins (PR #1520 review follow-up
// plan 2026-05-13-001 Phase 5); backporting is out of scope.
'emits EXTENDS edge: Derived → Base for template base Base<T>',
'emits EXTENDS edges: Derived → A, Derived → B for template multi-base list',
'Base<T>::method() resolves to Base::method inside template body',
'unqualified f() inside Derived<T>::g() does NOT bind to outer::v1::Base<T>::f (dependent base across inline namespace)',
'emits EXTENDS edge: Derived → Base for qualified template base outer::v1::Base<T>',
'outer::v1::Base<T>::f() resolves to Base::f inside template body',
'outer::v1::free_fn() resolves as a namespace free function, not a super-receiver method',
// Template specialization owner identity currently relies on
// class-template fingerprints in the registry-primary graph bridge.
// Legacy DAG collapses specializations to the simple class name.
'emits distinct Class nodes for List<User> and List<Order>',
'callSave() in each specialization resolves to its own save()',
'save specialization bodies route to their own sibling method',
// PR #1590 follow-up: explicit `this->` resolution in template class
// bodies and paired two-phase assertions are scope-resolver-only.
// Legacy DAG lacks this receiver-bound template semantics and
// dependent-base suppression parity for these shapes.
'Derived<T>::g() -> this->f() resolves to f (1 edge)',
'Derived<T>::k() -> this->base_method() resolves via EXTENDS chain (1 edge)',
'Derived<T>::g_unqualified() -> f() does NOT bind to Base<T>::f',
'Derived<T>::g_this() -> this->f() resolves to Base<T>::f (1 edge)',
'Derived<T>::g() -> this->f() emits zero CALLS edges when only hidden derived overload is arity-incompatible',
// Conversion-rank scoring (#1578 / #1606) disambiguates `f(int)` vs
// `f(double)` by ranking exact match over standard conversion. The
// legacy DAG has no conversion-rank scoring; it either picks
// arbitrarily or leaves the call unresolved. Scope-resolver-only
// correctness win.
'f(2.5) resolves to f(double) — exact match beats standard conversion',
'f(42) resolves to f(int) — exact match beats standard conversion',
'g(42) emits zero CALLS edges — int/long normalize to same type, ambiguous',
// char-literal promotion exercises the conversion ranker (step 4b).
// Legacy DAG has no conversion-rank scoring. Scope-resolver-only.
"p('a') resolves to p(int) — char promotion (rank 1) beats char→double conversion (rank 2)",
// 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)',
'g(1, 2) resolves to fixed-arity g(int, int), not g(int, ...)',
"h(1, 'a') resolves to h(int, double), not h(int, ...)",
'k(1, 2, 3) keeps the ellipsis overload viable when it is the only match',
// The legacy DAG path lacks the SFINAE / `requires`-clause aware
// overload filter (issue #1579). The two `process<T>` overloads
// guarded by mutually-exclusive `enable_if_t` predicates collapse
// into false multi-candidate ambiguity → 0 CALLS edges. The
// registry-primary path filters via `constraintCompatibility` and
// emits exactly 2 edges (one per ISO-resolved overload). Scope-
// resolver-only correctness win; backporting requires a constexpr
// evaluation engine in the legacy DAG.
'enable_if_t<is_integral_v<T>> overload binds only on integral call sites',
'enable_if_t<is_floating_point_v<T>> overload binds only on floating call sites',
'requires-clause overloads disambiguate same as enable_if_t (F4 AST shape)',
'is_pointer_v and is_class_v disambiguate pointer vs class arguments',
'is_reference_v keeps reference-shaped arguments distinct from values',
'is_class_v rejects primitive arguments while keeping class arguments',
'is_enum_v distinguishes known enum declarations from primitives',
'is_const_v and is_volatile_v disambiguate cv-qualified locals',
'is_void_v does not misclassify void pointers as void values',
// The legacy DAG path has no inline-namespace same-name ambiguity
// detection. When two inline children declare the same name, the
// legacy path picks an arbitrary match. The scope-resolver returns
// '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. PR #1810 threads call-site types through the
// resolveQualifiedReceiverMember contract — resolved in both mode paths.
// Legacy DAG emits an edge via the global callable fallback; the test
// now expects 1 edge, so the old expected-failure entry is removed.
// Normalized-signature ambiguity: `foo(int)` vs `foo(long)` both map to
// `int` via normalizeCppParamType. Scope-resolver suppresses via
// isOverloadAmbiguousAfterNormalization; legacy path picks arbitrarily.
'outer::foo(42) emits zero CALLS edges when v1 declares foo(int) and v2 declares foo(long) — both normalize to int',
// PR #1598: ADL free-function reference arg negative fixtures rely on
// scope-resolver-only correctness. The legacy DAG falls back to
// `pickUniqueGlobalCallable` which resolves the callee by simple-name
// workspace lookup, ignoring argument analysis. These fixtures expect
// zero CALLS edges (the registry-primary path correctly avoids a false-
// positive), but the legacy path emits one edge via the global fallback.
// Scope-resolver-only correctness wins; backporting is out of scope.
'process(data::value) emits zero CALLS edges \u2014 data::value is a variable, not a function',
'run_with(callback) emits zero CALLS edges when callback is a parameter, not a function reference',
// PR #1599 adversarial review findings: nearest-scope ADL blocker
// semantics and block-scope function declaration ADL suppression are
// 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',
]),
};
type ResolverParityEnv = Readonly<Record<string, string | undefined>>;
type VitestIt = typeof vitestIt;
type CallableIt = (name: string, ...args: unknown[]) => unknown;
export function resolverParityFlagName(languageSlug: string): string {
return `REGISTRY_PRIMARY_${languageSlug.toUpperCase().replace(/-/g, '_')}`;
}
export function isLegacyResolverParityRun(
languageSlug: string,
env: ResolverParityEnv = process.env,
): boolean {
const value = env[resolverParityFlagName(languageSlug)]?.trim().toLowerCase();
return value === '0' || value === 'false' || value === 'no';
}
export function isLegacyResolverParityExpectedFailure(
languageSlug: string,
testName: string,
env: ResolverParityEnv = process.env,
): boolean {
if (!isLegacyResolverParityRun(languageSlug, env)) return false;
return LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES[languageSlug]?.has(testName) ?? false;
}
export function createResolverParityIt(languageSlug: string): VitestIt {
const wrapped = ((name: string, ...args: unknown[]) => {
const runner = isLegacyResolverParityExpectedFailure(languageSlug, name)
? vitestIt.skip
: vitestIt;
return (runner as unknown as CallableIt)(name, ...args);
}) as VitestIt;
Object.assign(wrapped, vitestIt);
return wrapped;
}
export const FIXTURES = path.resolve(__dirname, '..', '..', 'fixtures', 'lang-resolution');
export const CROSS_FILE_FIXTURES = path.resolve(
__dirname,
'..',
'..',
'fixtures',
'cross-file-binding',
);
export type RelEdge = {
source: string;
target: string;
sourceLabel: string;
targetLabel: string;
sourceFilePath: string;
targetFilePath: string;
rel: GraphRelationship;
};
export function getRelationships(result: PipelineResult, type: string): RelEdge[] {
const edges: RelEdge[] = [];
for (const rel of result.graph.iterRelationships()) {
if (rel.type === type) {
const sourceNode = result.graph.getNode(rel.sourceId);
const targetNode = result.graph.getNode(rel.targetId);
edges.push({
source: sourceNode?.properties.name ?? rel.sourceId,
target: targetNode?.properties.name ?? rel.targetId,
sourceLabel: sourceNode?.label ?? 'unknown',
targetLabel: targetNode?.label ?? 'unknown',
sourceFilePath: sourceNode?.properties.filePath ?? '',
targetFilePath: targetNode?.properties.filePath ?? '',
rel,
});
}
}
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) => {
if (n.label === label) names.push(n.properties.name);
});
return names.sort();
}
export function edgeSet(edges: Array<{ source: string; target: string }>): string[] {
return edges.map((e) => `${e.source}${e.target}`).sort();
}
/** Get graph nodes by label with full properties (for parameterTypes assertions). */
export function getNodesByLabelFull(
result: PipelineResult,
label: string,
): Array<{ name: string; properties: Record<string, any> }> {
const nodes: Array<{ name: string; properties: Record<string, any> }> = [];
result.graph.forEachNode((n) => {
if (n.label === label) nodes.push({ name: n.properties.name, properties: n.properties });
});
return nodes.sort((a, b) => a.name.localeCompare(b.name));
}
// Tests can pass { skipGraphPhases: true } as third arg for faster runs
// (skips MRO, community detection, and process extraction).
export { runPipelineFromRepo };
export type { PipelineOptions, PipelineResult };