feat(impact): cross-language blast-radius coverage (22 languages + 14 frameworks) (#708)
Completes the cross-file dependency graph behind impact / affected / explore across all 22 supported languages and 14 web frameworks, validated on real-world repos (measured fair-coverage table added to the README). Per-language resolution + framework resolvers/synthesizers (Lua/Luau require, Shopify OS 2.0 Liquid sections, Delphi forms, Rust cross-module + Rocket macros, Swift Fluent, SvelteKit/Nuxt loader/component conventions, RN/Expo bridges). 0 cross-family false edges, full suite green (1187 passed). See #708. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bfa84d32b8
commit
07af3db6c7
+124
-5
@@ -16,7 +16,7 @@ import {
|
||||
FrameworkResolver,
|
||||
ImportMapping,
|
||||
} from './types';
|
||||
import { matchReference } from './name-matcher';
|
||||
import { matchReference, sameLanguageFamily, crossesKnownFamily } from './name-matcher';
|
||||
import { resolveViaImport, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs } from './import-resolver';
|
||||
import { detectFrameworks } from './frameworks';
|
||||
import { synthesizeCallbackEdges } from './callback-synthesizer';
|
||||
@@ -185,6 +185,10 @@ export class ReferenceResolver {
|
||||
private queries: QueryBuilder;
|
||||
private context: ResolutionContext;
|
||||
private frameworks: FrameworkResolver[] = [];
|
||||
// Per-`.razor`/`.cshtml`-file `@using` namespace set (own directives + folder
|
||||
// `_Imports.razor`, cascading to the project root). Used to disambiguate a
|
||||
// markup type ref to the right C# namespace.
|
||||
private razorUsingsCache = new Map<string, string[]>();
|
||||
// All per-resolver caches are LRU-bounded. Previously these were
|
||||
// unbounded Maps that grew with every distinct lookup and OOM'd on
|
||||
// codebases with 20k+ files (see issue: unbounded cache growth).
|
||||
@@ -560,6 +564,16 @@ export class ReferenceResolver {
|
||||
const receiver = name.substring(0, colonIdx);
|
||||
const member = name.substring(colonIdx + 2);
|
||||
if (this.knownNames.has(receiver) || this.knownNames.has(member)) return true;
|
||||
// Multi-segment path `a::b::c` (a Rust/C++ module call like
|
||||
// `database::profiles::find`) — the only segment that names a symbol is
|
||||
// the last (`c`); `member` above is `b::c`, which never matches a node
|
||||
// name, so without this the pre-filter drops the ref before the Rust path
|
||||
// resolver ever sees it. Mirror the dotted-name leaf check above.
|
||||
const lastColon = name.lastIndexOf('::');
|
||||
if (lastColon > colonIdx) {
|
||||
const tail = name.substring(lastColon + 2);
|
||||
if (tail && this.knownNames.has(tail)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// For path-like references (e.g., "snippets/drawer-menu.liquid"), check the filename
|
||||
@@ -620,11 +634,25 @@ export class ReferenceResolver {
|
||||
const jvmImport = resolveJvmImport(ref, this.context);
|
||||
if (jvmImport) return jvmImport;
|
||||
|
||||
// Razor/Blazor: a markup or `@code` type ref resolves through the file's
|
||||
// `@using` namespaces (incl. folder `_Imports.razor`). This precisely
|
||||
// disambiguates a simple name that exists in several namespaces — e.g.
|
||||
// `CatalogBrand` resolving to `BlazorShared.Models::CatalogBrand` (the DTO,
|
||||
// which the `.razor` `@using`s) rather than the same-named domain entity.
|
||||
if (ref.language === 'razor') {
|
||||
const razorResult = this.resolveRazorUsing(ref);
|
||||
if (razorResult) return razorResult;
|
||||
}
|
||||
|
||||
const candidates: ResolvedRef[] = [];
|
||||
|
||||
// Strategy 1: Try framework-specific resolution
|
||||
// Strategy 1: Try framework-specific resolution. Cross-language bridges
|
||||
// are deliberately preserved (Drupal `routing.yml` → PHP controller, RN
|
||||
// JS → native `calls`) — `gateFrameworkLanguage` only drops a type/import
|
||||
// edge between two KNOWN families (see its doc), never a `calls` bridge or
|
||||
// a config↔code edge.
|
||||
for (const framework of this.frameworks) {
|
||||
const result = framework.resolve(ref, this.context);
|
||||
const result = this.gateFrameworkLanguage(framework.resolve(ref, this.context), ref);
|
||||
if (result) {
|
||||
if (result.confidence >= 0.9) return result; // High confidence, return immediately
|
||||
candidates.push(result);
|
||||
@@ -632,14 +660,14 @@ export class ReferenceResolver {
|
||||
}
|
||||
|
||||
// Strategy 2: Try import-based resolution
|
||||
const importResult = resolveViaImport(ref, this.context);
|
||||
const importResult = this.gateLanguage(resolveViaImport(ref, this.context), ref);
|
||||
if (importResult) {
|
||||
if (importResult.confidence >= 0.9) return importResult;
|
||||
candidates.push(importResult);
|
||||
}
|
||||
|
||||
// Strategy 3: Try name matching
|
||||
const nameResult = matchReference(ref, this.context);
|
||||
const nameResult = this.gateLanguage(matchReference(ref, this.context), ref);
|
||||
if (nameResult) {
|
||||
candidates.push(nameResult);
|
||||
}
|
||||
@@ -947,6 +975,97 @@ export class ReferenceResolver {
|
||||
const node = this.queries.getNodeById(nodeId);
|
||||
return node?.language || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop an import/name-strategy resolution that crosses a language family.
|
||||
* Two regimes (mirrors `applyLanguageGate`'s candidate filter):
|
||||
* - `references` (type usage): STRICT — a `Type.member` static read names a
|
||||
* same-family type, never a coincidentally same-named symbol in another
|
||||
* language. Drops any non-same-family target.
|
||||
* - `imports` (import binding / `#include`): both-known — a C++ `#include
|
||||
* "X.h"` must not resolve to a same-named ObjC header on another platform
|
||||
* (basename collision), but a singleton-family / SFC language (`vue` →
|
||||
* `.ts`) importing across is left alone.
|
||||
* Applies to the import (strategy 2) + name-match (strategy 3) results.
|
||||
*/
|
||||
/**
|
||||
* Collect the `@using` namespaces in scope for a `.razor`/`.cshtml` file: its
|
||||
* own `@using` directives plus every `_Imports.razor` from the file's folder up
|
||||
* to the project root (Razor `_Imports` cascade). Cached per file.
|
||||
*/
|
||||
private getRazorUsings(filePath: string): string[] {
|
||||
const cached = this.razorUsingsCache.get(filePath);
|
||||
if (cached) return cached;
|
||||
const usings = new Set<string>();
|
||||
const addFrom = (src: string | null): void => {
|
||||
if (!src) return;
|
||||
for (const m of src.matchAll(/^\s*@using\s+(?:static\s+)?([A-Za-z_][\w.]*)/gm)) usings.add(m[1]!);
|
||||
};
|
||||
addFrom(this.context.readFile(filePath));
|
||||
let dir = filePath.includes('/') ? filePath.slice(0, filePath.lastIndexOf('/')) : '';
|
||||
// Walk up to the project root, reading each level's _Imports.razor.
|
||||
for (;;) {
|
||||
addFrom(this.context.readFile(dir ? `${dir}/_Imports.razor` : '_Imports.razor'));
|
||||
if (!dir) break;
|
||||
const slash = dir.lastIndexOf('/');
|
||||
dir = slash >= 0 ? dir.slice(0, slash) : '';
|
||||
}
|
||||
const arr = [...usings];
|
||||
this.razorUsingsCache.set(filePath, arr);
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a Razor/Blazor simple type ref through the file's `@using`
|
||||
* namespaces: `CatalogBrand` + `@using BlazorShared.Models` → the node whose
|
||||
* qualified name is `BlazorShared.Models::CatalogBrand`. Only resolves when the
|
||||
* `@using` set yields exactly ONE type (otherwise it stays ambiguous and falls
|
||||
* through to name-matching).
|
||||
*/
|
||||
private resolveRazorUsing(ref: UnresolvedRef): ResolvedRef | null {
|
||||
if (ref.referenceName.includes('.') || ref.referenceName.includes('::')) return null;
|
||||
const usings = this.getRazorUsings(ref.filePath);
|
||||
if (usings.length === 0) return null;
|
||||
const found = new Map<string, Node>();
|
||||
for (const ns of usings) {
|
||||
for (const cand of this.context.getNodesByQualifiedName(`${ns}::${ref.referenceName}`)) {
|
||||
found.set(cand.id, cand);
|
||||
}
|
||||
}
|
||||
if (found.size !== 1) return null;
|
||||
const target = found.values().next().value!;
|
||||
return { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'import' };
|
||||
}
|
||||
|
||||
private gateLanguage(result: ResolvedRef | null, ref: UnresolvedRef): ResolvedRef | null {
|
||||
if (!result) return result;
|
||||
const tgt = this.getLanguageFromNodeId(result.targetNodeId);
|
||||
if (!tgt || !ref.language) return result;
|
||||
if (ref.referenceKind === 'references' && !sameLanguageFamily(tgt, ref.language)) return null;
|
||||
if (ref.referenceKind === 'imports' && crossesKnownFamily(tgt, ref.language)) return null;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop a FRAMEWORK-strategy resolution that crosses two *known* language
|
||||
* families for a type-usage (`references`) or import-binding (`imports`)
|
||||
* edge. The framework strategy is intentionally ungated for cross-language
|
||||
* bridges, but those legitimate bridges are either `calls` edges (RN/Expo
|
||||
* JS → native) or config↔code edges whose config side (`yaml`/`blade`/…) is
|
||||
* not a known programming-language family. A `references`/`imports` edge
|
||||
* between two *known* families is always a coincidental name collision — the
|
||||
* React/Svelte/Vue PascalCase component resolvers name-match `getNodesByName`
|
||||
* without a language check, so a TS `<TestRunner>` ref happily matched a
|
||||
* Kotlin `class TestRunner`. Gating only the both-known-cross-family case
|
||||
* lets config bridges and `calls` bridges through untouched.
|
||||
*/
|
||||
private gateFrameworkLanguage(result: ResolvedRef | null, ref: UnresolvedRef): ResolvedRef | null {
|
||||
if (!result) return result;
|
||||
if (ref.referenceKind !== 'references' && ref.referenceKind !== 'imports') return result;
|
||||
const tgt = this.getLanguageFromNodeId(result.targetNodeId);
|
||||
if (tgt && ref.language && crossesKnownFamily(tgt, ref.language)) return null;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user