|
|
@@ -1731,6 +1731,18 @@ export function matchMethodCall(
|
|
|
return matchGoFieldChainCall(objectOrClass!, methodName!, ref, context);
|
|
|
}
|
|
|
|
|
|
+ // Rust call through a field of the enclosing type — `self.inner.run()`,
|
|
|
+ // emitted as `self.inner.run` (#1585). Same discipline as the Go branch
|
|
|
+ // above, and EXCLUSIVE for the same reason: validated field-type inference
|
|
|
+ // or nothing. Letting this shape reach the bare-name strategies below is
|
|
|
+ // how `self.inner.run()` resolved to a same-named method on an unrelated
|
|
|
+ // type — or to the calling method itself, a self-edge the source doesn't
|
|
|
+ // contain — whenever the field's type was external or merely shared a
|
|
|
+ // method name with something nearby.
|
|
|
+ if (ref.language === 'rust' && dotMatch && objectOrClass!.startsWith('self.')) {
|
|
|
+ return matchRustSelfFieldCall(objectOrClass!.slice('self.'.length), methodName!, ref, context);
|
|
|
+ }
|
|
|
+
|
|
|
// Java/Kotlin: receiver may be a field whose name doesn't match the type by
|
|
|
// Java naming convention (`userbo` → class `UserBO`, abbreviated). Look up
|
|
|
// the field in the enclosing class to get its declared type, then resolve
|
|
|
@@ -1992,6 +2004,110 @@ function matchGoFieldChainCall(
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
+// Rust primitives and the prelude's own types: a field of one of these never
|
|
|
+// names a project type, so a `self.<field>.<method>()` on it stays unresolved.
|
|
|
+const RUST_NON_PROJECT_FIELD_TYPES = new Set([
|
|
|
+ 'bool', 'char', 'str', 'String',
|
|
|
+ 'i8', 'i16', 'i32', 'i64', 'i128', 'isize',
|
|
|
+ 'u8', 'u16', 'u32', 'u64', 'u128', 'usize',
|
|
|
+ 'f32', 'f64',
|
|
|
+ 'Self', 'self',
|
|
|
+]);
|
|
|
+
|
|
|
+/**
|
|
|
+ * Reduce a Rust field's declared type text to the simple name of the type a
|
|
|
+ * method call on that field auto-derefs to, or null when there is none we can
|
|
|
+ * name. Only the layers Rust's method-call auto-deref looks through are
|
|
|
+ * unwrapped: references (`&`, `&'a mut`) and the owning smart pointers
|
|
|
+ * (`Box`, `Rc`, `Arc`) — `self.inner.run()` with `inner: Box<Inner>` calls
|
|
|
+ * `Inner::run`. Containers that do NOT auto-deref to their parameter
|
|
|
+ * (`Option<Inner>`, `Vec<Inner>`, `Mutex<Inner>`, `RefCell<Inner>`) keep their
|
|
|
+ * own name and, having no project node, resolve to nothing — `self.items.push()`
|
|
|
+ * must never become `Inner::push`. A trait object (`Box<dyn Source>`) yields
|
|
|
+ * the trait, whose method node the interface-impl synthesizer fans out. A
|
|
|
+ * generic parameter (`T`), a primitive, a tuple / array / raw pointer / fn
|
|
|
+ * type, or a non-identifier yields null.
|
|
|
+ */
|
|
|
+export function rustFieldTypeName(raw: string): string | null {
|
|
|
+ let t = raw.trim();
|
|
|
+ for (;;) {
|
|
|
+ const before = t;
|
|
|
+ t = t.replace(/^&\s*(?:'\w+\s+)?(?:mut\s+)?/, '');
|
|
|
+ t = t.replace(/^(?:Box|Rc|Arc)\s*<\s*/, '');
|
|
|
+ t = t.replace(/^(?:dyn|impl)\s+/, '');
|
|
|
+ if (t === before) break;
|
|
|
+ }
|
|
|
+ // Drop generic args, the closing `>`s of unwrapped pointers, and trait-object
|
|
|
+ // bounds (`dyn Source + Send`); keep the last path segment.
|
|
|
+ t = t.replace(/[<>+].*$/, '').trim();
|
|
|
+ const seg = t.split('::').filter(Boolean).pop();
|
|
|
+ if (!seg || !/^[A-Za-z_]\w*$/.test(seg)) return null;
|
|
|
+ if (RUST_NON_PROJECT_FIELD_TYPES.has(seg)) return null;
|
|
|
+ if (/^[A-Z]$/.test(seg)) return null; // bare single-letter generic parameter
|
|
|
+ return seg;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Resolve a Rust call through a field of the enclosing type —
|
|
|
+ * `self.inner.run()`, emitted by the extractor as `self.inner.run` (#1585).
|
|
|
+ * Mirrors the Go 2-hop precedent above (#1276): the owner type is the calling
|
|
|
+ * method's qualified-name prefix (`Outer::run` → `Outer`), the field's declared
|
|
|
+ * type comes from the owner struct's OWN declaration lines, and the method is
|
|
|
+ * resolved AND VALIDATED on that type by resolveMethodOnType. The caller
|
|
|
+ * treats this branch as exclusive for `self.<field>` receivers: a field whose
|
|
|
+ * type is external (`std::vec::IntoIter`, `regex::Regex`), a generic
|
|
|
+ * parameter, or not declared where we can see it yields null and the ref stays
|
|
|
+ * unresolved. Rust struct fields are not graph nodes, so the declaration text
|
|
|
+ * is the only place the type lives.
|
|
|
+ */
|
|
|
+function matchRustSelfFieldCall(
|
|
|
+ field: string,
|
|
|
+ methodName: string,
|
|
|
+ ref: UnresolvedRef,
|
|
|
+ context: ResolutionContext,
|
|
|
+): ResolvedRef | null {
|
|
|
+ // The extractor only ever emits a single field hop; anything else is not ours.
|
|
|
+ if (!field || field.includes('.')) return null;
|
|
|
+ const caller = context.getNodeById?.(ref.fromNodeId);
|
|
|
+ if (!caller) return null;
|
|
|
+ const sep = caller.qualifiedName.lastIndexOf('::');
|
|
|
+ if (sep <= 0) return null; // a free fn has no `self`
|
|
|
+ const owner = caller.qualifiedName.slice(0, sep).split('::').pop();
|
|
|
+ if (!owner) return null;
|
|
|
+
|
|
|
+ const owners = preferCallSiteFile(context.getNodesByName(owner), ref.filePath).filter(
|
|
|
+ (n) =>
|
|
|
+ (n.kind === 'struct' || n.kind === 'union' || n.kind === 'class') &&
|
|
|
+ n.language === 'rust'
|
|
|
+ );
|
|
|
+ const fieldEsc = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
+ // `pub inner: Inner,` / `inner: Box<dyn Source>,` / `pub(crate) inner: T }` —
|
|
|
+ // the type text runs to the field separator. A comma inside generic args
|
|
|
+ // (`HashMap<K, V>`) truncates the capture, which rustFieldTypeName then
|
|
|
+ // reduces to the container's own name — exactly the non-deref case it
|
|
|
+ // refuses anyway.
|
|
|
+ const fieldRe = new RegExp(`\\b${fieldEsc}\\s*:\\s*([^,{}]+)`);
|
|
|
+ for (const s of owners) {
|
|
|
+ const source = context.readFile(s.filePath);
|
|
|
+ if (!source) continue;
|
|
|
+ // Only the struct's own declaration lines, comment-stripped line by line —
|
|
|
+ // same discipline as the Go helper: prose or a same-named identifier
|
|
|
+ // elsewhere in the file can never donate a type.
|
|
|
+ const declLines = source.split('\n').slice(Math.max(0, s.startLine - 1), s.endLine);
|
|
|
+ for (const rawLine of declLines) {
|
|
|
+ const line = rawLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '');
|
|
|
+ const m = line.match(fieldRe);
|
|
|
+ if (!m || !m[1]) continue;
|
|
|
+ const fieldType = rustFieldTypeName(m[1]);
|
|
|
+ // The field is declared here; whether or not its type names a project
|
|
|
+ // symbol, this owner is the answer — no other same-named struct applies.
|
|
|
+ if (!fieldType) return null;
|
|
|
+ return resolveMethodOnType(fieldType, methodName, ref, context, 0.85, 'instance-method');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Split a camelCase or PascalCase string into words.
|
|
|
*/
|