mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
fix(resolution): a bare-recorded call on another expression's value is not the caller (#2223)
Generalizes the collapsed-chain self veto beyond TS/JS this./window. roots: a call the extractor recorded by its bare name but the source writes as a member call on anything other than this/self/super is never the calling method; and a value's initializer never calls the value itself. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- A method that calls a same-named method on another expression's result is no longer linked to itself in any language. Examples are Scala's `requestToArmeria(request).execute()` inside `execute()`, Rust's `self.0.into_route(state)` inside `into_route()`, and Kotlin's `this@Buffer.write(…)` from an inner object. A value whose initializer chain calls a same-named method, like sttp's `val response = basicRequest.get(…).response(…)`, no longer links to itself either.
|
||||
- In Spring projects, a class name now resolves to the one in the reference's own file or package before any guess by folder convention. A class nested inside another file's class is never reached by its bare name. Before, every MyBatis `Example` class's `new Criteria()` in mall linked to the first `Example`'s nested `Criteria`, and halo's Lombok `@Builder` linked to a nested `Builder` class elsewhere.
|
||||
- A method that hands its call on to another object is no longer linked to itself. In TypeScript and JavaScript, a call through `this.<field>` or `window.<x>` counts as recursion only when the field is declared as the method's own class. A guess from a receiver's name alone never lands on the calling method. Before, BookStack's `toggle()` doing `this.container.classList.toggle('open')`, `listen()` doing `window.$events.listen(…)`, and `FileStorage::delete` doing `$storage->delete($path)` each pointed at themselves.
|
||||
- C# names now follow the language's scopes. A block `namespace X { … }` qualifies only the types inside it, so a type declared after the block, or in a file's second namespace, is no longer filed under the first. A namespace written inside another is `Outer.Inner`. A `using` links to the project's namespace instead of another file's `using` of the same name. A `global using` applies only within its own project. A nested type is reachable by bare name only from inside its owner or a type deriving from it, including through another partial part. Before, serilog's `Guard` was filed under `JetBrains.Annotations`, its tests' `Some.InformationEvent()` went to the performance tests' `Some`, and AutoMapper's same-file `new Source()` could reach another test class's nested `Source`.
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* A method that hands its call on to another object — a wrapper — is not
|
||||
* calling itself. TS/JS calls through `this.<field>…` or `window.<x>…` reach
|
||||
* the resolver by their bare name, so BookStack's `toggle()` doing
|
||||
* `this.container.classList.toggle('open')` and `listen()` doing
|
||||
* `window.$events.listen(…)` bound to themselves; a guess from a receiver's
|
||||
* name alone (`FileStorage::delete` doing `$storage->delete($path)`) did too.
|
||||
* A recursion through a field the class declares as its own type stays.
|
||||
* calling itself. Calls through `this.<field>…` / `window.<x>…` (TS/JS) or
|
||||
* through an expression's value (Scala `requestToArmeria(request).execute()`,
|
||||
* Rust `self.0.into_route(state)`) reach the resolver by their bare name, so
|
||||
* BookStack's `toggle()` doing `this.container.classList.toggle('open')`
|
||||
* bound to itself; a guess from a receiver's name alone (`FileStorage::delete`
|
||||
* doing `$storage->delete($path)`) did too, and a value's initializer chain
|
||||
* (`val response = basicRequest.get(…).response(…)`) to the value. A recursion
|
||||
* through a field the class declares as its own type stays.
|
||||
*/
|
||||
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
@@ -64,6 +66,30 @@ class ImageStorage
|
||||
{
|
||||
}
|
||||
}
|
||||
`,
|
||||
'core/src/main/scala/sttp/ArmeriaBackend.scala': `package sttp
|
||||
|
||||
class ArmeriaBackend {
|
||||
def requestToArmeria(request: String): Client = new Client
|
||||
|
||||
def execute(request: String): Unit = {
|
||||
val armeriaRes = requestToArmeria(request).execute()
|
||||
}
|
||||
}
|
||||
|
||||
class CurlTest {
|
||||
val response = basicRequest
|
||||
.get("http://example.com")
|
||||
.response(asString)
|
||||
}
|
||||
`,
|
||||
'src/routing/route.rs': `pub struct BoxedIntoRoute(Box<dyn ErasedIntoRoute>);
|
||||
|
||||
impl BoxedIntoRoute {
|
||||
pub fn into_route(self, state: u32) -> u32 {
|
||||
self.0.into_route(state)
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
@@ -93,6 +119,11 @@ describe('a call a method hands on', () => {
|
||||
expect(selfCallLines('resources/js/tree.ts')).toEqual([5]);
|
||||
});
|
||||
|
||||
it('through another expression’s value is not the method itself', () => {
|
||||
expect(selfCallLines('core/src/main/scala/sttp/ArmeriaBackend.scala')).toEqual([]);
|
||||
expect(selfCallLines('src/routing/route.rs')).toEqual([]);
|
||||
});
|
||||
|
||||
it('through a receiver named like the caller’s class is not the method itself', () => {
|
||||
expect(selfCallLines('app/Uploads/FileStorage.php')).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1403,31 +1403,35 @@ export const CASE_INSENSITIVE_LANGUAGES = new Set<string>(['php', 'pascal', 'cfm
|
||||
* used to pick over the module-scope function the call actually means.
|
||||
*/
|
||||
/**
|
||||
* The links between `this` / `super` / `window` and the method of a TS/JS
|
||||
* call the extractor recorded by its bare name — `this.container.classList
|
||||
* .toggle()` reaches the resolver as `toggle`, with links `container`,
|
||||
* `classList`. Null for a call not written that way, `this.toggle()` included.
|
||||
* The receiver a call the extractor recorded by its bare name is written on,
|
||||
* read from the source: TS/JS keeps `this.container.classList.toggle()` and
|
||||
* `window.$events.listen()` bare, Scala `requestToArmeria(request).execute()`
|
||||
* and `_.get.whenRequestMatchesPartial(…)`. `'self'` for `this.m()` /
|
||||
* `self.m()` / `super.m()` / `super().m()`; null for a call written bare (or
|
||||
* not found). `links` are the member names between `this` and the method.
|
||||
*/
|
||||
function collapsedJsChain(ref: UnresolvedRef, context: ResolutionContext): { root: string; links: string[] } | null {
|
||||
if (!JS_FAMILY.has(ref.language) || ref.referenceKind !== 'calls' || !/^[A-Za-z_$][\w$]*$/.test(ref.referenceName)) return null;
|
||||
function bareCallReceiver(ref: UnresolvedRef, context: ResolutionContext): { receiver: string; links: string[] } | null {
|
||||
if (ref.referenceKind !== 'calls' || !/^[A-Za-z_$][\w$]*$/.test(ref.referenceName)) return null;
|
||||
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/);
|
||||
if (!lines) return null;
|
||||
const text = lines.slice(ref.line - 1, ref.line + 3).join('\n').slice(ref.column);
|
||||
const text = lines.slice(ref.line - 1, ref.line + 7).join('\n').slice(Math.max(0, ref.column));
|
||||
const name = ref.referenceName.replace(/\$/g, '\\$');
|
||||
const m = new RegExp(`^(this|super|window)((?:\\s*\\??\\.\\s*#?[\\w$]+(?:\\([^()]*\\))?)+?)\\s*\\??\\.\\s*${name}\\s*(?:<[^<>()]*>)?\\s*\\(`).exec(text);
|
||||
if (!m) return null;
|
||||
return { root: m[1]!, links: m[2]!.split('.').map((l) => l.replace(/[\s?]/g, '')).filter((l) => l !== '') };
|
||||
const at = new RegExp(`(?<![\\w$])${name}\\s*(?:<[^<>()]*>|\\[(?:[^\\[\\]]|\\[[^\\[\\]]*\\])*\\])?\\s*[({]`).exec(text);
|
||||
if (!at) return null;
|
||||
const before = text.slice(0, at.index).replace(/\s+$/, '');
|
||||
if (!/\??\.$/.test(before)) return null;
|
||||
const head = before.replace(/\??\.$/, '').replace(/\s+$/, '');
|
||||
if (/(?:^|[^\w$.])(?:this|self|super|Self)$/.test(head) || /(?:^|[^\w$.])super\s*\([^()]*\)$/.test(head)) return { receiver: 'self', links: [] };
|
||||
const chain = /(?:^|[^\w$.#])((?:this|super)(?:\s*\??\.\s*#?[\w$]+)+)$/.exec(head);
|
||||
const links = chain ? chain[1]!.split('.').slice(1).map((l) => l.replace(/[\s?]/g, '')) : [];
|
||||
return { receiver: head.slice(-40), links };
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a collapsed `this.<field>.m()` inside `m` can be `m` itself: only
|
||||
* when the class declares the field as its own type — a tree node's
|
||||
* `this.left.insert(v)` — never through a field of another type
|
||||
* (`this.editor.input.focus()`, `this.container.classList.toggle()`).
|
||||
*/
|
||||
/** Whether a call recorded by its bare name is written on something other than the caller's own object. */
|
||||
function isCollapsedNonRecursion(ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
const chain = collapsedJsChain(ref, context);
|
||||
return chain !== null && !isCollapsedSelfRecursion(chain, ref, context);
|
||||
const written = bareCallReceiver(ref, context);
|
||||
if (!written || written.receiver === 'self') return false;
|
||||
return !(JS_FAMILY.has(ref.language) && isCollapsedSelfRecursion({ root: 'this', links: written.links }, ref, context));
|
||||
}
|
||||
|
||||
function isCollapsedSelfRecursion(chain: { root: string; links: string[] }, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
@@ -8379,12 +8383,20 @@ export function matchReference(
|
||||
): ResolvedRef | null {
|
||||
const result = gateLanguageMatch(matchReferenceInner(ref, context), ref, context);
|
||||
// `this.container.classList.toggle()` inside `toggle()`, `window.$events
|
||||
// .listen()` inside `listen()`: a member of what the chain reaches, which is
|
||||
// the calling method only through a field of the caller's own type.
|
||||
// .listen()` inside `listen()`, Scala's `requestToArmeria(request).execute()`
|
||||
// inside `execute()`: a member of what the receiver is, which is the calling
|
||||
// method only through a TS/JS field of the caller's own type.
|
||||
if (result && result.targetNodeId === ref.fromNodeId && isCollapsedNonRecursion(ref, context)) return null;
|
||||
// Nor does a value's initializer call the value: sttp's `val response =
|
||||
// basicRequest.get(…).response(asStringAlways)` is a request's `response`.
|
||||
if (result && result.targetNodeId === ref.fromNodeId && ref.referenceKind === 'calls' &&
|
||||
VALUE_KINDS.has(context.getNodeById?.(ref.fromNodeId)?.kind ?? '')) return null;
|
||||
return result ? retargetSelfOverload(result, ref, context) : result;
|
||||
}
|
||||
|
||||
/** Node kinds that hold a value rather than run code. */
|
||||
const VALUE_KINDS: ReadonlySet<string> = new Set(['variable', 'constant', 'field', 'property']);
|
||||
|
||||
/** Languages whose methods overload by arity. */
|
||||
const OVERLOADING_LANGUAGES: ReadonlySet<string> = new Set(['csharp', 'java', 'kotlin', 'swift', 'cpp', 'scala', 'dart', 'vbnet']);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user