fix(rust): calls on field chains, struct literals and outside-type paths link by type (#2831)

Issue #2614 reported four Rust calls that linked to a same-named project
method. Case 1 (a slice's contains) was fixed on main by the typed-receiver
work. The other three still reproduced on main in both engines:
vis.map.get("a") on a HashMap field went to Ext::get, J::from(i) under
`use serde_json::Value as J` to a local `impl From<io::Error> for Fail`, and
Xyz { x: 1.0 }.to_val() to Stat::to_val.

Root cause. The extractor keeps one receiver level on a call, so a deeper
field chain (vis.map.get, self.a.b.m, self.0.m) or a struct literal reaches
the resolver as the bare method name, and the typed-receiver reading only
ran for x.m and self.field.m refs. The name strategies then took the one
project get, or one of the two to_val impls. J::from matched no qualified
name, J names no project type, and a single capital letter is read as a
possible generic parameter, so Strategy 3 took the crate's one from. The
same family: under an unrenamed `use serde_json::Value`, Value::default()
linked by qualified name to a project Value's impl.

Fix.
- matchRustWrittenReceiverCall, an exclusive hook beside the as_mut one:
  for a bare-named call, read the receiver from the text at the ref's
  column. Only a field chain rooted at a local, a parameter or self, or a
  struct literal (alone or in parentheses) is read; it is typed with
  rustValueType and the call resolved with rustTypedMethodCall. Any other
  receiver, an unreadable one, or a type no node names (one a macro
  declares) is left to the name strategies. A provided method of a project
  trait the type derives counts (axum-extra's #[derive(TypedPath)]).
- rustFieldValue reads an element of a tuple-typed field (self.pair.0).
- isRustPathCallReachable in gateRustScope: a call through a type path
  whose type the file's use, or the written path, names from std or a
  dependency reaches only the project's impl of a trait for that type, or
  for a trait's path the project's impls of that trait. A path dependency
  the project holds is the project's.

Validation.
- New __tests__/rust-written-receivers.test.ts (12 tests: the issue's
  crate verbatim, chain/literal/derive/path variants, guards): 9 fail on
  main's sources, all pass with the fix. The 27 Rust test files plus
  resolution and cross-language resolution pass.
- A/B against main 5b47bc49 (kernel on), changed call sites triaged as
  removed wrong / added right / wrong to right / same target now by type:
  ripgrep 30 / 18 / 10 / 37 and tokio 281 / 58 / 54 / 141 (every site
  read), axum 18 / 7 / 0 / 7 (every site read), rust-analyzer without
  its parser test data (951 files) 737 / 186 / 434 / 843 (every path
  removal checked by its use; every group sampled). No right link lost.
  Two wrong links added on rust-analyzer: a block-scoped glob
  (`const _: () = { use syntax::ast::*; }`) is read file-wide by the
  existing glob lookup, so a `String` field there reads as ast::String;
  typed locals already misread it the same way on main. Nodes identical
  on every repo; controls gin and express byte-identical.
- Kernel parity (Rust): 1226/1226 files on ripgrep, tokio and axum.
  Resolver-only change, so the kernel needs no mirror.
- Cost: ripgrep median 6.7 s to 6.5 s, tokio 20.2 s to 20.8 s (interleaved,
  median of 3, noisy box); a CPU profile puts about 0.3 s of tokio's 57 s
  busy CPU in the new code and the typed reading it calls.

Fixes #2614

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-23 06:25:23 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent dcb985f1d2
commit 8fdc8a1efb
4 changed files with 775 additions and 4 deletions
+1
View File
@@ -313,6 +313,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- 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 Rust, a method called through a chain of fields or on a struct literal, like `vis.map.get("a")`, `self.inner.config.port()` or `Xyz { x: 1.0 }.to_val()`, now links to the method of the type the last field is declared with, or of the literal's own type. A call through a type from the standard library or a dependency, like `J::from(i)` after `use serde_json::Value as J` or `Value::default()` after `use serde_json::Value`, no longer links to a project function of the same name. Before, such calls were linked by the method's name alone, so a `HashMap` field's `get` reached an unrelated project `get` and a struct literal's `to_val()` reached another type's. Re-index Rust projects after upgrading. Thanks @Egrn for the report and @danusha2345. (#2614)
- 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.
+474
View File
@@ -0,0 +1,474 @@
/**
* A Rust call resolves by the type its receiver is written with, where the
* source writes it (#2614):
*
* - a field chain, which the extractor records by the bare method name —
* `vis.map.get("a")` on a `HashMap` field, `self.inner.cfg.port()` — is a
* call on the type the struct declares for the last field;
* - a struct literal, `Xyz { x: 1.0 }.to_val()`, is a call on its own type;
* - a path through a type from std or a dependency — `J::from(i)` under
* `use serde_json::Value as J`, `serde_json::Value::from(i)` — reaches only
* the project's own impls of a trait for that type.
*
* By the name alone, the reporter's `vis.map.get("a")` went to `Ext::get` (an
* associated function, at that), `Xyz { … }.to_val()` to `Stat::to_val` (the
* same trait's other impl) and `J::from(i)` to `impl From<io::Error> for Fail`.
*/
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';
const roots: string[] = [];
const graphs: Record<'issue' | 'variants' | 'keeps', CodeGraph> = {} as never;
async function index(name: keyof typeof graphs, files: Record<string, string>): Promise<void> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), `cg-rust-written-${name}-`));
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);
}
graphs[name] = await CodeGraph.init(root, { index: true });
}
const manifest = (name: string): string =>
`[package]\nname = "${name}"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\nserde_json = "1"\n`;
beforeAll(async () => {
// The issue's crate, verbatim.
await index('issue', {
'Cargo.toml': manifest('repro'),
'src/main.rs': [
'use std::collections::HashMap;',
'use serde_json::Value as J;',
'',
'pub enum Val {',
' Null,',
' Int(i64),',
' List(Vec<Val>),',
'}',
'',
'pub struct Obj {',
' keys: Vec<String>,',
'}',
'',
'impl Obj {',
' pub fn contains(&self, k: &str) -> bool {',
' self.keys.iter().any(|x| x == k)',
' }',
'}',
'',
'pub struct Ext;',
'',
'impl Ext {',
' pub fn get(name: &str) -> Ext {',
' let _ = name;',
' Ext',
' }',
'}',
'',
'pub struct Fail;',
'',
'impl From<std::io::Error> for Fail {',
' fn from(_: std::io::Error) -> Fail {',
' Fail',
' }',
'}',
'',
'pub struct Xyz {',
' x: f64,',
'}',
'',
'pub struct Stat {',
' n: f64,',
'}',
'',
'pub trait ToVal {',
' fn to_val(&self) -> Val;',
'}',
'',
'impl ToVal for Xyz {',
' fn to_val(&self) -> Val {',
' Val::Int(self.x as i64)',
' }',
'}',
'',
'impl ToVal for Stat {',
' fn to_val(&self) -> Val {',
' Val::Int(self.n as i64)',
' }',
'}',
'',
'// 1. slice::contains, not Obj::contains',
'fn slice_has(names: &[&str], k: &str) -> bool {',
' names.contains(&k)',
'}',
'',
'// 2. HashMap::get on a struct field, not Ext::get',
'pub struct Vis {',
' map: HashMap<String, i32>,',
'}',
'',
'fn map_get(vis: &Vis) -> Option<&i32> {',
' vis.map.get("a")',
'}',
'',
'// 3. serde_json::Value::from via alias J, not Fail::from',
'fn to_json(i: i64) -> J {',
' J::from(i)',
'}',
'',
'// 4. method on struct literal: Xyz::to_val, not Stat::to_val',
'fn centroid() -> Val {',
' Xyz { x: 1.0 }.to_val()',
'}',
'',
'fn main() {',
' let o = Obj { keys: vec![] };',
' let _ = o.contains("a");',
' let _ = Ext::get("x");',
' let _ = Stat { n: 0.0 }.to_val();',
' let _ = slice_has(&["a"], "a");',
' let _ = map_get(&Vis { map: HashMap::new() });',
' let _ = to_json(1);',
' let _ = centroid();',
' let _ = Val::List(vec![Val::Null]);',
'}',
'',
].join('\n'),
});
// Field chains and struct literals of other shapes, beside namesakes of the
// methods they call; and paths through types from outside the project, with
// one `from` in the crate for a guess by the name to take.
await index('variants', {
'Cargo.toml': manifest('variants'),
'src/main.rs': 'mod chains;\nmod config;\nmod paths;\n\nfn main() {}\n',
'src/config.rs': [
'pub struct Config {',
' pub port: u16,',
'}',
'',
'impl Config {',
' pub fn load() -> Config {',
' Config { port: 1 }',
' }',
'',
' pub fn port(&self) -> u16 {',
' self.port',
' }',
'}',
'',
'/// A project type named like `serde_json::Value`.',
'pub struct Value;',
'',
'impl Default for Value {',
' fn default() -> Value {',
' Value',
' }',
'}',
'',
'pub struct Fail;',
'',
'impl From<std::io::Error> for Fail {',
' fn from(_: std::io::Error) -> Fail {',
' Fail',
' }',
'}',
'',
].join('\n'),
'src/chains.rs': [
'use std::collections::HashMap;',
'',
'use crate::config::Config;',
'',
'pub struct Inner {',
' pub map: HashMap<String, i32>,',
' pub cfg: Config,',
'}',
'',
'pub struct Outer {',
' pub inner: Inner,',
' pub pair: (Config, u8),',
'}',
'',
'pub struct Wrap(pub Config);',
'',
'/// Namesakes of the methods the calls below make, beside them.',
'pub struct Dial;',
'',
'impl Dial {',
' pub fn port(&self) -> u16 {',
' 0',
' }',
'',
' pub fn get(&self, _k: &str) -> Option<&i32> {',
' None',
' }',
'}',
'',
'pub enum Shape {',
' Circle { r: f64 },',
'}',
'',
'impl Shape {',
' pub fn area(&self) -> f64 {',
' 0.0',
' }',
'}',
'',
'pub struct Room {',
' pub w: f64,',
'}',
'',
'impl Room {',
' pub fn area(&self) -> f64 {',
' self.w',
' }',
'',
' pub fn fresh(&self) -> f64 {',
' Self { w: 2.0 }.area()',
' }',
'}',
'',
'impl Outer {',
' pub fn deep_std(&self) -> Option<&i32> {',
' self.inner.map.get("a")',
' }',
'',
' pub fn deep_project(&self) -> u16 {',
' self.inner.cfg.port()',
' }',
'',
' pub fn tuple_field(&self) -> u16 {',
' self.pair.0.port()',
' }',
'}',
'',
'impl Wrap {',
' pub fn tuple_self(&self) -> u16 {',
' self.0.port()',
' }',
'}',
'',
'pub fn param_chain(o: &Outer) -> u16 {',
' o.inner.cfg.port()',
'}',
'',
'pub fn param_chain_std(o: &Outer) -> i32 {',
' o.inner',
' .map',
' .get("b")',
' .copied()',
' .unwrap_or(0)',
'}',
'',
'pub fn local_chain() -> u16 {',
' let w = Wrap(Config::load());',
' w.0.port()',
'}',
'',
'pub fn literal() -> u16 {',
' Config { port: 2 }.port()',
'}',
'',
'pub fn literal_in_parens() -> u16 {',
' (&Config { port: 3 }).port()',
'}',
'',
'pub fn variant_literal() -> f64 {',
' Shape::Circle { r: 1.0 }.area()',
'}',
'',
'/// A trait a derive implements (axum-extra\'s `#[derive(TypedPath)]`): the impl is the macro\'s.',
'pub trait TypedPath {',
' fn with_query_params(self, q: u8) -> u8',
' where',
' Self: Sized,',
' {',
' q',
' }',
'}',
'',
'#[derive(TypedPath, Debug)]',
'pub struct UsersShow {',
' pub id: u32,',
'}',
'',
'pub fn derived() -> u8 {',
' UsersShow { id: 1 }.with_query_params(2)',
'}',
'',
].join('\n'),
'src/paths.rs': [
'use serde_json as json;',
'use serde_json::Value;',
'',
'pub fn written_outside(i: i64) -> serde_json::Value {',
' serde_json::Value::from(i)',
'}',
'',
'pub fn crate_alias(i: i64) -> json::Value {',
' json::Value::from(i)',
'}',
'',
'pub fn unrenamed_outside() -> Value {',
' Value::default()',
'}',
'',
'pub fn written_default() -> serde_json::Value {',
' serde_json::Value::default()',
'}',
'',
].join('\n'),
});
// The project's own impls on types from outside it, which those paths do reach.
await index('keeps', {
'Cargo.toml': manifest('keeps'),
'src/main.rs': [
'use std::io::Write as W;',
'use std::path::PathBuf;',
'',
'use serde_json::Value;',
'',
'pub struct Fail;',
'',
'impl From<Fail> for PathBuf {',
' fn from(_: Fail) -> PathBuf {',
' PathBuf::new()',
' }',
'}',
'',
'pub trait Blank {',
' fn blank() -> Self;',
'}',
'',
'impl Blank for serde_json::Value {',
' fn blank() -> Self {',
' serde_json::Value::Null',
' }',
'}',
'',
'pub struct Sink;',
'',
'impl std::io::Write for Sink {',
' fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {',
' Ok(buf.len())',
' }',
'',
' fn flush(&mut self) -> std::io::Result<()> {',
' Ok(())',
' }',
'}',
'',
'fn to_path(f: Fail) -> PathBuf {',
' PathBuf::from(f)',
'}',
'',
'fn blank() -> Value {',
' Value::blank()',
'}',
'',
'fn drain(s: &mut Sink) {',
' let _ = W::flush(s);',
'}',
'',
'fn main() {',
' let _ = to_path(Fail);',
' let _ = blank();',
'}',
'',
].join('\n'),
});
}, 120_000);
afterAll(() => {
for (const cg of Object.values(graphs)) cg?.close();
for (const root of roots) fs.rmSync(root, { recursive: true, force: true });
});
/** The calls on `line` of `file`, as `qualifiedName resolvedBy`, sorted. */
const callsAt = (name: keyof typeof graphs, file: string, line: number): string[] => {
const cg = graphs[name];
const out: string[] = [];
for (const node of cg.getNodesInFile(file)) {
for (const edge of cg.getOutgoingEdges(node.id)) {
if (edge.kind !== 'calls' || edge.line !== line || edge.provenance === 'heuristic') continue;
out.push(`${cg.getNode(edge.target)!.qualifiedName} ${String(edge.metadata?.resolvedBy)}`);
}
}
return [...new Set(out)].sort();
};
describe('the issue\'s crate (#2614)', () => {
it('a `HashMap` field\'s `get` is no project method', () => {
expect(callsAt('issue', 'src/main.rs', 72)).toEqual([]);
});
it('`J::from(i)` under `use serde_json::Value as J` is no project method', () => {
expect(callsAt('issue', 'src/main.rs', 77)).toEqual([]);
});
it('a struct literal calls its own type\'s method', () => {
expect(callsAt('issue', 'src/main.rs', 82)).toEqual(['Xyz::to_val instance-method']);
expect(callsAt('issue', 'src/main.rs', 89)).toEqual(['Stat::to_val instance-method']);
});
it('keeps what was already right', () => {
expect(callsAt('issue', 'src/main.rs', 63)).toEqual([]);
expect(callsAt('issue', 'src/main.rs', 87)).toEqual(['Obj::contains instance-method']);
expect(callsAt('issue', 'src/main.rs', 88)).toEqual(['Ext::get qualified-name']);
});
});
describe('a Rust call on a field chain', () => {
it('of a std type is no project method', () => {
expect(callsAt('variants', 'src/chains.rs', 56)).toEqual([]);
expect(callsAt('variants', 'src/chains.rs', 79)).toEqual([]);
});
it('of a project type is that type\'s method, not a namesake beside the call', () => {
expect(callsAt('variants', 'src/chains.rs', 60)).toEqual(['Config::port instance-method']);
expect(callsAt('variants', 'src/chains.rs', 64)).toEqual(['Config::port instance-method']);
expect(callsAt('variants', 'src/chains.rs', 70)).toEqual(['Config::port instance-method']);
expect(callsAt('variants', 'src/chains.rs', 75)).toEqual(['Config::port instance-method']);
expect(callsAt('variants', 'src/chains.rs', 88)).toEqual(['Config::port instance-method']);
});
});
describe('a Rust call on a struct literal', () => {
it('is its own type\'s method, in parentheses too', () => {
expect(callsAt('variants', 'src/chains.rs', 92)).toEqual(['Config::port instance-method']);
expect(callsAt('variants', 'src/chains.rs', 96)).toEqual(['Config::port instance-method']);
});
it('of an enum\'s variant is the enum\'s method, and `Self { … }` the impl type\'s', () => {
expect(callsAt('variants', 'src/chains.rs', 100)).toEqual(['Shape::area instance-method']);
expect(callsAt('variants', 'src/chains.rs', 50)).toEqual(['Room::area instance-method']);
});
it('reaches the method a trait it derives provides', () => {
expect(callsAt('variants', 'src/chains.rs', 119)).toEqual(['TypedPath::with_query_params instance-method']);
});
});
describe('a Rust path through a type from outside the project', () => {
it('written out or through a crate\'s alias is no project type\'s function', () => {
expect(callsAt('variants', 'src/paths.rs', 5)).toEqual([]);
expect(callsAt('variants', 'src/paths.rs', 9)).toEqual([]);
});
it('a `use` names, or the path writes, is not a project namesake\'s', () => {
expect(callsAt('variants', 'src/paths.rs', 13)).toEqual([]);
expect(callsAt('variants', 'src/paths.rs', 17)).toEqual([]);
});
it('still reaches the project\'s own impls for that type, or of that trait', () => {
expect(callsAt('keeps', 'src/main.rs', 37)).toEqual(['PathBuf::from qualified-name']);
expect(callsAt('keeps', 'src/main.rs', 41)).toEqual(['Value::blank qualified-name']);
expect(callsAt('keeps', 'src/main.rs', 45)).toEqual(['Sink::flush instance-method']);
});
});
+8 -4
View File
@@ -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, isRustForeignImplCallable, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, goDotImportedSupertype, GO_TYPE_KINDS, matchGoAssertedCall, matchGoIndexedCall, matchGoChainedCall, preferGoBuildTwin, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonAttrRefResolves, matchPythonTypedLocalCall, isImportShadowedByJsBinding, isManualMockPath } from './name-matcher';
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, isRustPathCallReachable, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, goDotImportedSupertype, GO_TYPE_KINDS, matchGoAssertedCall, matchGoIndexedCall, matchGoChainedCall, preferGoBuildTwin, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonAttrRefResolves, matchPythonTypedLocalCall, isImportShadowedByJsBinding, isManualMockPath } from './name-matcher';
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes';
@@ -3899,8 +3899,10 @@ 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). And a call, however written, reaches a project impl
* on a std type only as Rust would pick it (isRustForeignImplCallable).
* isRustNameInScope). A call through a path whose type is from outside the
* project reaches only the project's impls for that type (or of that trait,
* isRustPathCallReachable). 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') return result;
@@ -3909,7 +3911,9 @@ export class ReferenceResolver {
const target = this.nodeById(result.targetNodeId);
if (!target) return result;
if (bare && !isRustNameInScope(target, ref, this.context)) return null;
return ref.referenceKind === 'calls' && !isRustForeignImplCallable(target, ref, this.context) ? null : result;
if (ref.referenceKind !== 'calls') return result;
if (!bare && !isRustPathCallReachable(target, ref, this.context)) return null;
return isRustForeignImplCallable(target, ref, this.context) ? result : null;
}
/**
+292
View File
@@ -9285,6 +9285,72 @@ export function isRustForeignImplCallable(target: Node, ref: UnresolvedRef, cont
return local === undefined || rustFirstArgIs(local, ref, context);
}
/**
* Whether a Rust call through a type's path can mean `target` (#2614). A
* type the path names from std or a dependency — through the file's `use`
* (`J::from(i)` under `use serde_json::Value as J`, `Value::default()` under
* `use serde_json::Value`) or as written (`serde_json::Value::default()`) —
* is that crate's own: the project holds a function of it only as its own
* impl of a trait for it (`impl Blank for serde_json::Value`, `impl From<Fail>
* for PathBuf`), and a call through a trait's path (`W::flush(s)` under `use
* std::io::Write as W`) reaches the project's impls of that trait. A
* same-named function of a project type is another type's: the reporter's
* `J::from(i)` went to a local `impl From<io::Error> for Fail`, a guess by
* the name alone, and `Value::default()` to a project `Value` of its own.
*/
export function isRustPathCallReachable(target: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
if (target.language !== 'rust') return true;
const segments = ref.referenceName.split('::');
if (segments.length < 2 || !segments.every((s) => /^[A-Za-z_]\w*$/.test(s))) return true;
const type = segments[segments.length - 2]!;
if (!/^[A-Z]/.test(type) || type === 'Self') return true;
const outside = rustOutsidePathType(type, segments.slice(0, -2), ref, context);
if (outside === null) return true;
const impl = target.kind === 'method' ? rustTraitImplHeader(target, context) : null;
if (!impl) return false;
if (impl.trait === outside) return true;
if (impl.self !== outside) return false;
// The type the impl is for, as its own file names it: the outside one, not a project namesake.
const head = rustPathHead(impl.selfText.replace(/^(?:&\s*(?:'\w+\s+)?(?:mut\s+)?)+/, ''));
const lookup = rustTypeLookup(head?.name ?? impl.self, head?.segments ?? [], target.filePath, context, rustNodeOffset(target, context));
return lookup.outside || !lookup.nodes.some((n) => RUST_OWNER_KINDS.has(n.kind));
}
/**
* The name of the type a Rust path `path::type` names from std or a
* dependency, where the file's `use`s or the path itself say it is from
* there — `J` under `use serde_json::Value as J` is `Value` — or null: one of
* the project's, or one nothing written says is from outside it.
*/
function rustOutsidePathType(type: string, path: string[], ref: UnresolvedRef, context: ResolutionContext): string | null {
const scopes = rustScopesOf(ref.filePath, context);
const at = scopes ? (scopes.lineStarts[ref.line - 1] ?? 0) + Math.max(0, ref.column) : undefined;
let root: string | undefined;
let real = type;
if (path.length === 0) {
const bound = rustUsePath(type, ref.filePath, at, context);
if (!bound || bound.length < 2) return null;
root = bound[0];
real = bound[bound.length - 1]!;
} else {
root = rustUsePath(path[0]!, ref.filePath, at, context)?.[0] ?? path[0];
}
return root !== undefined && isRustOutsideCrate(root, ref, context) ? real : null;
}
/** Whether a Rust path's first segment names std, or a crate the manifests depend on that the project doesn't hold. */
function isRustOutsideCrate(root: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
if (RUST_STD_ROOTS.has(root)) return true;
if (root === 'crate' || root === 'self' || root === 'super' || rustProjectCrates(context).has(root)) return false;
if (!rustDependencyCrates(context).has(root)) return false;
// A path dependency the project holds, a workspace member or not, is the project's.
const graph = rustCrateGraph(context);
const own = rustCrateDirOf(ref.filePath, graph);
if (own !== null && graph.crates.get(own)?.keys.has(root)) return false;
for (const crate of graph.crates.values()) if (crate.name === root) return false;
return true;
}
/** Whether `type` is a Rust enum that declares `variant` (#2328). */
function declaresRustVariant(type: Node, variant: string, context: ResolutionContext): boolean {
if (type.kind !== 'enum') return false;
@@ -10385,6 +10451,8 @@ function rustFieldValue(
else written = rustValueType(recv, at, scopes, ref, context, depth + 1);
if (!written) return null;
const read = rustReadType(written, ref, context);
// `self.pair.0` on a field declared `(Config, u8)`: the tuple's field at that place.
if (read.kind === 'outside' && read.name === '(' && /^\d+$/.test(field)) return rustTupleField(written, Number(field));
if (read.kind !== 'named') return null;
const lookup = rustTypeLookup(read.name, read.path, read.written.file, context, read.written.at);
const structs = lookup.outside ? [] : lookup.nodes.filter((n) => n.kind === 'struct' || n.kind === 'union');
@@ -24330,6 +24398,225 @@ function matchRustAsMutChainCall(ref: UnresolvedRef, context: ResolutionContext)
return rustLentCall(projected && decl.pinned ? `Pin<&mut ${decl.type}>` : decl.type, name, ref, context, tried);
}
/**
* A Rust call on a field chain or a struct literal, which the extractor
* records by its bare name — `vis.map.get("a")`, `self.inner.cfg.port()`,
* `self.0.len()`, `Xyz { x: 1.0 }.to_val()` (#2614) — is a call on the type
* that receiver is written with: the last field's as its struct declares it,
* the literal's own (rustValueType, rustTypedMethodCall). By the name alone,
* the reporter's `vis.map.get("a")` on a `HashMap<String, i32>` field went to
* the project's one `get`, `Ext::get`, an associated function at that, and
* `Xyz { … }.to_val()` to `Stat::to_val`, the same trait's other impl.
* Undefined, for the name strategies to decide as before, where the receiver
* is anything else (a call's value, an index, a macro) or its type can't be
* read.
*/
function matchRustWrittenReceiverCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined {
const scopes = rustScopesOf(ref.filePath, context);
const lineStart = scopes?.lineStarts[ref.line - 1];
if (!scopes || lineStart === undefined || ref.column < 0) return undefined;
const start = lineStart + ref.column;
if (start >= (scopes.lineStarts[ref.line] ?? scopes.code.length)) return undefined;
const receiver = rustWrittenCallReceiver(scopes, start, ref.referenceName);
if (receiver === null) return undefined;
const written = rustValueType(receiver, start, scopes, ref, context, 0);
if (!written) return undefined;
const read = rustReadType(written, ref, context);
// A type no declaration the index holds names — one a macro declares, like
// tokio's `type AtomicUnsignedShort = …` inside `cfg_has_atomic_u64! { … }` —
// says nothing of what the call reaches, unless a wrapper around it has the
// method itself (`Arc::clone`): the name strategies decide, as before.
if (read.kind === 'named' && !read.wrappers.some((w) => RUST_WRAPPER_OWN_METHODS.get(w)?.has(ref.referenceName))) {
const lookup = rustTypeLookup(read.name, read.path, read.written.file, context, read.written.at);
if (lookup.undeclared && rustMethodsNamed(lookup.name, ref.referenceName, context).length === 0) return undefined;
}
let result = rustTypedMethodCall(read, ref.referenceName, ref, context);
if (result === 'unread') return undefined;
if (result === null && read.kind === 'named') result = rustDerivedTraitMethod(read, ref.referenceName, ref, context);
if (result) {
let typed = RUST_TYPED_REFS.get(context);
if (!typed) RUST_TYPED_REFS.set(context, (typed = new WeakSet()));
typed.add(ref);
}
return result;
}
/**
* The method a Rust project trait that a type derives provides, for a call
* on a value of the type: `#[derive(TypedPath)] struct UsersShow { … }`
* reaches axum-extra's `TypedPath::with_query_params` through the impl the
* derive writes, which the index doesn't hold. Null unless exactly one
* derived trait in scope at the call declares the method with a body.
*/
function rustDerivedTraitMethod(read: Extract<RustTypeRead, { kind: 'named' }>, method: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
const lookup = rustTypeLookup(read.name, read.path, read.written.file, context, read.written.at);
const types = lookup.outside ? [] : lookup.nodes.filter((n) => RUST_OWNER_KINDS.has(n.kind));
if (types.length !== 1) return null;
const found: Node[] = [];
for (const name of rustDerivedNames(types[0]!, context)) {
const traits = context.getNodesByName(name).filter((n) => n.kind === 'trait' && n.language === 'rust' && n.name === name);
for (const m of rustTraitMethods(traits, method, context)) {
const trait = rustTraitOf(m, context);
if (trait && rustHasBody(m, context) && isRustTraitInScope(trait.name, ref, m, context)) found.push(m);
}
}
const owners = new Set(found.map((m) => rustTraitOf(m, context)?.id));
return owners.size === 1 ? { original: ref, targetNodeId: found[0]!.id, confidence: 0.85, resolvedBy: 'instance-method' } : null;
}
/** The names a Rust type's `#[derive(…)]` attributes write, each by its path's last segment. */
function rustDerivedNames(decl: Node, context: ResolutionContext): string[] {
const scopes = rustScopesOf(decl.filePath, context);
if (!scopes) return [];
const start = rustNodeOffset(decl, context);
const keyword = new RegExp(`\\b(?:struct|enum|union)\\s+${escapeRegExp(decl.name)}\\b`).exec(scopes.code.slice(start, start + 2000));
if (!keyword) return [];
const at = start + keyword.index;
// The attributes right before the keyword: `#[derive(TypedPath, Deserialize)] #[typed_path("…")] pub struct …`.
const attrs = /((?:#\s*\[(?:[^[\]]|\[[^[\]]*\])*\]\s*)+)(?:pub(?:\s*\([^()]*\))?\s+)?$/.exec(scopes.code.slice(Math.max(0, at - 2000), at))?.[1] ?? '';
const names: string[] = [];
for (const m of attrs.matchAll(/\bderive\s*\(([^()]*)\)/g)) {
for (const part of m[1]!.split(',')) {
const name = part.trim().split('::').pop()?.trim();
if (name && /^[A-Za-z_]\w*$/.test(name)) names.push(name);
}
}
return names;
}
/** Past the whitespace at `i` in a Rust file's blanked text. */
function rustSpaceEnd(code: string, i: number): number {
while (i < code.length && /\s/.test(code[i]!)) i++;
return i;
}
/** Past the arguments of a call at `p` — `(…)`, after a turbofish `::<…>` — or -1 where no call's arguments start there. */
function rustCallArgsEnd(scopes: RustScopes, p: number): number {
const { code, close } = scopes;
let q = rustSpaceEnd(code, p);
if (code.startsWith('::', q)) {
q = rustSpaceEnd(code, q + 2);
if (code[q] !== '<') return -1;
const end = rustAngleEnd(code.slice(q, q + 512), 0);
if (end < 0) return -1;
q = rustSpaceEnd(code, q + end + 1);
}
if (code[q] !== '(') return -1;
const end = close.get(q);
return end === undefined ? -1 : end + 1;
}
/**
* A field chain or a struct literal at `from` in a Rust file's blanked text,
* up to `limit`: where it ends, how many fields it reads (`.inner`, `.0`),
* and whether it starts with a value that writes its type — a struct literal
* (`Xyz { … }`, `Shape::Circle { … }`, `Self { … }`) or one of these in
* parentheses, `(&Config { … })`. Null for anything else: a path's value, a
* call, a macro, a `match`.
*/
function rustFieldChainAt(scopes: RustScopes, from: number, limit: number): { end: number; hops: number; typed: boolean } | null {
const { code, close } = scopes;
let p = from;
let typed = false;
if (code[p] === '(') {
const end = close.get(p);
if (end === undefined || end >= limit) return null;
let q = rustSpaceEnd(code, p + 1);
if (code[q] === '&') {
q = rustSpaceEnd(code, q + 1);
if (/^mut\b/.test(code.slice(q, q + 4))) q = rustSpaceEnd(code, q + 3);
}
const inner = rustFieldChainAt(scopes, q, end);
if (!inner || rustSpaceEnd(code, inner.end) !== end || (inner.hops === 0 && !inner.typed)) return null;
p = end + 1;
typed = true;
} else {
const path = /^(?:[A-Za-z_]\w*\s*::\s*)*[A-Za-z_]\w*/.exec(code.slice(p, Math.min(limit, p + 512)))?.[0];
if (!path) return null;
const segments = path.split('::').map((s) => s.trim());
const last = segments[segments.length - 1]!;
let q = rustSpaceEnd(code, p + path.length);
// `Wrapper::<u8> { … }`: the literal's type arguments.
if (code.startsWith('::', q)) {
const lt = rustSpaceEnd(code, q + 2);
const end = code[lt] === '<' ? rustAngleEnd(code.slice(lt, lt + 512), 0) : -1;
if (end < 0) return null;
q = rustSpaceEnd(code, lt + end + 1);
}
if (/^[A-Z]/.test(last) && code[q] === '{') {
const end = close.get(q);
if (end === undefined || end >= limit) return null;
p = end + 1;
typed = true;
} else if (segments.length === 1 && /^[a-z_]/.test(last)) {
// A local, a parameter or `self`; a keyword (`match x { … }`) reads no field after it.
p += path.length;
} else {
return null;
}
}
let hops = 0;
for (;;) {
let q = rustSpaceEnd(code, p);
if (q >= limit || code[q] !== '.' || code[q + 1] === '.') break;
q = rustSpaceEnd(code, q + 1);
const id = /^(?:[A-Za-z_]\w*|\d+)/.exec(code.slice(q, Math.min(limit, q + 128)))?.[0];
// `.await` and a method call end the chain.
if (!id || id === 'await' || rustCallArgsEnd(scopes, q + id.length) >= 0) break;
p = q + id.length;
hops++;
}
return { end: p, hops, typed };
}
/**
* The receiver of the Rust call `name(…)` whose expression starts at `start`
* (where the extractor records a call it names bare), when it is a field
* chain — `vis.map`, `self.inner.cfg`, `self.0` — or a struct literal, alone
* or in parentheses (rustFieldChainAt), as the file's blanked text writes it.
* Null for any other receiver, and when a later link of the chain calls
* `name` again (`m.inner.get(k).get(j)`): that call is recorded at the same
* place, and can't be told from this one.
*/
function rustWrittenCallReceiver(scopes: RustScopes, start: number, name: string): string | null {
const { code, close } = scopes;
if (/\w/.test(code[start - 1] ?? '')) return null;
const chain = rustFieldChainAt(scopes, start, code.length);
// `x.name(…)` and `self.name(…)` are recorded with their receiver.
if (!chain || (chain.hops === 0 && !chain.typed)) return null;
const dot = rustSpaceEnd(code, chain.end);
if (code[dot] !== '.' || code[dot + 1] === '.') return null;
const at = rustSpaceEnd(code, dot + 1);
if (!code.startsWith(name, at) || /\w/.test(code[at + name.length] ?? '')) return null;
let p = rustCallArgsEnd(scopes, at + name.length);
if (p < 0) return null;
// The links after the call: `?`, `[…]`, `.field`, `.method(…)`.
for (;;) {
p = rustSpaceEnd(code, p);
const c = code[p];
if (c === '?') {
p++;
continue;
}
if (c === '[') {
const end = close.get(p);
if (end === undefined) return null;
p = end + 1;
continue;
}
if (c !== '.' || code[p + 1] === '.') break;
p = rustSpaceEnd(code, p + 1);
const id = /^(?:[A-Za-z_]\w*|\d+)/.exec(code.slice(p, p + 128))?.[0];
if (!id) break;
p += id.length;
const args = rustCallArgsEnd(scopes, p);
if (args < 0) continue;
if (id === name) return null;
p = args;
}
return code.slice(start, dot).trim();
}
/** Std pointers whose `as_ref` / `as_mut` lend what they hold: `Box<T>`, `NonNull<T>` → `&mut T`. */
const RUST_LENDING_POINTERS: ReadonlySet<string> = new Set(['Box', 'NonNull', 'Rc', 'Arc', 'Cow']);
@@ -26731,9 +27018,14 @@ function matchReferenceInner(
// extractor records by its bare name — `self.delay.as_mut().reset(…)` — is
// a call on the type the receiver is declared with, or on nothing of the
// project's (matchRustAsMutChainCall).
// So is one on a field chain or a struct literal — `vis.map.get("a")`,
// `Xyz { x: 1.0 }.to_val()` — a call on the type that receiver is written
// with (matchRustWrittenReceiverCall).
if (ref.language === 'rust' && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName)) {
const lent = nmTimedT('rustAsMut', ref, () => matchRustAsMutChainCall(ref, context));
if (lent !== undefined) return lent;
const written = nmTimedT('rustWritten', ref, () => matchRustWrittenReceiverCall(ref, context));
if (written !== undefined) return written;
}
// A Kotlin call on what an unqualified call returns, which the extractor