* 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>
151 lines
6.2 KiB
TypeScript
151 lines
6.2 KiB
TypeScript
/**
|
|
* A definition the language makes file-local is not a candidate for a
|
|
* cross-file name match: a C `static`, a Kotlin `private fun`, a Go unexported
|
|
* identifier in another package, a Rust non-`pub` item outside its module
|
|
* subtree. Each case pairs the invisible shape with the visible one of
|
|
* identical form, so the assertion discriminates on visibility alone.
|
|
*/
|
|
|
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import CodeGraph from '../src/index';
|
|
|
|
let tempDir: string;
|
|
let cg: CodeGraph | null = null;
|
|
|
|
function project(files: Record<string, string>): void {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-visibility-'));
|
|
for (const [rel, content] of Object.entries(files)) {
|
|
const abs = path.join(tempDir, rel);
|
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
fs.writeFileSync(abs, content);
|
|
}
|
|
}
|
|
|
|
/** `calls` targets of the function named `caller`, as `file:name` strings. */
|
|
async function calleesOf(caller: string): Promise<string[]> {
|
|
cg = await CodeGraph.init(tempDir, { index: true });
|
|
cg.resolveReferences();
|
|
const from = cg.getNodesByKind('function').concat(cg.getNodesByKind('method')).find((n) => n.name === caller)!;
|
|
expect(from).toBeDefined();
|
|
return cg
|
|
.getOutgoingEdges(from.id)
|
|
.filter((e) => e.kind === 'calls')
|
|
.map((e) => cg!.getNode(e.target))
|
|
.filter((n): n is NonNullable<typeof n> => !!n)
|
|
.map((n) => `${n.filePath}:${n.name}`);
|
|
}
|
|
|
|
afterEach(() => {
|
|
cg?.close();
|
|
cg = null;
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('C: a static function is local to its translation unit', () => {
|
|
it('does not resolve a call onto a static in another file', async () => {
|
|
project({
|
|
'core.c': 'void coreRun(void)\n{\n usbGetDescriptor();\n}\n',
|
|
'usb_audio.c': 'static void usbGetDescriptor(void)\n{\n}\n',
|
|
});
|
|
expect(await calleesOf('coreRun')).not.toContain('usb_audio.c:usbGetDescriptor');
|
|
});
|
|
|
|
it('still resolves onto a non-static function in another file', async () => {
|
|
project({
|
|
'core.c': 'void coreRun(void)\n{\n usbGetDescriptor();\n}\n',
|
|
'usb_audio.c': 'void usbGetDescriptor(void)\n{\n}\n',
|
|
});
|
|
expect(await calleesOf('coreRun')).toContain('usb_audio.c:usbGetDescriptor');
|
|
});
|
|
|
|
it('keeps a static inline defined in a header: it lives in every unit that includes it', async () => {
|
|
project({
|
|
'protocol.h': 'static inline void mav_put_char(char *buf, char c)\n{\n buf[0] = c;\n}\n',
|
|
'core.c': '#include "protocol.h"\n\nvoid coreRun(char *b)\n{\n mav_put_char(b, 0);\n}\n',
|
|
});
|
|
expect(await calleesOf('coreRun')).toContain('protocol.h:mav_put_char');
|
|
});
|
|
|
|
it('keeps a same-file static, whichever line the keyword is on', async () => {
|
|
project({
|
|
'core.c': 'static void\nhelper(void)\n{\n}\n\nvoid coreRun(void)\n{\n helper();\n}\n',
|
|
'other.c': 'static void helper(void)\n{\n}\n',
|
|
});
|
|
expect(await calleesOf('coreRun')).toEqual(['core.c:helper']);
|
|
});
|
|
});
|
|
|
|
describe('Kotlin: a private function is class- or file-local', () => {
|
|
it('does not resolve an SDK-style call onto another file\'s private fun', async () => {
|
|
project({
|
|
'Budget.kt': 'class Budget {\n private fun apply(bps: Long): Long = bps\n}\n',
|
|
'Main.kt': 'class Main {\n fun onCreate(editor: Editor) {\n editor.apply()\n }\n}\n',
|
|
});
|
|
expect(await calleesOf('onCreate')).not.toContain('Budget.kt:apply');
|
|
});
|
|
|
|
it('still resolves onto a public fun in another file', async () => {
|
|
project({
|
|
'Budget.kt': 'class Budget {\n fun apply(bps: Long): Long = bps\n}\n',
|
|
'Main.kt': 'class Main {\n fun onCreate(budget: Budget) {\n budget.apply(1L)\n }\n}\n',
|
|
});
|
|
expect(await calleesOf('onCreate')).toContain('Budget.kt:apply');
|
|
});
|
|
});
|
|
|
|
describe('Go: an unexported identifier is package-local', () => {
|
|
it('does not resolve a call onto an unexported func in another package', async () => {
|
|
project({
|
|
'cmd/probe/main.go': 'package main\n\nfunc fail(msg string) {}\n',
|
|
'server/turn.go': 'package server\n\nfunc Run() {\n\tfail("x")\n}\n',
|
|
});
|
|
expect(await calleesOf('Run')).not.toContain('cmd/probe/main.go:fail');
|
|
});
|
|
|
|
it('still resolves within the package and onto an exported func elsewhere', async () => {
|
|
project({
|
|
'server/util.go': 'package server\n\nfunc fail(msg string) {}\n',
|
|
'server/turn.go': 'package server\n\nfunc Run() {\n\tfail("x")\n\tReport()\n}\n',
|
|
'report/report.go': 'package report\n\nfunc Report() {}\n',
|
|
});
|
|
const callees = await calleesOf('Run');
|
|
expect(callees).toContain('server/util.go:fail');
|
|
expect(callees).toContain('report/report.go:Report');
|
|
});
|
|
});
|
|
|
|
describe('Rust: a non-pub item is visible to its module subtree only', () => {
|
|
it('does not resolve a sibling module\'s private fn, nor another crate\'s', async () => {
|
|
project({
|
|
'src/main.rs': 'mod util;\nmod net;\nfn main() {}\n',
|
|
'src/util.rs': 'fn count() -> usize { 0 }\n',
|
|
'src/net.rs': 'pub fn run() -> usize {\n count()\n}\n',
|
|
});
|
|
expect(await calleesOf('run')).not.toContain('src/util.rs:count');
|
|
});
|
|
|
|
it('keeps a trait-impl method, which has the trait\'s visibility', async () => {
|
|
project({
|
|
'src/main.rs': 'mod shape;\nmod draw;\nfn main() {}\n',
|
|
'src/shape.rs': 'pub struct Circle;\npub trait Area { fn area(&self) -> f64; }\nimpl Area for Circle {\n fn area(&self) -> f64 { 1.0 }\n}\n',
|
|
'src/draw.rs': 'use crate::shape::{Area, Circle};\npub fn render(c: &Circle) -> f64 {\n c.area()\n}\n',
|
|
});
|
|
expect(await calleesOf('render')).toContain('src/shape.rs:area');
|
|
});
|
|
|
|
it('still resolves a parent module\'s private fn from a child, and any pub fn', async () => {
|
|
project({
|
|
'src/main.rs': 'mod net;\nmod util;\nfn main() {}\n',
|
|
'src/net.rs': 'pub mod tcp;\nfn shared() {}\n',
|
|
'src/net/tcp.rs': 'use super::shared;\nuse crate::util::exported;\npub fn open() {\n shared();\n exported();\n}\n',
|
|
'src/util.rs': 'pub fn exported() {}\n',
|
|
});
|
|
const callees = await calleesOf('open');
|
|
expect(callees).toContain('src/net.rs:shared');
|
|
expect(callees).toContain('src/util.rs:exported');
|
|
});
|
|
});
|