Skip to content
Merged
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 @@ -210,6 +210,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In JavaScript and TypeScript, a barrel's `export { A, B as C } from './x'` now links each name it passes on to what `./x` exports, following barrels that pass it on in turn. Before, each name was looked up by itself and could link to a same-named symbol elsewhere in the project, such as another folder's `Provider`, and a name passed on from a package, like `export { debounce } from 'lodash'`, could link to a project function of that name. Re-index JavaScript and TypeScript projects after upgrading.
- In Swift, a call written after `!` or another prefix operator, or chained after `try` or `await`, now links to what it calls. `if !isReady()` links to `isReady`, `!store.contains(id)` and `try! JSONDecoder().decode(…)` link to the method of the type they're called on rather than to whichever `contains` or `decode` came first, and `!Settings.Defaults.isOn()` lands on the type it names. Re-index Swift projects after upgrading.
- In Swift, a call on a local created through `try`, `try!`, `try?` or `await`, or with a generic type's constructor, now links to that type's method: after `let storage = try! DiskStorage.Backend<String>(config: config)`, `storage.store(…)` links to `Backend`'s `store`. Before, such a local had no known type, so the call was left unlinked or matched by name to another type's method. Re-index Swift projects after upgrading.
- In Rust, a call like `String::from("…")`, `Vec::from([…])` or `NonNull::from(x)` no longer links to your project's own `impl From<YourType> for String` (or for `Vec`, `NonNull`, …) unless what it passes is a `YourType`. A method call on a standard-library value, like `files.iter()` or `buf.extend(…)`, links to your trait's implementation for that type only where the trait is imported. Before, every `String::from("…")` call in dioxus's CLI linked to one such conversion, and tokio's `vec.iter()` calls to an internal trait's `Vec` impl. Re-index Rust projects after upgrading.
- In Rust, a type alias, struct, enum or function declared without `pub` now links only from its own module and the modules inside it, and a name a `use` brings in from one of your workspace's crates, like `use tokio_test::task::spawn` or `use crate::error::Error`, links only to an item of that crate or of a crate it depends on. Before, a file's `use crate::Result` could link its `Result<…>` types to another module's private `type Result`, as in dioxus's CLI, and a call to an imported function could link to another crate's function of the same name, such as tokio's test `spawn(…)` calls to one in its examples, or to nothing because a private function of that name was in the way. Re-index Rust projects after upgrading.

## [1.6.2] - 2026-10-03
Expand Down
140 changes: 140 additions & 0 deletions __tests__/rust-foreign-impl-calls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* 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([]);
});
});
13 changes: 9 additions & 4 deletions src/resolution/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
isImportableKind,
CPP_DEFINE_SIGNATURE,
} from './types';
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, GO_TYPE_KINDS, matchGoAssertedCall, isGoAssertedLocal, isGoLocallyBound } from './name-matcher';
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, GO_TYPE_KINDS, matchGoAssertedCall, isGoAssertedLocal, isGoLocallyBound } from './name-matcher';
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes';
Expand Down Expand Up @@ -3441,12 +3441,17 @@ export class ReferenceResolver {
/**
* A bare Rust name reaches only what is in scope — every strategy's result,
* a framework resolver's `Ok(x)` → `struct Ok` construction included (see
* isRustNameInScope).
* isRustNameInScope). And a call, however written, reaches a project impl
* on a std type only as Rust would pick it (isRustForeignImplCallable).
*/
private gateRustScope(result: ResolvedRef | null, ref: UnresolvedRef): ResolvedRef | null {
if (!result || ref.language !== 'rust' || !/^[A-Za-z_]\w*$/.test(ref.referenceName)) return result;
if (!result || ref.language !== 'rust') return result;
const bare = /^[A-Za-z_]\w*$/.test(ref.referenceName);
if (!bare && ref.referenceKind !== 'calls') return result;
const target = this.nodeById(result.targetNodeId);
return target && !isRustNameInScope(target, ref, this.context) ? null : result;
if (!target) return result;
if (bare && !isRustNameInScope(target, ref, this.context)) return null;
return ref.referenceKind === 'calls' && !isRustForeignImplCallable(target, ref, this.context) ? null : result;
}

/** Is `filePath` (project-relative) one of the files the index holds? */
Expand Down
Loading