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 @@ -232,6 +232,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- 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 written `Self::new(…)`, `Self::from_std(…)` or `Self::Text(…)` now links to that function or variant of the type whose `impl` it is written in, even when other types have one of the same name. Before, such a call was linked only when nothing else in the project shared the name, so most `Self::new(…)` constructors and many helper calls were missing from callers and impact. Re-index Rust projects after upgrading.
- In Rust, a call through a generic type parameter, like `Key::get(…)` in protocolbuffers/protobuf's `fn map_get<Key: MapKey>`, now links to the method the trait its bound names declares, or a trait that trait extends: `trait MapKey: … + FfiMapKey` makes it `FfiMapKey::get`, whether the bound is written in `<…>`, in a `where` clause or on the `impl`. A parameter bounded only by traits from outside your project, like `T::default()` under `T: Default`, no longer links to one of your types' unrelated `default` functions, and two bound traits that both declare the method link neither. Re-index Rust 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 method called through `self: Pin<&mut Self>`, `self: Box<Self>`, `self: Rc<Self>` or `self: Arc<Self>`, or on a value declared `Pin<&mut …>`, now links the way Rust looks it up: to the type's own method, like `self.poll_flush(cx)` inside tokio's `impl AsyncWrite for File`, rather than to a catch-all `impl AsyncWrite for Pin<P>`, while `Pin`'s own methods, like `self.get_mut()`, stay unlinked. A type's own method now also comes before a trait's of the same name, so tokio's `Arc::from_raw(…)` calls are the standard library's rather than its internal `Link` trait's, and a method called on a tuple, like serde's `(self.ip(), self.port()).serialize(…)`, no longer links to an unrelated type's. Before, most calls made through such a `self` linked to nothing. Re-index Rust projects after upgrading.
- In Rust, a method called on what `.as_mut()` or `.as_ref()` hands back now links by the type its receiver is declared with: `self.delay.as_mut().reset(…)` on a `Pin<Box<Sleep>>` field reaches `Sleep::reset`, a field pin-project hands out reaches its own type's method, and a `Box` or `NonNull` lends what it holds. A method on an `Option` reached this way, like `self.child.as_mut().map(…)`, or on a generic type no longer links to an unrelated project method that shares its name. Before, these calls were matched by name alone, so tokio's `self.delay.as_mut().reset(…)` linked to the calling `Interval::reset`, and dioxus's `Option` calls to its own `map` and `filter` helpers. Re-index Rust projects after upgrading.
Expand Down
173 changes: 173 additions & 0 deletions __tests__/rust-generic-bound-paths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* A Rust call through a generic type parameter — `Key::get(…)` in
* protocolbuffers/protobuf's `fn map_get<Key: MapKey>` — is to the method the
* traits its bounds name declare, or the traits they extend: `trait MapKey:
* … + FfiMapKey` makes it `FfiMapKey::get`.
*
* Before, such a call linked only when a declaration's qualified name happened
* to end with the written text (`FfiMapKey::get` ends with `Key::get`), which
* #2723 stopped; a bound from outside the project (`T: Default`) let a guess by
* the method's name take a project type's unrelated `default`.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import type { Node } from '../src/types';

const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});

async function indexed(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-rust-bound-paths-'));
roots.push(root);
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);
}
return CodeGraph.init(root, { index: true });
}

function callable(cg: CodeGraph, qualifiedName: string): Node {
const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName);
if (!node) throw new Error(`no function or method ${qualifiedName}`);
return node;
}

