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 @@ -211,6 +211,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 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 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.
- In C++, every name a declaration lists now has the declaration's type, like `strings_found` in google/breakpad's `ByteBuffer symbols_found, strings_found;` or `data_` in a class's `Table *index_, *data_;`, so calls on it link to that class's methods. Before, only the first name had its type read, and a call on another name linked to nothing or to the class of an earlier variable with the same name.
Expand Down
209 changes: 209 additions & 0 deletions __tests__/rust-self-path-calls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/**
* A Rust `Self::x(…)` call is `x` of the type `Self` is where it is written:
* `Self::new(…)` in `impl Error` is `Error::new`, `Self::Text(…)` in `impl
* Message` its variant, and in a trait's own body the trait's method. The
* receiver names no type of its own, so a guess by the name alone took such a
* call only when nothing else shared the name: clap's 118 `Self::new(…)`-style
* calls stayed unresolved, as did dioxus's `Self::current()` in `impl
* Workspace` once its Android tooling's `AndroidTools::current` was indexed.
*/
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-self-path-'));
const files: Record<string, string> = {
'Cargo.toml': '[package]\nname = "cli"\nversion = "0.1.0"\n',
'src/main.rs': 'mod android;\nmod bound;\nmod config;\nmod error;\nmod frame;\nmod listener;\nmod message;\nmod parse;\nmod parser;\nmod store;\nmod workspace;\n\nfn main() {}\n',
'src/error.rs': [
'pub struct Error {',
' kind: u8,',
'}',
'',
'impl Error {',
' pub fn new(kind: u8) -> Self {',
' Error { kind }',
' }',
'',
' pub fn io() -> Self {',
' Self::new(1)',
' }',
'}',
'',
].join('\n'),
'src/config.rs': [
'use crate::store::Store;',
'',
'pub struct Config;',
'',
'impl Config {',
' pub fn new() -> Self {',
' Config',
' }',
'}',
'',
'impl Store for Config {',
' fn open() -> Self {',
' Self::new()',
' }',
'}',
'',
].join('\n'),
'src/store.rs': [
'pub trait Store {',
' fn open() -> Self;',
'',
' fn open_default() -> Self',
' where',
' Self: Sized,',
' {',
' Self::open()',
' }',
'}',
'',
'pub fn open() {}',
'',
].join('\n'),
'src/message.rs': [
'pub enum Message {',
' Text(String),',
' Binary(Vec<u8>),',
'}',
'',
'impl Message {',
' pub fn text(s: &str) -> Self {',
' Self::Text(s.to_string())',
' }',
'}',
'',
].join('\n'),
'src/frame.rs': 'pub enum Frame {\n Text(String),\n}\n',
'src/workspace.rs': [
'pub struct Workspace;',
'',
'impl Workspace {',
' pub fn current() -> Workspace {',
' Workspace',
' }',
'',
' pub fn reload() -> Workspace {',
' Self::current()',
' }',
'',
' pub fn depth(n: u32) -> u32 {',
' if n == 0 { 0 } else { Self::depth(n - 1) + 1 }',
' }',
'}',
'',
].join('\n'),
'src/android.rs': 'pub struct AndroidTools;\n\nimpl AndroidTools {\n pub fn current() -> Option<AndroidTools> {\n None\n }\n}\n',
// `Self` is `Box<T>`: `Self::new` is `Box::new`, from outside the project,
// not the `new` of a struct that shares the trait's name.
'src/parse.rs': [
'pub trait Parser {',
' fn parse() -> Self;',
'}',
'',
'impl<T: Parser> Parser for Box<T> {',
' fn parse() -> Self {',
' Self::new(T::parse())',
' }',
'}',
'',
].join('\n'),
'src/parser.rs': 'pub struct Parser;\n\nimpl Parser {\n pub fn new() -> Self {\n Parser\n }\n}\n',
// An outside type's own `local_addr` is std's: nothing the index holds.
'src/bound.rs': [
'use std::net::{SocketAddr, TcpListener};',
'',
'pub trait Bound {',
' fn local_addr(&self) -> std::io::Result<SocketAddr>;',
'}',
'',
'impl Bound for TcpListener {',
' fn local_addr(&self) -> std::io::Result<SocketAddr> {',
' Self::local_addr(self)',
' }',
'}',
'',
].join('\n'),
// A trait impl's method handing on to the type's own function of its name.
'src/listener.rs': [
'pub trait Listener {',
' fn local_addr(&self) -> u16;',
'}',
'',
'pub struct TcpListener;',
'',
'impl TcpListener {',
' pub fn local_addr(&self) -> u16 {',
' 0',
' }',
'}',
'',
'impl Listener for TcpListener {',
' fn local_addr(&self) -> u16 {',
' Self::local_addr(self)',
' }',
'}',
'',
].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 the `calls` edges out of `fn`, declared in `file` (at `line`, if given), reach, as `file@line`, sorted. */
const callsFrom = (file: string, fn: string, line?: number): string[] => {
const out: string[] = [];
for (const node of cg.getNodesInFile(file).filter((n) => n.name === fn && (line === undefined || n.startLine === line))) {
for (const edge of cg.getOutgoingEdges(node.id)) {
if (edge.kind !== 'calls') continue;
const target = cg.getNode(edge.target)!;
out.push(`${target.filePath}@${target.startLine}`);
}
}
return [...new Set(out)].sort();
};

describe('a Rust `Self::x(…)` call', () => {
it('is the impl\'s own type\'s function, whatever else shares its name', () => {
expect(callsFrom('src/error.rs', 'io')).toEqual(['src/error.rs@6']);
expect(callsFrom('src/workspace.rs', 'reload')).toEqual(['src/workspace.rs@4']);
// A recursion, in the type's own impl.
expect(callsFrom('src/workspace.rs', 'depth')).toEqual(['src/workspace.rs@12']);
});

it('is its variant, for a tuple variant built through `Self`', () => {
expect(callsFrom('src/message.rs', 'text')).toEqual(['src/message.rs@2']);
});

it('is the type\'s own in a trait impl, and the trait\'s in the trait\'s own body', () => {
expect(callsFrom('src/config.rs', 'open')).toEqual(['src/config.rs@6']);
expect(callsFrom('src/store.rs', 'open_default')).toEqual(['src/store.rs@2']);
});

it('is never another type\'s that shares the trait\'s name', () => {
expect(callsFrom('src/parse.rs', 'parse').filter((t) => t.startsWith('src/parser.rs'))).toEqual([]);
});

it('is the type\'s own function first, never the trait method it is written in', () => {
// The trait impl's `local_addr` (line 14) calls the type's own, at line 8.
expect(callsFrom('src/listener.rs', 'local_addr', 14)).toEqual(['src/listener.rs@8']);
expect(callsFrom('src/bound.rs', 'local_addr', 8)).toEqual([]);
});
});
110 changes: 110 additions & 0 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8076,6 +8076,110 @@ function rustEnclosingImplLine(ref: UnresolvedRef, context: ResolutionContext):
return 0;
}

