Skip to content

fix(resolution): a definition its language makes file-local is not a cross-file target - #1732

Closed
danusha2345 wants to merge 2 commits into
colbymchenry:mainfrom
danusha2345:fix/cross-file-visibility
Closed

fix(resolution): a definition its language makes file-local is not a cross-file target#1732
danusha2345 wants to merge 2 commits into
colbymchenry:mainfrom
danusha2345:fix/cross-file-visibility

Conversation

@danusha2345

@danusha2345 danusha2345 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #1730. Fixes #1731. Standalone off main (b9ca4b7), two commits — the second narrows the C rule to translation units after the first cut removed 4,306 real calls onto header static inlines; details in the comment below.

What

Name matching accepted any same-named definition as the target of a reference from another file, however the language scopes that definition. One test, isVisibleAcrossFiles, now declines a candidate in another file when:

language rule source of truth
C / C++ the function is defined in a source file (.c .cc .cpp .cxx .m .mm) and its definition line carries static — a header's static inline is textually included into every unit that names it and stays visible read from source (the extractor records no storage class; the kernel arm would need the same field)
Kotlin, Java, C#, Swift, Scala, Dart, PHP visibility === 'private' extractor
Go lowercase identifier and a different directory the name's case — the extractor's isExported is unset for every Go method, exported or not
Rust non-pub item and the reference is outside the item's module subtree extractor + path: src/net.rs / src/net/mod.rs own src/net/; a child reaches its ancestors' private items via super::. A method in an impl Trait for Type block has the trait's visibility and is exempt (read from the enclosing impl header)

Same-file candidates are always visible.

Where the test runs, and why there

In ReferenceResolver, on the target the whole name-matching pipeline settled on (exact-match, fuzzy, instance-method, qualified-name all come through it), so a rejection ends the reference unresolved. The first cut declined inside matchByExactName, #1720-style, and that let the ref fall through to matchFuzzy, which committed to a same-language namesake the ranking had passed over: eight new fuzzy edges on one tree, every one onto a JavaScript fail in a different file, the real fail being a local const fail = (msg) => … the graph does not hold (#1669's shape). matchFuzzy checks its own survivor as well, since nothing runs after it.

Measurement

Five corpora, each indexed at b9ca4b7 and at this branch (wasm arm), edge rows joined back to symbols and keyed with resolvedBy:

corpus files LOST GAINED
betaflight fork (C) 2,109 .c 145 0
Android / Go / Electron app 114 .kt, 42 .go, 96 .js 142 0
emmc-reader-gui (Rust workspace) 71 .rs 195 0
skylab_hub (Rust + Kotlin) 35 .rs, 4 .kt 92 0
vitejs/vite (JS/TS only) 1,635 0 0

Read back from source, the removals are the shapes in the two issues: STM32 USB class sources → static handlers in GD32's usbd_enum.c, usbd_get_descriptor → a static get_device_descriptor in a USB class file it never links; editor.apply() → an unrelated class's private fun apply; latch.await() in tests → a test file's private fun await (×8); leaflet.js / validate.mjs → unexported Go add / join (55 cross-language rows); Vec::new() → a private fn new in another crate (×7); ui.add(…) (egui) → a private add; f.finish()HashingWriter::finish in another crate. The Rust breakdown after the trait exemption: 171 instance-method, 8 exact-match calls, 16 references — none onto a trait-impl method.

Not touched: the Go extractor's isExported on methods (a separate extractor defect noted in #1731), the C storage class as a node field, and the local-closure gap behind the JS fail (#1669).

Tests

__tests__/cross-file-visibility.test.ts, whole pipeline over source fixtures, each invisible shape paired with the visible one of identical form: C static in another source file vs non-static vs a header's static inline (kept) vs same-file static with the keyword on its own line; Kotlin private fun apply reached through an SDK-style editor.apply() vs a public fun apply on a typed receiver; Go unexported func across packages vs within the package and an exported func elsewhere; Rust a sibling module's private fn vs a parent's private fn from a child, a trait-impl method through a typed receiver, and a pub fn anywhere. 11 cases. resolution.test.ts, frameworks-integration.test.ts, pr19-improvements.test.ts unchanged: 200 across cross-file-visibility + resolution.test.ts, tsc clean.

🤖 Generated with Claude Code

…cross-file target