/** `calls` callees of a function or method, as `qualifiedName`. */
function calls(cg: CodeGraph, caller: string): string[] {
return cg
.getCallees(callable(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => r.node.qualifiedName)
.sort();
}

const lines = (...l: string[]): string => [...l, ''].join('\n');

const CARGO = lines('[package]', 'name = "pb"', 'version = "0.1.0"', 'edition = "2021"');

/** protobuf's map key traits: the FFI methods live on a supertrait of the bound. */
const KEYS = lines(
'pub struct RawMap;',
'',
'pub trait FfiMapKey: Sized {',
' unsafe fn get(map: RawMap, key: Self, value: *mut u8) -> bool;',
' unsafe fn insert(map: RawMap, key: Self, value: u8) -> bool;',
'}',
'',
'pub trait MapKey: Clone + FfiMapKey {}',
);

describe('a Rust call through a generic parameter reaches its bound trait\'s method', () => {
it('reproduction: `Key::get(…)` in `fn map_get<Key: MapKey>` is FfiMapKey::get, through the supertrait', async () => {
const cg = await indexed({
'Cargo.toml': CARGO,
'src/lib.rs': lines('pub mod keys;', 'pub mod map;'),
'src/keys.rs': KEYS,
'src/map.rs': lines(
'use crate::keys::{MapKey, RawMap};',
'',
'pub struct Map;',
'',
'impl Map {',
' pub fn map_get<Key: MapKey>(key: Key) -> bool {',
' unsafe { Key::get(RawMap, key, std::ptr::null_mut()) }',
' }',
'}',
),
});
try {
expect(calls(cg, 'Map::map_get')).toEqual(['FfiMapKey::get']);
} finally {
cg.close();
}
});

it('a bound in a `where` clause, and one on the `impl`, count the same', async () => {
const cg = await indexed({
'Cargo.toml': CARGO,
'src/lib.rs': lines('pub mod keys;', 'pub mod map;'),
'src/keys.rs': KEYS,
'src/map.rs': lines(
'use crate::keys::{MapKey, RawMap};',
'',
'pub struct Map;',
'',
'impl Map {',
' pub fn map_insert<Key>(key: Key) -> bool',
' where',
' Key: MapKey,',
' {',
' unsafe { Key::insert(RawMap, key, 1) }',
' }',
'}',
'',
'pub struct Typed<K>(K);',
'',
'impl<K: MapKey> Typed<K> {',
' pub fn lookup(&self) -> bool {',
' unsafe { K::get(RawMap, self.0.clone(), std::ptr::null_mut()) }',
' }',
'}',
),
});
try {
expect(calls(cg, 'Map::map_insert')).toEqual(['FfiMapKey::insert']);
expect(calls(cg, 'Typed::lookup')).toEqual(['FfiMapKey::get']);
} finally {
cg.close();
}
});

it('a bound from outside the project, `T: Default`, links no project `default`', async () => {
const cg = await indexed({
'Cargo.toml': CARGO,
'src/lib.rs': lines(
'pub struct Defaults;',
'',
'impl Defaults {',
' pub fn default() -> Defaults {',
' Defaults',
' }',
'}',
'',
'pub fn make<T: Default>() -> T {',
' T::default()',
'}',
),
});
try {
expect(calls(cg, 'make')).toEqual([]);
} finally {
cg.close();
}
});

it('two bound traits that both declare the method link neither', async () => {
const cg = await indexed({
'Cargo.toml': CARGO,
'src/lib.rs': lines(
'pub trait Engine {',
' fn run() -> u8;',
'}',
'',
'pub trait Task {',
' fn run() -> u8;',
'}',
'',
'pub fn start<T: Engine + Task>() -> u8 {',
' T::run()',
'}',
),
});
try {
expect(calls(cg, 'start')).toEqual([]);
} finally {
cg.close();
}
});
});
34 changes: 34 additions & 0 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9243,6 +9243,35 @@ function matchRustSelfPath(name: string, ref: UnresolvedRef, context: Resolution
return { original: ref, targetNodeId: target.id, confidence: target.filePath === ref.filePath ? 0.9 : 0.8, resolvedBy: 'qualified-name' };
}

/**
* A Rust call through a generic type parameter of the function or `impl`
* around it — `Key::get(…)` in protobuf's `fn map_get<Key: MapKey>`, with
* `trait MapKey: FfiMapKey` — is to the method one of the traits its bounds
* name declares, or one of the traits they extend: `FfiMapKey::get`. Bounds
* only from outside the project (`T: Default`) name no project method, nor do
* two traits that both declare it: null. Undefined when `name` is no generic
* parameter there.
*/
function matchRustGenericPath(name: string, method: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined {
const scopes = rustScopesOf(ref.filePath, context);
if (!scopes) return undefined;
const written: RustWrittenType = { text: name, file: ref.filePath, at: (scopes.lineStarts[ref.line - 1] ?? 0) + Math.max(0, ref.column) };
const bounds = rustGenericBounds(name, written, context);
if (bounds === undefined) return undefined;
const traits = bounds.filter((b) => !/^['?]/.test(b.trim()) && !RUST_MARKER_TRAITS.has(rustPathHead(b)?.name ?? ''));
if (traits.length === 0) return null;
const result = rustTraitsMethodCall(traits, written, method, ref, context);
if (result === 'unread') return null;
// (The bound names the trait, so its method is reached as a typed call is:
// its missing `pub` doesn't hide it from another module.)
if (result) {
let typed = RUST_TYPED_REFS.get(context);
if (!typed) RUST_TYPED_REFS.set(context, (typed = new WeakSet()));
typed.add(ref);
}
return result;
}

/**
* Whether `candidate` is declared directly in an inline `mod <name> { … }` of
* its own file — the only way a path through `name` reaches it, since an
Expand Down Expand Up @@ -18745,6 +18774,11 @@ export function matchMethodCall(
const own = matchRustSelfPath(methodName!, ref, context);
if (own !== undefined) return own;
}
// A Rust `T::x(…)` through a generic parameter: the method its bounds' traits declare (matchRustGenericPath).
if (ref.language === 'rust' && match === colonMatch && /^[A-Z]\w*$/.test(objectOrClass!) && objectOrClass !== 'Self') {
const bounded = matchRustGenericPath(objectOrClass!, methodName!, ref, context);
if (bounded !== undefined) return bounded;
}

// Strategy 1: Direct class name match (existing logic). When the receiver
// names a class that exists in several files (`Logger.log()` / `Logger::log()`
Expand Down