Files
codegraph/__tests__/rust-foreign-impl-calls.test.ts
T
Colby MchenryandClaude Opus 5.5 60b2f1e7bb fix(rust): a call reaches a project impl on a std type only as Rust would pick it (#2697)
A crate can't declare a method on String, Vec, Arc or NonNull, only
implement a trait for one, and name matching took those impls for any
call of that name. Every String::from("...") in dioxus's CLI went to its
impl From<IDI> for String, tokio's NonNull::from(waiter) calls to its
impl From<&Handle> for NonNull<Entry>, its tests' files.iter() to an
internal Storage trait's impl for Vec, and clap's String::from calls
to its impl From<Str> for OsString.

A call into a method of a project impl on a std type now stands only
when Rust could pick it. A std trait (From, PartialEq, ...) is
implemented for the crate's own type argument, so the call must pass
one: built from it (IDI::Install), self in an impl of it, or a name
the enclosing function declares with it (fn from(name: Str)). A trait
of the project's must be in scope in the calling file: declared,
implemented or imported there, or brought in by a glob that may carry
it. Otherwise the call is left unresolved, since what it calls is the
standard library's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 01:37:17 +00:00

141 lines
4.4 KiB
TypeScript

/**
* A crate can't declare a method on `String`, `Vec`, `Arc` or `NonNull`, only
* implement a trait for one. A call reaches such an impl only as Rust picks it:
* - a std trait the crate implements for one of its own types (`impl
* From<IDI> for String`) is what `String::from(x)` calls only when `x` is an
* `IDI`. dioxus's CLI's `String::from("…")` calls all went to that impl,
* tokio's `NonNull::from(waiter)` to its `impl From<&Handle> for NonNull<Entry>`;
* - a trait of the crate's own (`impl Storage for Vec<Slot>`) is reached only
* where it is in scope: tokio's tests' `files.iter()` on a `Vec` went to its
* `Storage` impl, in files that never import `Storage`.
*/
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-foreign-impl-'));
const files: Record<string, string> = {
'Cargo.toml': '[package]\nname = "cli"\nversion = "0.1.0"\n',
'src/main.rs': 'mod cli;\nmod names;\nmod slots;\nmod store;\nmod windows;\n\nfn main() {}\n',
'src/windows.rs': [
'pub enum Idi {',
' Install,',
'}',
'',
'impl From<Idi> for String {',
' fn from(_e: Idi) -> Self {',
' String::new()',
' }',
'}',
'',
'pub fn label() -> String {',
' String::from(Idi::Install)',
'}',
'',
].join('\n'),
'src/names.rs': [
'use std::ffi::OsString;',
'',
'pub struct Str;',
'',
'impl From<Str> for OsString {',
' fn from(_name: Str) -> Self {',
' OsString::new()',
' }',
'}',
'',
'pub fn os_name(name: Str) -> OsString {',
' OsString::from(name)',
'}',
'',
].join('\n'),
'src/cli.rs': [
'use std::ffi::OsString;',
'',
'pub fn banner() -> String {',
' String::from("dx")',
'}',
'',
'pub fn arg() -> OsString {',
' OsString::from("--release")',
'}',
'',
].join('\n'),
'src/store.rs': [
'pub trait Storage {',
' fn iter(&self) -> usize;',
'}',
'',
'pub struct Slot;',
'',
'impl Storage for Vec<Slot> {',
' fn iter(&self) -> usize {',
' self.len()',
' }',
'}',
'',
].join('\n'),
// Storage in scope: its `iter`.
'src/slots.rs': [
'use crate::store::{Slot, Storage};',
'',
'pub fn count(slots: Vec<Slot>) -> usize {',
' slots.iter()',
'}',
'',
].join('\n'),
// No Storage here: a Vec's own `iter`.
'tests/files.rs': [
'pub fn paths(files: Vec<String>) -> usize {',
' files.iter().count()',
'}',
'',
].join('\n'),
};
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 });
}, 60_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/** What `name`, called in `file`, links to, as `file@line` of each target, sorted. */
const callsOf = (file: string, name: string): string[] => {
const out: string[] = [];
for (const node of cg.getNodesInFile(file)) {
for (const edge of cg.getOutgoingEdges(node.id)) {
if (edge.kind !== 'calls') continue;
const target = cg.getNode(edge.target)!;
if (target.name === name) out.push(`${target.filePath}@${target.startLine}`);
}
}
return [...new Set(out)].sort();
};
describe('a Rust call into a project impl on a std type', () => {
it('is not a std trait impl unless its argument is the impl\'s own type', () => {
expect(callsOf('src/cli.rs', 'from')).toEqual([]);
});
it('reaches it when the argument is that type: built from it, or declared with it', () => {
expect(callsOf('src/windows.rs', 'from')).toEqual(['src/windows.rs@6']);
expect(callsOf('src/names.rs', 'from')).toEqual(['src/names.rs@6']);
});
it('reaches a project trait\'s impl only where the trait is in scope', () => {
expect(callsOf('src/slots.rs', 'iter')).toEqual(['src/store.rs@8']);
expect(callsOf('tests/files.rs', 'iter')).toEqual([]);
});
});