Skip to content

fix(extract): never bind a cross-file call to a definition in another language family#1718

Closed
edinaldoof wants to merge 1 commit into
Graphify-Labs:v8from
edinaldoof:fix/cross-language-call-resolution
Closed

fix(extract): never bind a cross-file call to a definition in another language family#1718
edinaldoof wants to merge 1 commit into
Graphify-Labs:v8from
edinaldoof:fix/cross-language-call-resolution

Conversation

@edinaldoof

Copy link
Copy Markdown
Contributor

Problem

The cross-file call resolver matches raw-call callees against a repo-wide label index with no language check. In a repo that mixes a web app with a native Android app (a common layout: Next.js/React + a Kotlin WebView shell), this manufactures phantom cross-language call edges — which the extraction spec itself forbids ("calls edges MUST stay within one language: a Python function cannot calls a JS/TS/Go/Rust/Java symbol and vice versa — cross-language call edges are phantom artifacts, never emit them"). The local AST resolver never enforced that rule.

Minimal repro:

// web/Upcoming.tsx
declare function register(cb: () => void): void;
export function UpcomingPanel() {
  register(refreshHeading);   // callback passed by name
  return null;
}
// android/HeadingSensorBridge.kt
class HeadingSensorBridge {
    fun refreshHeading() { println("native sensor") }
}

Current output:

UpcomingPanel --indirect_call--> .refreshHeading() [INFERRED 0.8]

A TSX component "calling" a Kotlin method. The #1659 import-evidence gate doesn't apply (it covers JS/TS direct calls only), and the indirect path's callable-target guard passes because the Kotlin method is a real callable — just in the wrong language.

Direct calls from non-JS/TS callers have the same hole with no gate at all:

# py/worker.py
def process():
    return refreshHeading()   # binds to the Kotlin fun
process() --calls--> .refreshHeading() [INFERRED 0.8]

Beyond the wrong edge itself, the damage compounds downstream: on the codebase where I hit this, the phantom edge bridged the Android community and the web-navigation community, inflating the Kotlin node's betweenness centrality enough that GRAPH_REPORT.md surfaced it as a top suggested question ("Why does this node connect community X to communities Y, Z?") — an artifact presented as an insight.

Fix

Filter resolution candidates by language interop family right after the label-index lookup, before the single-candidate/import-evidence logic runs. Families are grouped by real interop so legitimate cross-language resolution keeps working:

  • jsts: .js/.jsx/.mjs/.cjs/.ts/.tsx/.mts/.cts + .vue/.svelte/.astro (one module graph)
  • jvm: .java/.kt/.kts/.scala/.groovy/.gradle (JVM interop — a Kotlin call to a Java method still resolves)
  • native: .c/.h/.cpp/.cc/.cxx/.hpp/.cu/.cuh/.metal/.m/.mm/.swift (shared headers, ObjC/C++ mixing, Swift↔ObjC bridging)
  • single-language families for python, go, rust, ruby, php, dotnet, lua, zig, elixir, julia, dart, shell, powershell

Two deliberate permissive defaults, preserving existing behavior everywhere the guard has no opinion:

  • candidates whose family is unknown (no source_file, non-code nodes) are never filtered
  • callers with an unmapped extension skip the guard entirely

This covers both the indirect_call path and the direct-call path (the same candidates list feeds both), and can only ever narrow candidates — so an ambiguous 2-candidate name split across languages now collapses to the right-language definition instead of hitting the tie-breakers.

Tests

tests/test_cross_language_call_resolution.py (mirrors the test_phantom_cross_package_call.py structure):

  • test_tsx_callback_does_not_bind_to_kotlin_method — the repro above
  • test_python_call_does_not_bind_to_kotlin_function — direct-call path for non-JS/TS callers
  • test_same_language_callback_still_resolves — positive control: TS callback with import evidence keeps its INFERRED indirect_call
  • test_jvm_interop_kotlin_call_to_java_still_resolves — positive control: Kotlin→Java keeps resolving (verified this passes on main too, so the family grouping changes nothing for JVM interop)

Full suite: 2986 passed; the handful of failures on my machine (test_skillgen, test_hooks, test_ollama_retry_cap, test_detect::test_graphifyignore_hermetic_without_vcs) fail identically on a clean checkout of main — environment-related, untouched by this change.

… language family

The cross-file call resolver matches raw-call callees against a repo-wide
label index with no language check. In a repo that mixes a web app with a
native Android app, a TSX callback passed by name (register(refreshHeading))
resolved to a same-named Kotlin method and shipped as an INFERRED
indirect_call edge — a phantom the extraction spec itself forbids ('calls
edges MUST stay within one language'). Direct calls from non-JS/TS callers
had the same hole with no gate at all: a bare Python call bound to the lone
same-named Kotlin fun. Found on a production Next.js + Android codebase,
where the phantom edge also inflated the Kotlin node's betweenness enough
to surface it as a top suggested question in GRAPH_REPORT.md.

Resolution candidates are now filtered by language interop family before
the single-candidate/import-evidence logic runs. Families are grouped by
REAL interop so legitimate cross-language resolution keeps working:
Kotlin/Java/Scala/Groovy share the JVM, C/C++/Objective-C/CUDA share
headers (Swift bridges to ObjC), and JS/TS variants plus Vue/Svelte/Astro
SFCs compile into one module graph. Candidates whose family is unknown
(no source_file, non-code nodes) are never filtered, preserving the
previous permissive behavior, and callers with an unmapped extension skip
the guard entirely.
@safishamsi

Copy link
Copy Markdown
Collaborator

Merged into v8 (bf7fa50), authorship preserved. Reviewed, full suite green (3078 passed). Thanks for the fix!

@safishamsi safishamsi closed this Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants