mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-07 09:16:16 +08:00
fix(impact): gate cross-known-family references/imports (TS↔native name collisions)
The cross-language gate added in dbc4862 covered only `references` edges from
the import/name-match strategies. Two holes remained, both surfaced validating
react-native-async-storage (a TS `type TestRunner` colliding with a Kotlin
`class TestRunner`):
1. Framework strategy was ungated. The React PascalCase component resolver
name-matches `getNodesByName` with no language check (its COMPONENT_KINDS
includes `class`), so a TS ref to `TestRunner` resolved onto the Kotlin
class at confidence 0.8 — outranking the cross-language-penalized (0.5) TS
name-match. Same hole for Svelte/Vue.
2. `imports` edges were never gated (only `references`), so a TS `import React`
matched a Swift `React`, and a C++ `#include "X.h"` matched a same-named
ObjC header on another platform (basename collision).
Adds `crossesKnownFamily` (both sides in a KNOWN family — jvm/apple/web/c — and
different) and applies it:
- `gateFrameworkLanguage`: new gate on the framework strategy for
references/imports. Both-known so config↔code bridges (yaml/blade side is not
a known family) and `calls` bridges (different kind) are untouched.
- `gateLanguage` (strategies 2/3): extended to also gate `imports`. Both-known
so a `.vue`/`.svelte` file (own language tag) importing a `.ts` module is left
alone, while a C++→ObjC header basename collision is dropped.
- `applyLanguageGate` (name-match candidates): filters `imports` candidates too,
so a ref RE-POINTS to the same-family target instead of merely dropping.
Validated: async-storage cross-known-family references/imports → 0 (the 6 TS
refs + 5 imports re-point to the TS type); rn-device-info JS↔native `calls`
bridges intact (91 JS→Java, 37 JS→ObjC, full Java↔ObjC↔C++ pairing). Node count
unaffected (edge-resolution change only). Two regression tests added (both fail
without the fix). Full suite 1167 passed.
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
74b599c86b
commit
082353eb7f
+42
-11
@@ -16,7 +16,7 @@ import {
|
||||
FrameworkResolver,
|
||||
ImportMapping,
|
||||
} from './types';
|
||||
import { matchReference, sameLanguageFamily } 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';
|
||||
@@ -622,11 +622,13 @@ export class ReferenceResolver {
|
||||
|
||||
const candidates: ResolvedRef[] = [];
|
||||
|
||||
// Strategy 1: Try framework-specific resolution. NOT language-gated:
|
||||
// framework resolvers deliberately bridge config↔code across languages
|
||||
// (Drupal `routing.yml` → PHP controller, Spring `@Value` → YAML key).
|
||||
// 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);
|
||||
@@ -951,15 +953,44 @@ export class ReferenceResolver {
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop a resolution that crosses a language family when the reference is
|
||||
* `sameLanguageOnly` (a `Type.member` static read names a same-family type,
|
||||
* never a coincidentally same-named symbol in another language). Covers ALL
|
||||
* strategies (framework / import / name-match) at one chokepoint.
|
||||
* 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.
|
||||
*/
|
||||
private gateLanguage(result: ResolvedRef | null, ref: UnresolvedRef): ResolvedRef | null {
|
||||
if (!result || ref.referenceKind !== 'references') return result;
|
||||
if (!result) return result;
|
||||
const tgt = this.getLanguageFromNodeId(result.targetNodeId);
|
||||
if (tgt && ref.language && !sameLanguageFamily(tgt, ref.language)) return null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,15 +85,46 @@ export function sameLanguageFamily(a: string, b: string): boolean {
|
||||
return fa !== undefined && fa === LANGUAGE_FAMILY[b];
|
||||
}
|
||||
/**
|
||||
* Drop cross-family candidates for a `references` (type-usage) edge. A type used
|
||||
* in language X — a field/param/return type, a `Type.member` static read — names
|
||||
* a same-family type, never a coincidentally same-named symbol in another
|
||||
* language (the Android `BatteryManager` system class vs a JS `BatteryManager`).
|
||||
* Cross-language communication is modeled by `calls` bridges, not `references`.
|
||||
* True when `lang` belongs to a known multi-language family (jvm/apple/web/c).
|
||||
* Languages not listed (php, python, go, ruby, rust, dart, …) and config
|
||||
* formats (yaml/xml/blade) form their own singleton families and return
|
||||
* `false` — used to leave config↔code framework bridges (whose config side is
|
||||
* never a known programming-language family) out of the cross-family gate.
|
||||
*/
|
||||
export function isKnownLanguageFamily(lang: string): boolean {
|
||||
return LANGUAGE_FAMILY[lang] !== undefined;
|
||||
}
|
||||
/**
|
||||
* True when `a` and `b` are two DIFFERENT *known* language families — the
|
||||
* signature of a coincidental cross-language name collision (a TS `import
|
||||
* React` matching a Swift `import React`, a C++ `#include "X.h"` matching a
|
||||
* same-named ObjC header on another platform). The both-*known* test is
|
||||
* deliberately weaker than {@link sameLanguageFamily}'s negation: a
|
||||
* single-file-component language that carries its own tag (`vue`/`svelte`)
|
||||
* importing a `.ts` module, or any singleton-family language (php/go/ruby/…),
|
||||
* returns `false` here and is left alone.
|
||||
*/
|
||||
export function crossesKnownFamily(a: string, b: string): boolean {
|
||||
return isKnownLanguageFamily(a) && isKnownLanguageFamily(b) && !sameLanguageFamily(a, b);
|
||||
}
|
||||
/**
|
||||
* Drop cross-language candidates from a name lookup. Two regimes:
|
||||
* - `references` (type-usage): a type named in language X resolves to a
|
||||
* SAME-family type, never a coincidentally same-named symbol in another
|
||||
* language (the Android `BatteryManager` system class vs a JS one). Strict
|
||||
* same-family filter — cross-language communication is `calls`, not refs.
|
||||
* - `imports` (import binding): an `import`/`#include` never crosses two
|
||||
* KNOWN families (TS `import React` ↮ Swift `import React`). Weaker
|
||||
* both-known filter so `.vue`/`.svelte` (own tag) importing `.ts` survives.
|
||||
*/
|
||||
function applyLanguageGate(candidates: Node[], ref: UnresolvedRef): Node[] {
|
||||
if (ref.referenceKind !== 'references') return candidates;
|
||||
return candidates.filter((c) => sameLanguageFamily(c.language, ref.language));
|
||||
if (ref.referenceKind === 'references') {
|
||||
return candidates.filter((c) => sameLanguageFamily(c.language, ref.language));
|
||||
}
|
||||
if (ref.referenceKind === 'imports') {
|
||||
return candidates.filter((c) => !crossesKnownFamily(c.language, ref.language));
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user