diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fbd831f..9ec68ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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::()`, and a call on a type that implements one trait twice, like `Encoder` 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`, `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 Worker`), 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::("name")`, `parse_config::()` or `Pool::::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. diff --git a/__tests__/rust-written-receivers.test.ts b/__tests__/rust-written-receivers.test.ts new file mode 100644 index 00000000..679f7ff4 --- /dev/null +++ b/__tests__/rust-written-receivers.test.ts @@ -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 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): Promise { + 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),', + '}', + '', + 'pub struct Obj {', + ' keys: Vec,', + '}', + '', + '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 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,', + '}', + '', + '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 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,', + ' 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 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 {', + ' 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']); + }); +}); diff --git a/src/resolution/index.ts b/src/resolution/index.ts index c568b3de..816b020d 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -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; } /** diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index e4098cd8..25a697a1 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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 + * 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 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` 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, 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:: { … }`: 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`, `NonNull` → `&mut T`. */ const RUST_LENDING_POINTERS: ReadonlySet = 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