mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
fix(resolution): a call through a class name reaches the class method it inherits (#2180)
Strategy 1 (a receiver naming a class) looked only in the class's own
file, so an inherited class method fell to the name-word guess: Horse's
THorse.Get('/ping', …) (THorseCore's, three class(…) heads up) went to a
route-group interface's Get 120 times.
When the class itself declares no such method, walk the supertypes its
declaration heads name (Pascal class(…), Python bases, Ruby <, PHP/TS/JS
extends, and the existing Java-family / Dart / Swift / ObjC / Scala
readers), nearest first, up to five levels.
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 call through a class name, like `THorse.Get(…)`, `MyView.as_view()` or `ColorChoices.values()`, now reaches the class method that class inherits. The inheritance is read from the class declarations themselves, in Pascal, Python, Ruby, PHP, TypeScript/JavaScript and the Java family. Before, such a call was resolved by guessing from the name. On Horse, `THorse.Get('/ping', …)` used to land on a route-group interface's `Get` 120 times. Re-index after upgrading.
|
||||
- In Objective-C, `[super init]` and other messages to `super` now reach the superclass's method, never the sending class's own override. This also works where the index lost the `@implementation`'s range. On SDWebImage, `NSObject` subclasses' `[super init]` used to land on `SDDiskCache`'s `init`, and `[self class]` in tests on a proxy class's `class`. On AFNetworking, a serializer's `[super encodeWithCoder:]` now reaches its superclass. Re-index after upgrading.
|
||||
- In Solidity, a call with no receiver now reaches a function of the contract around it or of one it inherits. On OpenZeppelin, `_msgSender()` in contracts built on `Context` used to land on `ERC2771Context`'s override 83 times. In Erlang, a call to another module's function now follows the file's `-import(Mod, [f/N])`. On cowboy, `-import(req_SUITE, [do_get/3])` calls used to land on another suite's `do_get/3`. A bare call with no such import no longer links to some other module. Re-index after upgrading.
|
||||
- Calls on a library's own types and modules no longer land on a project method that happens to share the name. In Rust, `Vec::new()`, `String::from(…)` and module paths like `task::spawn(…)` stay the standard library's. A later link of a multi-line Rust or Go chain now counts as a chained call, named after the chain's first line. In Go, `w.Header().Get(…)` is net/http's. In R, a plain call like `range(x)` is a function, not an object's method. In JavaScript, `response.text()` / `.json()` is fetch's. On tokio, `task::spawn(…)` used to land on `TaskTracker::spawn` 300 times. On ggplot2, `range(…)` used to land on a coordinate system's `range` method. Re-index after upgrading.
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* A call through a class name reaches a class method the class inherits,
|
||||
* read from its own and its ancestors' declaration heads, before any guess by
|
||||
* name: Horse's `THorse.Get('/ping', …)` is THorseCore's (three `class(…)`
|
||||
* heads up), not a route-group interface's `Get`; DRF's `MyView.as_view()` is
|
||||
* APIView's; netbox's `ColorChoices.values()` is ChoiceSet's.
|
||||
*/
|
||||
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';
|
||||
|
||||
let root = '';
|
||||
let cg: CodeGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-inherited-class-method-'));
|
||||
const files: Record<string, string> = {
|
||||
'src/Horse.Core.pas': `unit Horse.Core;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
THorseCore = class
|
||||
public
|
||||
class function Get(const APath: string): THorseCore;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
class function THorseCore.Get(const APath: string): THorseCore;
|
||||
begin
|
||||
Result := nil;
|
||||
end;
|
||||
|
||||
end.
|
||||
`,
|
||||
'src/Horse.Core.Group.pas': `unit Horse.Core.Group;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
IHorseCoreGroup = interface
|
||||
function Get(const APath: string): IHorseCoreGroup;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
end.
|
||||
`,
|
||||
'src/Horse.pas': `unit Horse;
|
||||
|
||||
interface
|
||||
|
||||
uses Horse.Core;
|
||||
|
||||
type
|
||||
THorseProvider = class(THorseCore)
|
||||
end;
|
||||
|
||||
THorse = class(THorseProvider)
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
end.
|
||||
`,
|
||||
'samples/Console.dpr': `program Console;
|
||||
|
||||
uses Horse;
|
||||
|
||||
begin
|
||||
THorse.Get('/ping');
|
||||
end.
|
||||
`,
|
||||
'app/views.py': `class APIView:
|
||||
@classmethod
|
||||
def as_view(cls):
|
||||
return cls
|
||||
|
||||
|
||||
class GenericAPIView(APIView):
|
||||
pass
|
||||
|
||||
|
||||
class UserList(GenericAPIView):
|
||||
pass
|
||||
|
||||
|
||||
class Router:
|
||||
def as_view(self):
|
||||
return None
|
||||
`,
|
||||
'app/urls.py': `from app.views import UserList
|
||||
|
||||
urlpatterns = [UserList.as_view()]
|
||||
`,
|
||||
};
|
||||
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);
|
||||
}
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cg?.close();
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** `Owner::member` of every call edge out of a file. */
|
||||
function callsFrom(file: string): string[] {
|
||||
const ids = cg.getNodesInFile(file).map((n) => n.id);
|
||||
return cg
|
||||
.getOutgoingEdgesFrom(ids)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => cg.getNode(e.target)!.qualifiedName)
|
||||
.sort();
|
||||
}
|
||||
|
||||
describe('a class method reached through a subclass name', () => {
|
||||
it('Pascal: walks the class(…) heads', () => {
|
||||
expect(callsFrom('samples/Console.dpr').filter((q) => q.endsWith('Get'))).toEqual(['THorseCore::Get']);
|
||||
});
|
||||
|
||||
it('Python: walks the class bases', () => {
|
||||
expect(callsFrom('app/urls.py')).toEqual(['APIView::as_view']);
|
||||
});
|
||||
});
|
||||
@@ -4927,6 +4927,58 @@ function headTypeArguments(cls: Node, sup: string, context: ResolutionContext):
|
||||
return angleArguments(head.slice(at.index)).map((a) => a.replace(/<[\s\S]*$/, '').split('.').pop()!.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* The supertypes a class declaration names in its own head, for the
|
||||
* languages whose heads say it plainly: Pascal `TX = class(TBase, IFoo)`,
|
||||
* Python `class X(Base):`, Ruby `class X < Base`, PHP / TS / JS `extends
|
||||
* Base`, and the Java-family heads.
|
||||
*/
|
||||
function declaredSupertypes(cls: Node, context: ResolutionContext): string[] {
|
||||
switch (cls.language) {
|
||||
case 'java': case 'csharp': case 'kotlin': return classHeadSupertypes(cls, context);
|
||||
case 'dart': return dartSupertypesOf(cls.name, context);
|
||||
case 'swift': return swiftDeclOf(cls.name, context).supers;
|
||||
case 'objc': return objcSupertypesOf(cls.name, context);
|
||||
case 'scala': return scalaSupertypesOf(cls.name, context);
|
||||
default: break;
|
||||
}
|
||||
const lines = context.getFileLines?.(cls.filePath) ?? context.readFile(cls.filePath)?.split(/\r?\n/) ?? [];
|
||||
const head = lines.slice(cls.startLine - 1, cls.startLine + 2).join(' ');
|
||||
const names = (text: string | undefined): string[] =>
|
||||
text ? [...text.matchAll(/([A-Za-z_][\w.:\\]*)/g)].map((m) => m[1]!.split(/::|\.|\\/).pop()!).filter((w) => !/^(?:metaclass|object)$/.test(w)) : [];
|
||||
switch (cls.language) {
|
||||
case 'pascal': return names(/=\s*class\s*\(([^)]*)\)/i.exec(head)?.[1]);
|
||||
case 'python': return names(/\bclass\s+\w+\s*\(([^)]*)\)/.exec(head)?.[1]?.replace(/\w+\s*=\s*[\w.]+/g, ''));
|
||||
case 'ruby': return names(/\bclass\s+[\w:]+\s*<\s*([\w:]+)/.exec(head)?.[1]);
|
||||
case 'php': case 'typescript': case 'tsx': case 'javascript': case 'jsx':
|
||||
return names(/\bextends\s+([\w.\\]+)/.exec(head)?.[1]);
|
||||
default: return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** A method named `name` on a supertype of the given classes, nearest first. */
|
||||
function inheritedClassMethod(classes: Node[], name: string, context: ResolutionContext): Node | null {
|
||||
const seen = new Set<string>(classes.map((c) => c.id));
|
||||
let frontier = classes;
|
||||
for (let depth = 0; depth < 5 && frontier.length > 0; depth++) {
|
||||
const next: Node[] = [];
|
||||
for (const cls of frontier) {
|
||||
for (const sup of declaredSupertypes(cls, context)) {
|
||||
for (const decl of context.getNodesByName(sup)) {
|
||||
if (decl.language !== cls.language || !isMethodOwnerKind(decl) || seen.has(decl.id)) continue;
|
||||
seen.add(decl.id);
|
||||
const method = context.getNodesInFile(decl.filePath).find((n) => n.kind === 'method' && n.name === name &&
|
||||
n.qualifiedName.slice(0, Math.max(0, n.qualifiedName.lastIndexOf('::'))).split(/::|\./).pop() === decl.name);
|
||||
if (method) return method;
|
||||
next.push(decl);
|
||||
}
|
||||
}
|
||||
}
|
||||
frontier = next;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The simple names a Java / C# / Kotlin class declaration's head extends or implements. */
|
||||
function classHeadSupertypes(cls: Node, context: ResolutionContext): string[] {
|
||||
const lines = context.getFileLines?.(cls.filePath) ?? context.readFile(cls.filePath)?.split(/\r?\n/) ?? [];
|
||||
@@ -5749,6 +5801,10 @@ export function matchMethodCall(
|
||||
};
|
||||
}
|
||||
}
|
||||
// A class method the named class inherits — Horse's `THorse.Get(…)` is
|
||||
// THorseCore's, three `class(…)` heads up — before any guess by name.
|
||||
const inherited = inheritedClassMethod(classCandidates.filter((c) => c.language === ref.language), methodName!, context);
|
||||
if (inherited) return { original: ref, targetNodeId: inherited.id, confidence: 0.8, resolvedBy: 'qualified-name' };
|
||||
return null;
|
||||
});
|
||||
if (strat1) return strat1;
|
||||
|
||||
Reference in New Issue
Block a user