Skip to content

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

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant