mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 00:01:39 +08:00
Root cause: when nothing types a call's receiver, matchMethodCall's last
strategy links `recv.m()` to the one project method named `m` (0.7), or to
the best receiver-word match among several. A standard-library call on a
plain value - `options.setdefault(...)` on a dict, `m.getOrElse(...)` on an
Option, `data.put(...)` on a map in a `var` - then linked to an unrelated
project method of that name. Go, Rust, Kotlin, C#, VB.NET and Dart already
keep such a guess only when the receiver names the method's owner
(stdMethodNames); Python, Scala and Java had no list.
Fix (contributor): Python, Scala and Java standard-library method lists in
stdMethodNames. Ours: the PR also exempted a receiver named `cls` from the
rule; both extractors already strip `cls.` from Python calls, so it never
applied there, while in C#, Dart, Rust and VB.NET it let
`var cls = Make(); cls.ToString()` link to an unrelated Money.ToString.
Removed, with a C# test case. CHANGELOG bullet rewritten and moved next to
the Python built-in method bullets.
Validation: library-method-calls.test.ts: 3 of its 5 cases fail on main
(Python build, Scala go, Java untyped), all 5 pass; the C# case fails when
`cls` is put back. Kernel A/B against main 0174f804, 11 repos: 0 added,
0 retargeted, 540 removed. Python: flask 12, netbox 2, django 140, fastapi 0,
all wrong (list/dict/file/re-match calls landing on ListMixin, QueryDict,
HttpRequest, AnonymousUser, _AppCtxGlobals). Java: Minestom 20, spring-ai 86,
all wrong (Map put/computeIfAbsent, reactor Context put, stream(), enum
valueOf). Scala: playframework 98 (4 of them Java), scalachess 31,
http4s 151; 267 wrong, 13 right links lost where a project type has
collection-like methods and the receiver is not named after it (scalachess
Board.put/replace through for-comprehension binds, ByColor through
`type Pockets`; http4s Headers.put; Play Form.fold, Router.orElse). gin and
serilog byte-identical. CPU on playframework, median of 3 interleaved:
71.7 s main, 71.1 s branch. Also settles the Holder.LOOKUP.get and
Mode.ON.equals cases of PR 1949.
Original PR #1947 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
5.0 KiB
TypeScript
134 lines
5.0 KiB
TypeScript
/**
|
|
* A standard-library method called on a receiver of unknown type — Python
|
|
* `options.setdefault(...)`, Scala `m.getOrElse(...)`, Java `m.put(...)` on a
|
|
* `var` — does not bind by name to the one project method that shares the
|
|
* name: the receiver's own words must name the method's type.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { CodeGraph } from '../src';
|
|
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
|
|
|
|
let dir: string;
|
|
let cg: CodeGraph;
|
|
|
|
const files: Record<string, string> = {
|
|
'py/globals.py':
|
|
'class Globals:\n' +
|
|
' def setdefault(self, key, value):\n' +
|
|
' return value\n' +
|
|
' def reset(self):\n' +
|
|
' return self.setdefault("a", 1)\n' +
|
|
'\n' +
|
|
'def build(options):\n' +
|
|
' options.setdefault("x", 1)\n' +
|
|
' return options.copy().setdefault("y", 2)\n',
|
|
'java/com/app/Registry.java':
|
|
'package com.app;\n' +
|
|
'public class Registry {\n' +
|
|
' public static final String KEY = "k";\n' +
|
|
' public Object get(String k) { return null; }\n' +
|
|
' public int size() { return 0; }\n' +
|
|
' public int hash() { return 0; }\n' +
|
|
' public boolean equals(Object o) { return false; }\n' +
|
|
' public String toString() { return "r"; }\n' +
|
|
' int count() { return size(); }\n' +
|
|
' Object own(String k) { return this.get(k); }\n' +
|
|
'}\n',
|
|
'sc/Registry.scala':
|
|
'package app\n' +
|
|
'class Registry {\n' +
|
|
' def getOrElse(d: Int): Int = d\n' +
|
|
'}\n',
|
|
'sc/Use.scala':
|
|
'package app\n' +
|
|
'object Use {\n' +
|
|
' def go(): Int = {\n' +
|
|
' val m = compute()\n' +
|
|
' m.getOrElse(0)\n' +
|
|
' }\n' +
|
|
' def compute() = Option(1)\n' +
|
|
' def viaName(): Int = { val registry = make(); registry.getOrElse(0) }\n' +
|
|
' def make() = new Registry\n' +
|
|
'}\n',
|
|
'java/com/app/Use.java':
|
|
'package com.app;\n' +
|
|
'import java.util.*;\n' +
|
|
'public class Use {\n' +
|
|
' private Registry registry;\n' +
|
|
' Object untyped(String a) {\n' +
|
|
' var data = load();\n' +
|
|
' data.put(a, 1);\n' +
|
|
' return data.get(a);\n' +
|
|
' }\n' +
|
|
' Map<String, Object> load() { return null; }\n' +
|
|
' Object typed(String k) { return registry.get(k); }\n' +
|
|
' int named() { var userRegistry = make(); return userRegistry.size(); }\n' +
|
|
' Registry make() { return null; }\n' +
|
|
'}\n',
|
|
'cs/App/Money.cs':
|
|
'namespace App {\n' +
|
|
' public class Money {\n' +
|
|
' public override string ToString() { return "m"; }\n' +
|
|
' }\n' +
|
|
' public class Use {\n' +
|
|
' object Make() { return null; }\n' +
|
|
' public string Look() { var cls = Make(); return cls.ToString(); }\n' +
|
|
' }\n' +
|
|
'}\n',
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
await initGrammars();
|
|
await loadAllGrammars();
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-libmethods-'));
|
|
for (const [rel, text] of Object.entries(files)) {
|
|
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(dir, rel), text);
|
|
}
|
|
cg = CodeGraph.initSync(dir);
|
|
await cg.indexAll();
|
|
});
|
|
|
|
afterAll(() => {
|
|
cg.destroy();
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
const node = (name: string, file: string) =>
|
|
cg.getNodesByName(name).find((n) => n.filePath.endsWith(file) && (n.kind === 'function' || n.kind === 'method'))!;
|
|
const calleesOf = (name: string, file: string) =>
|
|
cg.getCallees(node(name, file).id)
|
|
.filter(({ edge }) => edge.kind === 'calls')
|
|
.map(({ node: n }) => n.qualifiedName.split('::').slice(-2).join('::'))
|
|
.sort();
|
|
|
|
describe('library-method calls on untyped receivers', () => {
|
|
it('Python: `options.setdefault()` declines; `self.setdefault()` keeps its own method', () => {
|
|
expect(calleesOf('build', 'globals.py')).toEqual([]);
|
|
expect(calleesOf('reset', 'globals.py')).toEqual(['Globals::setdefault']);
|
|
});
|
|
|
|
it('Scala: `m.getOrElse()` on an untyped value declines; a receiver named after the type keeps its method', () => {
|
|
expect(calleesOf('go', 'Use.scala')).toEqual(['Use::compute']);
|
|
expect(calleesOf('viaName', 'Use.scala')).toEqual(['Registry::getOrElse', 'Use::make']);
|
|
});
|
|
|
|
it('Java: `data.put()` / `data.get()` on an untyped `var` do not bind to a lone project method', () => {
|
|
expect(calleesOf('untyped', 'Use.java')).toEqual(['Use::load']);
|
|
});
|
|
|
|
it('Java: a typed field, an own-class call and a receiver named after the type keep their method', () => {
|
|
expect(calleesOf('typed', 'Use.java')).toEqual(['Registry::get']);
|
|
expect(calleesOf('count', 'Registry.java')).toEqual(['Registry::size']);
|
|
expect(calleesOf('own', 'Registry.java')).toEqual(['Registry::get']);
|
|
expect(calleesOf('named', 'Use.java')).toEqual(['Registry::size', 'Use::make']);
|
|
});
|
|
|
|
it('C#: a receiver named `cls` is a value like any other, not the class itself', () => {
|
|
expect(calleesOf('Look', 'Money.cs')).toEqual(['Use::Make']);
|
|
});
|
|
});
|