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:
Colby Mchenry
2026-09-30 17:13:27 +00:00
committed by GitHub
parent de74c050bc
commit aa0c73c416
3 changed files with 187 additions and 0 deletions
+1
View File
@@ -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.
+130
View File
@@ -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']);
});
});
+56
View File
@@ -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;