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 @@ -244,6 +244,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Rust, a method call on a local variable or a parameter, like `opts.build()`, `server.run()` or `serializer.serialize_str(…)`, now links to the method of the type the variable is declared with: its parameter type or `let` annotation, the value it is built from (`Server::new()`, `Opts { … }`, `Arc::new(…)`), or the field a match arm takes from a variant (`Commands::Build(opts) => opts.build()`). A call through `&`, `Box`, `Rc` or `Arc` reaches the value inside, a call on an `impl Trait`, a `dyn Trait` or a bounded generic reaches the trait's method, and among types that share a name, the one the file imports wins. A call on a standard-library or dependency value, like `cx.waker()` on a `Context` or `path.join(…)` on a `PathBuf`, no longer links to an unrelated project method of the same name. Before, such calls were linked by the method's name alone, so dioxus's `opts.build()` lost its link once another type had a `build`. Re-index Rust projects after upgrading. Thanks @Egrn for the report and @danusha2345. (#2614)
- In Rust, a variable that holds what a method call returns, like `let server = builder.port(80).build();`, `let settings = config.load()?;` or `let conn = client.connect().await;`, now has the type that method is declared to return, so calls on the variable link to that type's methods. A generic method's type comes from the call's turbofish, like `arg.get::<ValueHint>()`, and a call on a type that implements one trait twice, like `Encoder<Bytes>` and `Encoder<&[u8]>`, links to the implementation that takes the argument given. Before, such calls were linked by name and could reach another type's method of the same name. Re-index Rust projects after upgrading.
- In Rust, a `for` loop's variable now has the type of an item of what the loop goes over, like `for item in &self.items` over a `Vec<Item>`, `for (key, value) in &map` over a `HashMap`, or `for sc in cmd.get_subcommands()` over the `impl Iterator` a method returns, and each name in a tuple pattern like `let (tx, rx) = mpsc::channel(8);` has the type of its part of the tuple, so calls on them link to that type's methods. Before, such calls were linked by name: tokio's `tx.send(…)` calls on channel senders went to an internal type's `send`. Re-index Rust projects after upgrading.
- In Rust, a call on a field of `self`, like `self.store.save()` or `self.inner.lock()`, now links to the method of the type the struct declares for that field, also when the field's type is a parameter the `impl` bounds by a trait (`impl<S: Store> Worker<S>`), a trait object, or held in an `Arc` or a `Box`. A field whose type comes from the standard library or a dependency, like `jar: cookie::CookieJar` or `inner: std::sync::Mutex<…>`, no longer links to the project's own type of the same name, which often made a wrapper's method look like it called itself. 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.
- In C++, a call on a parameter, a local lambda or functor, a lambda's parameter or a range-`for` variable no longer links to a function or method elsewhere that only shares its name, like protocolbuffers/protobuf's code generators calling `format(…)` on their `Formatter& format` parameter, which linked to the conformance suite's `TestResult::format`, or facebook/rocksdb's tests calling a local `auto key = [](int i) {…};`. C++ finds the local first, so that is what the call calls.
- In TypeScript and JavaScript, an import of a package your project declares itself with `declare module '…'` in a `.d.ts` file, like a `typings/mammoth/index.d.ts` written for an untyped package or VS Code's own `vscode.d.ts`, now links to that declaration, so `codegraph callers`, impact and `codegraph affected` show the code that uses your typings instead of nothing or a project symbol that only shares a name. Named, default and namespace imports, `export =` and renaming export lists are read the way TypeScript reads them, a function such a file declares without a body (`export function convertToHtml(…): Promise<Result>;`) is now indexed so calls to it have something to link to, and only an import of its package reaches it. A `declare module` that augments a package's own types, a wildcard like `declare module '*.svg'`, and copies of typings kept in test fixtures are left alone. Re-index TypeScript projects after upgrading.
Expand Down
176 changes: 176 additions & 0 deletions __tests__/rust-self-field-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* A Rust call on a field of the impl's own type — `self.store.save()` — is a
* call on the type the struct declares for the field, read as a written
* type: the struct's own parameter by the bounds the impl gives it, a trait
* object by its trait, a pointer's own methods its own (`Arc::clone`), a
* type name by the `use` that brings it into the struct's file.
*/
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;

