Files
codegraph/__tests__/rust-go-call-shape.test.ts
Colby Mchenry 715c62b0d9 fix(rust,go): read a call's shape before guessing its method (#2167)
The extractors keep one receiver level, so chain links arrive as bare names.
Read at the call site:
- bare (no `.` / `::` before it): never a method — Rust reaches one only
  through `self.` / `Type::`, Go through a receiver (axum's routing
  `get(handler)` went to a cookie jar's `get`, tokio's `drop(x)` to `Drop`
  impls, #1861's pinned `reset()` → `Target::reset` is now the free fn);
- chained (after `.`, receiver read back through arguments, macros `name!`,
  `::` paths and Go composite literals): never a free function, and a
  standard-library method name (Option/Result/iterator/collection/string
  methods; Go's String/Error/Header/locks/reflect/time) needs a receiver
  named after its owner — project-specific names keep their match (clap's
  `flag("n").short('n')` → Arg::short, cobra's `c.Root().Name()`, gin's
  `c.Set(…)`).
The same std gate applies to the method-call matcher's unique-name guess.

A/B (edges removed / added): clap -1496/+93, tokio -1064/+235, axum
-695/+42, ripgrep -477/+32, cobra -167, serde -119/+1, bat -40/+4, gin
-32/+26.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:58:08 +00:00

94 lines
3.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Rust and Go extractors keep one receiver level, so a chain's later links
* (`sym.filename().map(…)`, `c.Flags().String(…)`) arrive as bare names. Read
* at the call: a bare call never reaches a method (axum's routing `get(h)`
* went to a cookie jar's `get`, tokio's `drop(x)` to a `Drop` impl); a chained
* call with a standard-library name (`unwrap`, `map`, Go's `String`) needs a
* receiver named after the owner, while a project-specific one keeps its
* match (clap's `flag("n").short('n')` → `Arg::short`).
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-go-shape-'));
const files: Record<string, string> = {
'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n',
'src/guard.rs': `pub struct MutexGuard;
impl MutexGuard {
pub fn map(self) -> Self { self }
}
pub struct CookieJar;
impl CookieJar {
pub fn get(&self) -> Option<u8> { None }
}
pub struct Arg;
impl Arg {
pub fn short(self, c: char) -> Self { self }
}
pub fn flag(name: &str) -> Arg { Arg }
`,
'src/main.rs': `mod guard;
use guard::flag;
fn get(handler: u8) -> u8 { handler }
fn run(sym: Option<&str>) {
let _ = sym.map(|s| s.len());
let _ = get(1);
let _ = flag("n").short('n');
}
fn main() { run(None); }
`,
'go.mod': 'module example.com/app\n\ngo 1.21\n',
'flags.go': `package app
type customString struct{}
func (c customString) String() string { return "" }
type FlagSet struct{}
func Flags() *FlagSet { return nil }
`,
'cmd.go': `package app
func run() {
Flags().String("name", "", "")
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
const callsIn = (file: string, fn: string): string[] => {
const node = cg.getNodesInFile(file).find((n) => n.name === fn)!;
return cg.getOutgoingEdges(node.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName);
};
describe('Rust and Go call shapes', () => {
it('Rust: bare never a method, std chain names need their receiver, project chain names keep theirs', () => {
const targets = callsIn('src/main.rs', 'run');
expect(targets.some((t) => t.endsWith('MutexGuard::map'))).toBe(false);
expect(targets.some((t) => t.endsWith('CookieJar::get'))).toBe(false);
expect(targets.some((t) => t.endsWith('Arg::short'))).toBe(true);
});
it('Go: a std-named chained call is not a project type’s method', () => {
expect(callsIn('cmd.go', 'run').some((t) => t.endsWith('customString::String'))).toBe(false);
});
});