Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
147 changes: 147 additions & 0 deletions __tests__/rust-module-path-call.test.ts
Original file line number Diff line number Diff line change
@@ -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<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;

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([]);
});
});
9 changes: 7 additions & 2 deletions src/resolution/import-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
resolveObjectLiteralBinding,
localReceiverTypePatterns,
normalizeInferredTypeName,
isLexicallyReachable,
} from './name-matcher';

/**
Expand Down Expand Up @@ -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 &&
Expand All @@ -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' };
Expand Down