const config = [
'pub struct Config;',
'',
'impl Config {',
' pub fn load(&self) {}',
'}',
'',
].join('\n');

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-self-field-'));
const files: Record<string, string> = {
'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nsemverlib = "1"\n',
'src/main.rs': 'mod config_a;\nmod config_b;\nmod model;\nmod proc;\nmod range;\nmod use_range;\nmod ver;\nmod worker;\n\nfn main() {}\n',
'src/range.rs': '/// The project\'s own `Range`, a namesake of the dependency\'s.\npub struct Range;\n',
'src/ver.rs': [
'pub trait Lowest {',
' fn lowest(&self) -> u32;',
'}',
'',
'impl Lowest for semverlib::Range {',
' fn lowest(&self) -> u32 {',
' 0',
' }',
'}',
'',
].join('\n'),
'src/use_range.rs': [
'use semverlib::Range;',
'',
'use crate::ver::Lowest;',
'',
'pub fn check(r: Range) -> u32 {',
' r.lowest()',
'}',
'',
].join('\n'),
'src/proc.rs': [
'pub trait Kill {',
' fn kill(&mut self) -> bool;',
'}',
'',
'/// The project\'s own `Child`, holding std\'s.',
'pub struct Child {',
' child: std::process::Child,',
'}',
'',
'impl Kill for Child {',
' fn kill(&mut self) -> bool {',
' self.child.kill().is_ok()',
' }',
'}',
'',
].join('\n'),
'src/config_a.rs': config,
'src/config_b.rs': config,
'src/model.rs': [
'pub trait Store {',
' fn save(&self);',
'}',
'',
'pub trait Render {',
' fn render(&self);',
'}',
'',
'pub struct State;',
'',
'impl Clone for State {',
' fn clone(&self) -> Self {',
' State',
' }',
'}',
'',
'pub struct Inner;',
'',
'impl Inner {',
' pub fn run(&self) {}',
'}',
'',
'// Namesakes, which a guess by name alone may take.',
'pub struct Decoy;',
'',
'impl Decoy {',
' pub fn save(&self) {}',
' pub fn run(&self) {}',
'}',
'',
].join('\n'),
'src/worker.rs': [
'use std::sync::Arc;',
'',
'use crate::config_b::Config;',
'use crate::model::{Inner, Render, State, Store};',
'',
'pub struct Worker<S: Store> {',
' store: S,',
' shared: Arc<State>,',
' cfg: Config,',
' view: Box<dyn Render>,',
'}',
'',
'impl<S: Store> Worker<S> {',
' pub fn tick(&self) {',
' self.store.save();',
' let _ = self.shared.clone();',
' self.cfg.load();',
' self.view.render();',
' }',
'}',
'',
].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`, reach, as `file@line`, sorted. */
const callsFrom = (file: string, fn: string): string[] => {
const out: string[] = [];
for (const node of cg.getNodesInFile(file).filter((n) => n.name === fn)) {
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 call on a field of `self`', () => {
it('is the method of the type the struct declares for the field', () => {
// `S: Store` → `Store::save`; `Arc<State>`'s `clone` is the `Arc`'s;
// `Config` is the one `config_b` declares, which the file uses;
// `Box<dyn Render>` → `Render::render`.
expect(callsFrom('src/worker.rs', 'tick')).toEqual(['src/config_b.rs@4', 'src/model.rs@2', 'src/model.rs@6']);
});

it('is no project impl for a project type that only shares the std type\'s name', () => {
// `std::process::Child::kill` is std's; the project's `impl Kill for
// Child` is for its own `Child`, so the call is no self-loop. (The
// trait's `kill` declaration links to that impl on its own.)
const impl = cg.getNodesInFile('src/proc.rs').find((n) => n.name === 'kill' && n.startLine === 11)!;
expect(cg.getOutgoingEdges(impl.id).filter((e) => e.kind === 'calls')).toEqual([]);
});
});

describe('a Rust call on a dependency\'s type', () => {
it('is a project trait\'s impl for that type, read by the path the impl writes', () => {
// `impl Lowest for semverlib::Range`, though the project has a `Range` of its own.
expect(callsFrom('src/use_range.rs', 'check')).toEqual(['src/ver.rs@6']);
});
});
Loading