mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 01:29:02 +08:00
fix(rust): a module-path call links the function its module declares
A Rust path into a module, `crate::util::take(1)`, `super::util::take(1)`,
`util::take(1)` or a `use crate::util::take;`, went to the first node of
that name in the module's file, methods included, so an `impl Buf { fn
take }` above `pub fn take` caught every such call. A function written
inside another function, or in a `mod tests { ... }` above the module's
own item, could take it too, a path into the module's own file linked
nothing, and a path through a module a `use` takes from the standard
library (`fs::read` under `use std::fs;`) went to the project's own `fs`
module.
The path's item is now the module's own: never a method, an impl's or a
trait's associated item, or an item of a function body; one written at
the file's top level before one in a module the file declares inline,
which the path reaches through the module's re-export (`#[cfg(unix)] mod
imp { ... }` + `pub use imp::*;`). A path whose head a `use` binds from
the standard library is left to it (main's isBoundToOutOfRepoImport rule
for bare names), and a path into the module's own file resolves.
Validation:
- rust-module-path-call.test.ts: 8/8; on main 7 fail (method Buf::take,
Deep::get, Only::grab, no import edge, the inline-module namesake, the
re-export from its own file, std `fs::read` -> src/fs/mod.rs).
- A/B on main 7cceba52, 6 Rust repos: 162 changed sites, all inspected and
right. gleam: 133 calls like `fs::write(...)` / `crate::fs::mkdir(...)` move
from `impl ... for ProjectIO`'s methods to compiler-cli's free functions,
4 wrong `type_::list(...)` -> `Type::list` edges are gone, 15 `use`s link;
tokio: `crate::io::uring::open::open(...)` moves from `Op::open` to the free
`open`, +4; dioxus +7, 1 more; ripgrep +1; clap and serde unchanged.
express and flask byte-identical.
- Cost (medians of 3 interleaved runs): tokio CPU 36.6 -> 36.0 s, gleam
34.3 -> 35.2 s, both within the runs' spread; peak RSS unchanged. In a
CPU profile of tokio the changed functions take under 0.1 s. The
standard-library check reads the file's `use`s, so it runs only when the
path would otherwise link a project item (asked for every path, it cost
0.3 s on tokio).
This is the separable part of PR #2308. Its bare-call bindings (`as`
aliases, block-local `use`, shadowing by locals, enum variants through a
`use`) and the second `use` parser it adds are left out: main's `use` walk
(rustUsesOf, rustWalkModules, rustModuleItems) and scope reader
(rust-scopes.ts) cover the same ground, and folding those cases into them
is proposed separately.
Original PR #2308 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Colby Mchenry
co-authored by
Claude Opus 5.5
parent
69cddc0156
commit
3f8e8b0f65
@@ -119,6 +119,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- The call paths `codegraph context` prints now say what bridges each dynamic-dispatch hop, in the same words `codegraph_explore` uses: a call through an interface reads `interface → impl`, and a C++ override, a Redux thunk or a Gin middleware chain is named as one. Before, every hop of a kind it didn't know was labeled an "event", so a call through an interface looked like an event being emitted.
|
||||
- `codegraph_explore` and `codegraph context` now include some of the subtypes and implementors of a class or interface you ask about, and the type hierarchy the library returns for one (`getTypeHierarchy`) now lists all of them. Before, a bug in the hierarchy walk left every type without subtypes, so only what it extends or implements came back. Thanks @jocax for the report and @danusha2345 for the fix. (#2833)
|
||||
- In Rust, code that uses an enum only through its variants, like `mode::Mode::A` in an expression, a `Mode::B =>` match arm or `Self::A` inside the enum's own `impl`, now shows up in that enum's callers and impact, linked to the enum that actually declares the variant rather than to a same-named type elsewhere. Standard-library variants like `Ordering::Less` or `Option::Some` and associated items like `Foo::new()` or `Foo::MAX` don't count as using a project enum. Re-index Rust projects after upgrading. Thanks @mg-mg-mg for the report and @danusha2345. (#2328)
|
||||
- In Rust, a call written through a module path, like `crate::util::take(3)`, `super::util::take(3)` or `util::take(3)`, and a `use` of a function, like `use crate::util::take;`, now link to the function that module declares. Before, they could land on a method of the same name declared above it in that file, like an `impl Buf`'s `take`, or on a function written inside another function, and a call through a module the standard library provides, like `fs::read(…)` under `use std::fs;`, could link to your own `fs` module's `read`. Re-index Rust projects after upgrading. Thanks @danusha2345. (#2308)
|
||||
- Axum routes now link to their handler when rustfmt wraps the `.route(` call so the handler sits on a later line, and when the handler's name also appears elsewhere on the line, as in `delete(handlers::user::delete)` or `.route("/login", post(handlers::user::login))`; Actix's `web::resource(…)` and `.route(…)` registrations get the same fix. A call inside a closure handler is no longer mistaken for a route, and an Actix resource no longer takes the next route's handler as its own. Thanks @mg-mg-mg for the report and @danusha2345. (#2326)
|
||||
- In Play projects, a route or a call like `MediaType.parse(...)` now links to a method of the class or object it names, and a call to an overloaded one to the overload its arguments fit. Before, it went to the first method of that name anywhere in the class's file, often a nested or sibling class's. When a Scala and a Java type share a name, as Play's own `RequestAuthority` does, each language's calls now reach their own. Thanks @danusha2345.
|
||||
- In JavaScript and TypeScript, the functions written inside a named object literal are now symbols of their own even when the object isn't exported: a plain `const api = { load() {…} }`, an object declared inside an IIFE or a function, and a namespace hung on the page or on another object, like `window.App = { init() {…} }`, `App.utils = {…}` or `dw_page = {…}`. Calls such as `api.load()`, `window.App.init()`, `App.utils.pad()`, a sibling's `this.render()`, and `App.init()` from another script on the page now reach them, the calls made inside a member belong to that member instead of the object, and a bare `init()` is no longer taken for `App.init`, so script-tag apps no longer lose most of their code from `callers` and impact. A member's qualified name now carries its object, exported or not (`api::load`, `window.App::init`), so asking for `App.init` finds it. Re-index JavaScript and TypeScript projects after upgrading. Thanks @tkhoaaa for the report and @danusha2345. (#2300)
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* A Rust call written through a module path — `crate::util::take(3)`,
|
||||
* `super::util::take(3)`, `self::util::take(3)`, `util::take(3)` after
|
||||
* `use crate::util;` — is the free function that module declares, and so is
|
||||
* a `use crate::util::take;`. A path never names a method (that takes
|
||||
* `Type::method`), nor a function local to another function's body, even when
|
||||
* the module's file declares one of the same name above the free function.
|
||||
*/
|
||||
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-module-path-'));
|
||||
const files: Record<string, string> = {
|
||||
'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n',
|
||||
'src/lib.rs': `mod util;
|
||||
mod only;
|
||||
mod a;
|
||||
mod caller;
|
||||
mod importer;
|
||||
mod handle;
|
||||
mod worker;
|
||||
mod twice;
|
||||
mod inline_reexport;
|
||||
mod fs;
|
||||
mod uses_std;
|
||||
|
||||
pub fn via_self() -> usize {
|
||||
self::util::take(1)
|
||||
}
|
||||
`,
|
||||
// A namesake in an inline module, before the module's own item.
|
||||
'src/twice.rs': `#[cfg(test)]
|
||||
mod tests {
|
||||
pub fn pick() -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pick() -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
pub fn via_twice() -> usize {
|
||||
crate::twice::pick()
|
||||
}
|
||||
`,
|
||||
// The item lives in an inline module the file passes on with a glob.
|
||||
'src/inline_reexport.rs': `#[cfg(unix)]
|
||||
mod imp {
|
||||
pub fn lone() {}
|
||||
}
|
||||
pub use imp::*;
|
||||
|
||||
pub fn via_inline() {
|
||||
crate::inline_reexport::lone();
|
||||
}
|
||||
`,
|
||||
// A project module named like a module of the standard library.
|
||||
'src/fs/mod.rs': 'pub fn read(path: &str) -> usize {\n path.len()\n}\n',
|
||||
'src/uses_std.rs': `use std::fs;
|
||||
|
||||
pub fn loads() {
|
||||
let _ = fs::read("config.toml");
|
||||
}
|
||||
`,
|
||||
'src/util.rs': `pub struct Buf;
|
||||
|
||||
impl Buf {
|
||||
pub fn take(&self, n: usize) -> usize {
|
||||
n
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Take {
|
||||
fn take(&self) -> usize;
|
||||
}
|
||||
|
||||
fn outer() -> usize {
|
||||
fn take() -> usize {
|
||||
0
|
||||
}
|
||||
take()
|
||||
}
|
||||
|
||||
pub fn take(n: usize) -> usize {
|
||||
n
|
||||
}
|
||||
`,
|
||||
'src/only.rs': `pub struct Only;
|
||||
|
||||
impl Only {
|
||||
pub fn grab(&self) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/a/mod.rs': 'pub mod b;\n',
|
||||
'src/a/b.rs': `pub struct Deep;
|
||||
|
||||
impl Deep {
|
||||
pub fn get(&self) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get() -> usize {
|
||||
1
|
||||
}
|
||||
`,
|
||||
'src/importer.rs': 'use crate::util::take;\n',
|
||||
'src/handle.rs': 'pub struct Handle;\n',
|
||||
'src/worker.rs': `pub use crate::handle::Handle;
|
||||
mod dump;
|
||||
|
||||
pub trait Lock {
|
||||
type Handle;
|
||||
}
|
||||
|
||||
pub struct Guard;
|
||||
|
||||
impl Lock for Guard {
|
||||
type Handle = u8;
|
||||
}
|
||||
`,
|
||||
'src/worker/dump.rs': 'use super::Handle;\n',
|
||||
'src/caller.rs': `use crate::util;
|
||||
|
||||
pub fn via_crate() -> usize {
|
||||
crate::util::take(1)
|
||||
}
|
||||
|
||||
pub fn via_super() -> usize {
|
||||
super::util::take(1)
|
||||
}
|
||||
|
||||
pub fn via_module() -> usize {
|
||||
util::take(1)
|
||||
}
|
||||
|
||||
pub fn via_nested() -> usize {
|
||||
crate::a::b::get()
|
||||
}
|
||||
|
||||
pub fn via_method_only() -> usize {
|
||||
crate::only::grab()
|
||||
}
|
||||
|
||||
pub fn via_missing() -> usize {
|
||||
crate::nowhere::take(1)
|
||||
}
|
||||
`,
|
||||
};
|
||||
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 });
|
||||
});
|
||||
|
||||
/** What the function `name` in `file` calls, as `kind qualifiedName file:line`. */
|
||||
const callsFrom = (file: string, name: string): string[] => {
|
||||
const fn = cg.getNodesInFile(file).find((n) => n.name === name)!;
|
||||
return cg.getOutgoingEdgesFrom([fn.id]).filter((e) => e.kind === 'calls')
|
||||
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
|
||||
};
|
||||
|
||||
describe('Rust calls through a module path', () => {
|
||||
it('reach the free function the module declares, never a same-named method or local fn', () => {
|
||||
const take = 'function take src/util.rs:20';
|
||||
expect(callsFrom('src/caller.rs', 'via_crate')).toEqual([take]);
|
||||
expect(callsFrom('src/caller.rs', 'via_super')).toEqual([take]);
|
||||
expect(callsFrom('src/caller.rs', 'via_module')).toEqual([take]);
|
||||
expect(callsFrom('src/lib.rs', 'via_self')).toEqual([take]);
|
||||
});
|
||||
|
||||
it('bind a use of the function to it', () => {
|
||||
const ids = cg.getNodesInFile('src/importer.rs').map((n) => n.id);
|
||||
const targets = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'imports')
|
||||
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
|
||||
expect(targets).toEqual(['function take src/util.rs:20']);
|
||||
});
|
||||
|
||||
it('never bind a use to an associated type of the module\'s impl or trait', () => {
|
||||
const ids = cg.getNodesInFile('src/worker/dump.rs').map((n) => n.id);
|
||||
const targets = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'imports')
|
||||
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
|
||||
expect(targets.filter((t) => t.includes('src/worker.rs'))).toEqual([]);
|
||||
});
|
||||
|
||||
it('walk a nested module path to its file', () => {
|
||||
expect(callsFrom('src/caller.rs', 'via_nested')).toEqual(['function get src/a/b.rs:9']);
|
||||
});
|
||||
|
||||
it('link nothing when the module declares no such free function, or is not in the index', () => {
|
||||
expect(callsFrom('src/caller.rs', 'via_method_only')).toEqual([]);
|
||||
expect(callsFrom('src/caller.rs', 'via_missing')).toEqual([]);
|
||||
});
|
||||
|
||||
it('prefer the module\'s own item to a namesake in a module it declares inline', () => {
|
||||
expect(callsFrom('src/twice.rs', 'via_twice')).toEqual(['function pick src/twice.rs:8']);
|
||||
});
|
||||
|
||||
it('reach an inline module\'s item the module passes on, from the module\'s own file', () => {
|
||||
expect(callsFrom('src/inline_reexport.rs', 'via_inline')).toEqual(['function lone src/inline_reexport.rs:3']);
|
||||
});
|
||||
|
||||
it('leave a path through a module a `use` takes from the standard library to it', () => {
|
||||
// `fs::read` under `use std::fs;` is std's, not `src/fs/mod.rs`'s `read`.
|
||||
expect(callsFrom('src/uses_std.rs', 'loads')).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
isPythonFromImportModule,
|
||||
isRustAssociatedItem,
|
||||
tsGlobalScope,
|
||||
isLexicallyReachable,
|
||||
rustItemPlacement,
|
||||
} from './name-matcher';
|
||||
import { JS_ARRAY_METHODS } from './js-builtins';
|
||||
|
||||
@@ -3694,11 +3696,18 @@ function resolveRustPathReference(
|
||||
const modSegs = segments.slice(0, -1);
|
||||
|
||||
const file = resolveRustModuleFile(modSegs, ref.filePath, context);
|
||||
if (!file || file === ref.filePath) return null;
|
||||
if (!file) return null;
|
||||
|
||||
// A module's item, never an impl's associated one: serde's `use
|
||||
// crate::de::Error` is not `de/mod.rs`'s `impl SeqAccess`'s `type Error`.
|
||||
const target = context.getNodesInFile(file).find(
|
||||
// The module's own item. A path never names a method (that is
|
||||
// `Type::method`), nor a fn local to another fn's body: the file's first
|
||||
// same-named node was taken, so an `impl Buf { fn take }` above
|
||||
// `pub fn take` caught every `crate::util::take(…)`. Two module-level items
|
||||
// of one name are `#[cfg]` variants of it; the first one stands for both.
|
||||
// With none, the path reaches an inline module's item through a re-export
|
||||
// (`#[cfg(…)] mod imp { … }` + `pub use imp::*;`), never an associated item
|
||||
// of an `impl` or `trait` body: serde's `use crate::de::Error` is not
|
||||
// `de/mod.rs`'s `impl SeqAccess`'s `type Error`.
|
||||
const candidates = context.getNodesInFile(file).filter(
|
||||
(n) =>
|
||||
n.name === leaf &&
|
||||
!isRustAssociatedItem(n) &&
|
||||
@@ -3709,14 +3718,22 @@ function resolveRustPathReference(
|
||||
n.kind === 'trait' ||
|
||||
n.kind === 'type_alias' ||
|
||||
n.kind === 'constant' ||
|
||||
n.kind === 'method' ||
|
||||
n.kind === 'class' ||
|
||||
n.kind === 'interface')
|
||||
n.kind === 'interface') &&
|
||||
isLexicallyReachable(n, ref, context)
|
||||
);
|
||||
if (target) {
|
||||
return { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'import' };
|
||||
}
|
||||
return null;
|
||||
const target =
|
||||
candidates.find((n) => rustItemPlacement(n, context) === 'top') ??
|
||||
candidates.find((n) => rustItemPlacement(n, context) === 'inline');
|
||||
if (!target) return null;
|
||||
// A path whose head a `use` binds from the standard library — `fs::read`
|
||||
// under `use std::fs;`, `error::Error` under `use std::{error, io};` — is
|
||||
// std's, whatever the module the project has of that name: `fs/mod.rs`'s own
|
||||
// `read` is not `std::fs::read`. (Asked only here, where it changes the
|
||||
// answer: the check reads the file's `use`s.)
|
||||
const head = modSegs[0]!;
|
||||
if (!/^(?:crate|self|super)$/.test(head) && isBoundToOutOfRepoImport({ ...ref, referenceName: head }, context)) return null;
|
||||
return { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'import' };
|
||||
}
|
||||
|
||||
/** The crate-root directory (holds `lib.rs`/`main.rs`), walking up from a file. */
|
||||
|
||||
@@ -8994,6 +8994,21 @@ export function isRustAssociatedItem(n: Node): boolean {
|
||||
return n.language === 'rust' && RUST_ASSOCIATED_KINDS.has(n.kind) && n.qualifiedName.includes('::');
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a Rust item sits in its file's module, as a path through the module
|
||||
* reaches it: written at the file's top level ('top'), or in a module the
|
||||
* file declares inline (`mod imp { … }`, 'inline'), which adds nothing to its
|
||||
* qualified name. Null for an item a module path never names: a method, an
|
||||
* `impl`'s or a trait's associated item, an item of a function body — each
|
||||
* qualified by its owner.
|
||||
*/
|
||||
export function rustItemPlacement(n: Node, context: ResolutionContext): 'top' | 'inline' | null {
|
||||
if (n.language !== 'rust' || n.qualifiedName !== n.name) return null;
|
||||
const layout = rustUsesOf(n.filePath, context);
|
||||
const at = (layout.lineStarts[n.startLine - 1] ?? 0) + Math.max(0, n.startColumn ?? 0);
|
||||
return rustBlockAt(layout.blocks, at) === null ? 'top' : 'inline';
|
||||
}
|
||||
|
||||
/** Does one of the file's globs bring in this candidate's module (or, for `use super::*`, its parent's)? */
|
||||
function rustGlobCovers(uses: RustUses, candidate: Node, ref: UnresolvedRef): boolean {
|
||||
if (uses.globs.has(rustModuleName(candidate.filePath))) return true;
|
||||
|
||||
Reference in New Issue
Block a user