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 @@ -246,6 +246,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- 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 Rust, a call written with a turbofish, like `matches.get_one::<String>("name")`, `parse_config::<Settings>()` or `Pool::<u8>::new()`, now links to the function it calls. Before, the type arguments stayed in the call's name, so no such call was linked. Re-index Rust projects after upgrading.
- In Rust, a call through a value's declared type, like `reader.as_mut().poll_read(cx)` on `reader: Pin<&mut R>`, now reads the declaration in scope at the call. Before, a `let` of the same name inside an earlier block, or a struct literal's field of that name written above the call, could be taken for it. 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
82 changes: 82 additions & 0 deletions __tests__/rust-declared-type-scope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* The type a Rust value is declared with — read for `reader.as_mut().m()` and
* calls through a `Pin` — is its binding's in scope at the call: a `let`
* in a block the call is outside of is not it, and neither is a struct
* literal's `reader: …` field. A backward line scan took the nearest line
* naming `reader:` above the call, whatever it was.
*/
import { describe, it, expect, beforeAll, afterAll } 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-declared-scope-'));
fs.mkdirSync(path.join(root, 'src'));
fs.writeFileSync(path.join(root, 'Cargo.toml'), '[package]\nname = "app"\nversion = "0.1.0"\n');
fs.writeFileSync(path.join(root, 'src', 'main.rs'), [
'use std::pin::Pin;',
'',
'pub struct Reader;',
'',
'impl Reader {',
' pub fn poll_read(self: Pin<&mut Self>) {}',
'}',
'',
'pub struct Other;',
'',
'impl Other {',
' pub fn poll_read(self: Pin<&mut Self>) {}',
'}',
'',
'pub struct Holder {',
' reader: Pin<Box<Other>>,',
'}',
'',
'pub fn literal_field(mut reader: Pin<&mut Reader>) {',
' let _h = Holder {',
' reader: Box::pin(Other),',
' };',
' reader.as_mut().poll_read();',
'}',
'',
'pub fn inner_block(mut reader: Pin<&mut Reader>, mut other: Other) {',
' {',
' let reader: Pin<&mut Other> = Pin::new(&mut other);',
' let _ = reader;',
' }',
' reader.as_mut().poll_read();',
'}',
'',
'fn main() {}',
'',
].join('\n'));
cg = await CodeGraph.init(root, { index: true });
});

afterAll(() => {
cg?.destroy();
fs.rmSync(root, { recursive: true, force: true });
});

function callees(name: string): string[] {
const fn = cg.getNodesByName(name).find((n) => n.kind === 'function');
expect(fn).toBeDefined();
return cg.getOutgoingEdges(fn!.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName);
}

describe('Rust declared types are read from the binding in scope', () => {
it("a struct literal's field named like the value is no declaration of it", () => {
expect(callees('literal_field')).toContain('Reader::poll_read');
expect(callees('literal_field')).not.toContain('Other::poll_read');
});

it('a let in a block the call is outside of does not shadow it', () => {
expect(callees('inner_block')).toContain('Reader::poll_read');
expect(callees('inner_block')).not.toContain('Other::poll_read');
});
});
24 changes: 10 additions & 14 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21199,22 +21199,18 @@ function rustTypeTextAt(text: string): string {

/**
* The type a Rust local or parameter is written with — `reader: Pin<&mut
* R>`, `let timer: Option<Timer> = …` — read off its nearest declaration in
* the calling function; null when that declaration writes none (`let f =
* pin!(f)`).
* R>`, `let timer: Option<Timer> = …` — as its binding in scope at the call
* writes it (rustBindingAt: a block's own `let`, the latest shadow, never a
* struct literal's `reader: …` field); null when that binding writes none
* (`let f = pin!(f)`).
*/
function rustDeclaredTypeText(name: string, ref: UnresolvedRef, context: ResolutionContext): string | null {
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [];
const start = Math.max(0, enclosingScopeStartLine(ref, context) - 1);
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const declared = new RegExp(`\\blet\\s+(?:mut\\s+)?${escaped}\\b(?:\\s*:(?!:)\\s*(.*))?|\\b${escaped}\\s*:(?!:)\\s*(.*)$`);
for (let i = Math.min(lines.length, ref.line) - 1; i >= start; i--) {
const m = declared.exec(lines[i] ?? '');
if (!m) continue;
const written = m[1] ?? m[2];
return written === undefined ? null : rustTypeTextAt(written);
}
return null;
const scopes = rustScopesOf(ref.filePath, context);
if (!scopes) return null;
const offset = (scopes.lineStarts[ref.line - 1] ?? 0) + Math.max(0, ref.column);
const found = rustBindingAt(scopes, name, offset);
if (!found || found.ambiguous || found.binding.type === undefined) return null;
return rustTypeTextAt(found.binding.type);
}

/**
Expand Down