Name matching accepted any same-named definition as the target of a call
from another file, however the language scopes it. isVisibleAcrossFiles
now declines, for a candidate in another file:

  C / C++   a function whose definition line carries `static` (read from
            source — the extractor records no storage class, and the kernel
            arm would need the same field)
  Kotlin, Java, C#, Swift, Scala, Dart, PHP
            visibility === 'private'
  Go        a lowercase identifier from another directory (by the name's
            case: the extractor's isExported is unset for every Go method)
  Rust      a non-`pub` item unless the reference is in the item's module
            subtree (a child sees its ancestors' private items via super::);
            a method in an `impl Trait for Type` block has the trait's
            visibility and is exempt

The test runs in ReferenceResolver on the target the whole name-matching
pipeline settled on, so a rejection ends the reference unresolved. Declining
inside matchByExactName instead let the ref fall through to matchFuzzy,
which committed to a same-language namesake the ranking had passed over —
eight edges on one tree, all onto a local `const fail = …` arrow the graph
does not hold. matchFuzzy checks its own survivor too; nothing runs after it.

Five corpora, all against b9ca4b7, wasm arm, edge rows keyed with
resolvedBy:

  betaflight fork (2,109 C files)        LOST 4,451  GAINED 0   (colbymchenry#1730)
  Android/Go/JS app (114 kt, 42 go)      LOST   142  GAINED 0   (colbymchenry#1731)
  emmc-reader-gui (71 rs)                LOST   195  GAINED 0
  skylab_hub (35 rs)                     LOST    92  GAINED 0
  vitejs/vite (JS/TS only)               LOST     0  GAINED 0

Samples read back: `Vec::new()` onto a private `fn new` in another crate,
`ui.add(…)` (egui) onto a private `add`, `latch.await()` onto a test file's
`private fun await`, `leaflet.js` onto an unexported Go `func add`,
`usbd_get_descriptor` onto a `static get_device_descriptor` in a USB class
file it never links.

Fixes colbymchenry#1730. Fixes colbymchenry#1731.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…udes it

The C rule declined any `static` function defined in another file. A
`static` in a SOURCE file is local to that translation unit and the rule is
right there; a `static` in a header — `static inline`, the whole of
MAVLink's generated `mavlink_msg_*.h` — is textually included into every
unit that names it, and the call is real. On the betaflight tree 4,306 of
the 4,451 rows the first cut removed were exactly that: `testsuite.h` and
`mavlink_msg_*.h` calling `protocol.h`'s `_mav_put_char_array`,
`mav_array_assign_char` and each other's `_pack` / `_decode` helpers.

The rule now applies only to a candidate whose file is a translation unit
(`.c .cc .cpp .cxx .c++ .m .mm`). Same tree: LOST 145, GAINED 0, every one
onto a `static` in another `.c` — STM32 USB class sources onto GD32's
`usbd_enum.c`, and the USB descriptor table shape from colbymchenry#1730. Header
targets in the removed set: 0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@danusha2345

Copy link
Copy Markdown
Contributor Author

Correction, pushed as 42a9267: the first cut's C rule was wrong for headers, and the betaflight number in the description was mostly that error.

Of the 4,451 rows it removed, 4,306 targeted a static inline in a header — MAVLink's generated mavlink_msg_*.h calling protocol.h's _mav_put_char_array / mav_array_assign_char and each other's _pack / _decode helpers. Those calls are real: a header is textually included, so the function exists in every unit that names it. Only a static defined in a translation unit is local to it.

The rule now applies to candidates in a source file (.c .cc .cpp .cxx .c++ .m .mm) only. Same tree, same base: LOST 145, GAINED 0, header targets in the removed set: 0. The 145 are the shape #1730 describes — STM32 USB class sources onto GD32's usbd_enum.c statics, and the descriptor-table get_device_* case. A test pins the header case (static inline in protocol.h, called from core.c, edge kept). The other four corpora are unaffected by the change (no C).

I'll correct #1730's count there too; the 10% figure in it was counting the header calls.

@colbymchenry colbymchenry left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linux verification (Forge) — approve the approach

Reviewed and verified on Linux (Node 22.19, CODEGRAPH_KERNEL=0). The placement is correct: isVisibleAcrossFiles runs in ReferenceResolver after matchReference settles (and again inside matchFuzzy), so a rejection leaves the ref unresolved instead of fuzzy-falling-through — the pitfall called out in #1731.

Repro on main (df435d5) — all three #1731 shapes wrongly resolve:

  • Kotlin: onCreateBudget.kt:apply (private)
  • Go: Runcmd/probe/main.go:fail (unexported, other package)
  • Rust: runsrc/util.rs:count (sibling private)

On this PR's logic (rebased onto latest main) — same fixtures decline; __tests__/cross-file-visibility.test.ts 11/11 + resolution.test.ts 189/189 pass. Rust child→ancestor + trait-impl exemptions look right; Go judged by identifier case as required while #1734 is still open.

This branch was two commits behind main (#1742 AGENTS.md, #1743 generators). To land without conflict I rebased your commits and opened #1745 (changelog cite for #1731 + C source-vs-header clarification only). Happy to close this PR in favor of #1745, or take a force-push rebase here instead — your call.

colbymchenry added a commit that referenced this pull request Sep 8, 2026
…cross-file target (#1731) (#1745)

* fix(resolution): a definition its language makes file-local is not a cross-file target

Name matching accepted any same-named definition as the target of a call
from another file, however the language scopes it. isVisibleAcrossFiles
now declines, for a candidate in another file:

  C / C++   a function whose definition line carries `static` (read from
            source — the extractor records no storage class, and the kernel
            arm would need the same field)
  Kotlin, Java, C#, Swift, Scala, Dart, PHP
            visibility === 'private'
  Go        a lowercase identifier from another directory (by the name's
            case: the extractor's isExported is unset for every Go method)
  Rust      a non-`pub` item unless the reference is in the item's module
            subtree (a child sees its ancestors' private items via super::);
            a method in an `impl Trait for Type` block has the trait's
            visibility and is exempt

The test runs in ReferenceResolver on the target the whole name-matching
pipeline settled on, so a rejection ends the reference unresolved. Declining
inside matchByExactName instead let the ref fall through to matchFuzzy,
which committed to a same-language namesake the ranking had passed over —
eight edges on one tree, all onto a local `const fail = …` arrow the graph
does not hold. matchFuzzy checks its own survivor too; nothing runs after it.

Five corpora, all against b9ca4b7, wasm arm, edge rows keyed with
resolvedBy:

  betaflight fork (2,109 C files)        LOST 4,451  GAINED 0   (#1730)
  Android/Go/JS app (114 kt, 42 go)      LOST   142  GAINED 0   (#1731)
  emmc-reader-gui (71 rs)                LOST   195  GAINED 0
  skylab_hub (35 rs)                     LOST    92  GAINED 0
  vitejs/vite (JS/TS only)               LOST     0  GAINED 0

Samples read back: `Vec::new()` onto a private `fn new` in another crate,
`ui.add(…)` (egui) onto a private `add`, `latch.await()` onto a test file's
`private fun await`, `leaflet.js` onto an unexported Go `func add`,
`usbd_get_descriptor` onto a `static get_device_descriptor` in a USB class
file it never links.

Fixes #1730. Fixes #1731.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(resolution): a static in a header is part of every unit that includes it

The C rule declined any `static` function defined in another file. A
`static` in a SOURCE file is local to that translation unit and the rule is
right there; a `static` in a header — `static inline`, the whole of
MAVLink's generated `mavlink_msg_*.h` — is textually included into every
unit that names it, and the call is real. On the betaflight tree 4,306 of
the 4,451 rows the first cut removed were exactly that: `testsuite.h` and
`mavlink_msg_*.h` calling `protocol.h`'s `_mav_put_char_array`,
`mav_array_assign_char` and each other's `_pack` / `_decode` helpers.

The rule now applies only to a candidate whose file is a translation unit
(`.c .cc .cpp .cxx .c++ .m .mm`). Same tree: LOST 145, GAINED 0, every one
onto a `static` in another `.c` — STM32 USB class sources onto GD32's
`usbd_enum.c`, and the USB descriptor table shape from #1730. Header
targets in the removed set: 0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(changelog): cite #1731 and narrow the C file-local note

Rebased #1732 onto latest main. Clarify that only a static in another
source file is declined (header static inline stays), name the Kotlin/Go/Rust
shapes from #1731, and note the post-pipeline placement that avoids fuzzy
fallback.

---------

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
@danusha2345

Copy link
Copy Markdown
Contributor Author

Closing in favor of #1745 — thanks for rebasing and verifying it on Linux rather than rewriting. The Go by-name-case rule stays until #1734 lands; I'll rebase #1734, #1715, #1718, #1735 and #1736 onto current main today.

@danusha2345 danusha2345 closed this Sep 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

2 participants