From f5d858835f91467240c65ecb17d52b29c27a1e46 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Thu, 1 Oct 2026 19:25:28 +0300 Subject: [PATCH] fix(rust): a module path names the module's free function, not a method A 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;`) maps the module prefix to its file and then took the first node of that name in it - `method` was among the accepted kinds, and nothing ruled out a fn local to another fn's body. So an `impl Buf { pub fn take }` above `pub fn take` caught every path call, and a module with only the method bound the call to it. A Rust path never names a method (that takes `Type::method`) nor a function-local item: drop `method` from the kinds and keep only lexically reachable items (the same check #2224 applies to the framework heuristics). A module whose file declares no such item still falls through, and links nothing. Side effect: `use crate::util::take;` of such a function now binds to it; it used to land on the method and be dropped, leaving no import edge. A/B on the user's Rust projects (copies, calls edges main -> fix): gnss_rust 13 calls retargeted from `GnssTimeConverter::utc_to_gps_time` (and 6 siblings) to the module's free wrappers of the same name, +7 `use crate::gnsstime::{...}` import edges; every call site is a `crate::gnsstime::(...)` path - 13/13, 7/7 correct. usb-doctor, emmc-reader-gui, skylab_gui, rp2350-dongle-emu, ld2416: no change. Node counts unchanged. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/rust-module-path-call.test.ts | 147 ++++++++++++++++++++++++ src/resolution/import-resolver.ts | 9 +- 3 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 __tests__/rust-module-path-call.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eaa9f0bee..05f274d2e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- In Rust, a call written through a module path, like `crate::util::take(…)`, `super::util::take(…)` or `util::take(…)`, now links to the free function that module declares, and a `use crate::util::take;` now links to it too. Before, a same-named method in an `impl` block above the function, or a function nested inside another one in that file, took the call, and the `use` often linked to nothing. - A Python module member used through the imported module, like `generic.ObjectEditView` after `from netbox.views import generic`, `serializers.ValidationError` or `utils.assert_deserialize(…)`, now also follows the package's re-exports to its definition. Before, netbox's view classes lost their `generic.*` base classes, and mealie's test helpers called through `utils.` went unresolved. - A Python name imported from a package now follows the package's re-exports to where it's defined. That covers both `from .users import *` and `from .tokens import Token` in its `__init__.py`. Before, httpx's own tests' `httpx.Client(…)` and `httpx.URL(…)`, pytest's `pytest.raises(…)` and `pytest.param(…)`, and netbox's `from users.models import User` went unresolved or to a same-named class elsewhere, such as a test's `User` or an Alembic migration's copy of a model. - In NestJS projects, the provider naming convention (`…Service`, `…Controller`, …) now picks a class only from the reference's own file. Another file's provider is reached through its import, or by ordinary name matching that prefers the nearest declaration. Before, the convention's preference for `*.service.ts` files sent Nest's own specs' `TransientService` and `TestService` to same-named classes in unrelated sample apps. diff --git a/__tests__/rust-module-path-call.test.ts b/__tests__/rust-module-path-call.test.ts new file mode 100644 index 0000000000..7f61bd92bb --- /dev/null +++ b/__tests__/rust-module-path-call.test.ts @@ -0,0 +1,147 @@ +/** + * 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 = { + 'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n', + 'src/lib.rs': `mod util; +mod only; +mod a; +mod caller; +mod importer; + +pub fn via_self() -> usize { + self::util::take(1) +} +`, + '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/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('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([]); + }); +}); diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index d1ca2c1fe0..71726d75eb 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -17,6 +17,7 @@ import { resolveObjectLiteralBinding, localReceiverTypePatterns, normalizeInferredTypeName, + isLexicallyReachable, } from './name-matcher'; /** @@ -2186,6 +2187,10 @@ function resolveRustPathReference( const file = resolveRustModuleFile(modSegs, ref.filePath, context); if (!file || file === ref.filePath) return null; + // 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(…)`. const target = context.getNodesInFile(file).find( (n) => n.name === leaf && @@ -2196,9 +2201,9 @@ 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' };