interface RustSelfType {
/** Its name (`TcpListener`), or the trait's in a trait's own body. */
type: string;
/** The first segment of the path it is written with (`tokio` of `tokio::net::TcpListener`), if any. */
root: string | null;
/** The trait a trait impl implements. */
trait: string | null;
/** Inside a trait's own body. */
isTrait: boolean;
}

/** The type `Self` is where `ref` is written: its `impl`'s implementing type (and trait), or the trait it sits in. */
function rustSelfType(ref: UnresolvedRef, context: ResolutionContext): RustSelfType | null {
const header = rustEnclosingImplLine(ref, context);
if (header === 0) return null;
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [];
let text = (lines[header - 1] ?? '').replace(/\/\/.*$/, '');
for (let j = header; !text.includes('{') && j < Math.min(lines.length, header + 8); j++) text += ` ${(lines[j] ?? '').replace(/\/\/.*$/, '')}`;
const trait = /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?trait\s+([A-Za-z_]\w*)/.exec(text)?.[1];
if (trait) return { type: trait, root: null, trait: null, isTrait: true };
const impl = parseRustImplHeader(text);
// The type as written: `tokio::net::TcpListener` after a trait impl's `for`,
// or right after an inherent impl's `impl<T: Into<U>>`.
const written = (impl
? /\sfor\s+(?:&\s*(?:'\w+\s+)?(?:mut\s+)?)?((?:[A-Za-z_]\w*\s*::\s*)*[A-Za-z_]\w*)/.exec(text)
: /\bimpl\b\s*(?:<(?:[^<>]|<[^<>]*>)*>)?\s*((?:[A-Za-z_]\w*\s*::\s*)*[A-Za-z_]\w*)/.exec(text))?.[1]?.split('::').map((s) => s.trim()) ?? [];
const type = impl ? impl.self : written[written.length - 1];
if (!type) return null;
return { type, root: written.length > 1 ? written[0]! : null, trait: impl?.trait || null, isTrait: false };
}

/**
* Whether `Self`'s type comes from outside the project: written through std's
* or a dependency's path, brought in by an outside `use`, or a std type the
* project doesn't declare. Its own functions are then nothing the index holds.
*/
function isRustOutsideSelf(self: RustSelfType, ref: UnresolvedRef, context: ResolutionContext): boolean {
if (self.root !== null) return self.root === 'std' || self.root === 'core' || self.root === 'alloc' || rustDependencyCrates(context).has(self.root);
const uses = rustUsesOf(ref.filePath, context);
if (uses.external.has(self.type) && !uses.bound.has(self.type)) return true;
return RUST_STD_TYPES.has(self.type) && !isRustProjectNamed(self.type, RUST_DECLARED_TYPE_KINDS, context);
}

const RUST_SELF_PATH_KINDS = new Set(['method', 'function', 'enum_member']);

/**
* A Rust `Self::x(…)` call is `x` of the type `Self` is where it is written:
* `Self::new(…)` in `impl Error` is `Error::new`, `Self::Text(…)` in `impl
* Message` its variant, and in a trait impl or a trait's own body the trait's
* method when the type has none. The receiver names no type of its own, so a
* guess by the name alone took such a call only when nothing else shared the
* name: clap's 118 `Self::new(…)`-style calls stayed unresolved, tokio's 56.
* The type's own file first — where its impls are — then the nearest. Null
* when the type has no such function the index holds — the call is then no
* other type's — and undefined when there is no `impl` or `trait` around it.
*/
function matchRustSelfPath(name: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined {
const self = rustSelfType(ref, context);
if (!self) return undefined;
// An outside type (axum's `impl Listener for tokio::net::TcpListener`) has
// only the project's impls on it, written beside the call: another type of
// its name elsewhere is not it.
const outside = !self.isTrait && isRustOutsideSelf(self, ref, context);
// A trait's method is one its own body declares: clap's `impl<T: Parser>
// Parser for Box<T>` calls `Self::new`, which is `Box::new`, not the
// `new` of a struct also named `Parser`.
const inTrait = (n: Node, trait: string): boolean => (context.getNodesInFileNamed?.(n.filePath, trait) ?? context.getNodesInFile(n.filePath))
.some((t) => t.kind === 'trait' && t.name === trait && t.startLine <= n.startLine && (t.endLine ?? 0) >= n.startLine);
const pick = (owner: string, trait = false): Node | null => {
const all = context.getNodesByName(name).filter((n) => n.language === 'rust' && RUST_SELF_PATH_KINDS.has(n.kind) &&
(n.qualifiedName === `${owner}::${name}` || n.qualifiedName.endsWith(`::${owner}::${name}`)) && (!trait || inTrait(n, owner)) &&
(trait || !outside || n.filePath === ref.filePath) &&
// `fn local_addr(&self) { Self::local_addr(self) }` in a trait impl hands
// the call on to the type's own function of that name, never to itself;
// in the type's own impl it is a recursion (gleam's `resolve_type_var_id`).
(self.trait === null || n.id !== ref.fromNodeId));
// Rust looks up the type's own (inherent) function first, `#[cfg]` twins
// of it standing for one another; a trait impl's only when there is none,
// and several trait impls of one name (`From<A>`, `From<B>`) can't be told apart.
const inherent = all.filter((n) => !isRustTraitImplMethod(n, context));
const candidates = inherent.length > 0 ? inherent : all.length === 1 ? all : [];
if (candidates.length === 0) return null;
const own = candidates.find((n) => n.filePath === ref.filePath);
if (own) return own;
let best = candidates[0]!;
let score = computePathProximity(ref.filePath, best.filePath);
for (const c of candidates.slice(1)) {
const s = computePathProximity(ref.filePath, c.filePath);
if (s > score) {
score = s;
best = c;
}
}
return best;
};
// The trait's own method only where nothing could shadow it: an outside
// type may have an inherent one of the name, and `fn local_addr(&self) {
// Self::local_addr(self) }` hands the call on to the type's own.
const delegates = context.getNodeById?.(ref.fromNodeId)?.name === name;
const target = pick(self.type, self.isTrait) ?? (self.trait && !outside && !delegates ? pick(self.trait, true) : null);
if (!target) return null;
return { original: ref, targetNodeId: target.id, confidence: target.filePath === ref.filePath ? 0.9 : 0.8, resolvedBy: 'qualified-name' };
}

/** Whether `candidate` is declared in the same `impl` / `trait` block as `ref`, above it. */
function isInEnclosingRustImpl(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
const header = rustEnclosingImplLine(ref, context);
Expand Down Expand Up @@ -15450,6 +15554,12 @@ export function matchMethodCall(
if (literalMatch) return literalMatch;
}

// A Rust `Self::x(…)`: `x` of the impl's own type (matchRustSelfPath).
if (ref.language === 'rust' && match === colonMatch && objectOrClass === 'Self') {
const own = matchRustSelfPath(methodName!, ref, context);
if (own !== undefined) return own;
}

// Strategy 1: Direct class name match (existing logic). When the receiver
// names a class that exists in several files (`Logger.log()` / `Logger::log()`
// with a `Logger` in both `a/` and `b/`), try the class in the call site's
Expand Down