From 7a29580fc1c1240148163c2efbe548ea3f5e69d0 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Wed, 30 Sep 2026 21:26:32 +0300 Subject: [PATCH] fix(resolution): keep library-method calls on untyped receivers off same-named project methods A member call on a receiver of unknown type reached the lone same-named method for an untyped `recv.m` (instance-method 0.7). Standard-library names collected such calls: on Play ~2,500 Scala `map`/`get`/`foreach` calls and ~1,100 Java test-map `put` calls landed on unrelated project methods, and Python `d.setdefault()` on a same-file class's method. The standard-library method lists that already demand receiver evidence for Rust, Go, Kotlin, C# and VB.NET gain Python, Scala and Java sets: such a call now needs the receiver's own name to name the candidate's type. `cls` joins `self`/`this`/`base` as a receiver that is the calling type itself. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/library-method-calls.test.ts | 119 +++++++++++++++++++++++++ src/resolution/name-matcher.ts | 68 ++++++++++++-- 3 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 __tests__/library-method-calls.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8611ba779c..8656621d11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- Standard-library calls on a value of unknown type, such as `options.setdefault(...)` in Python, `m.getOrElse(...)` in Scala or `data.get(...)` on a `var` in Java, no longer link to a project method that merely shares the name. Re-index after upgrading. (#1947) - In TypeScript, an awaited call with type arguments like `await fetchApi("/me")` or `await api.get(url)` now links to the function it calls. Before, the call was dropped, so typed fetch wrappers looked like they had no callers. (#2586) - In React, a component rendered at module scope, in a top-level constant or inside a test's `it(…)` callback, like `createRoot(el).render()` or `render()`, now counts as rendered there, so an app's entry reaches its root component and a component's tests show among its callers. (#2587) - In Python, `import a.b as X` written inside a function now links its calls like `X.f()`, as the same import at module level already did. Thanks @Egrn for the report. (#2600) diff --git a/__tests__/library-method-calls.test.ts b/__tests__/library-method-calls.test.ts new file mode 100644 index 0000000000..c732f979eb --- /dev/null +++ b/__tests__/library-method-calls.test.ts @@ -0,0 +1,119 @@ +/** + * 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 = { + '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 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', +}; + +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']); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 0862d98144..e4d2aea56c 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -5845,6 +5845,60 @@ const CSHARP_STD_METHODS: ReadonlySet = new Set([ /** The same .NET names as VB.NET writes them — in any case. */ const VBNET_STD_METHODS: ReadonlySet = new Set([...CSHARP_STD_METHODS].map((m) => m.toLowerCase())); +/** + * Python's dict / list / set / str / file / `re` methods. On an untyped + * receiver (`options.setdefault(…)`, `d.copy()`) they are the builtin + * type's, never a same-named project method the call happens to share a + * file with. + */ +const PYTHON_STD_METHODS: ReadonlySet = new Set([ + 'get', 'setdefault', 'pop', 'popitem', 'update', 'items', 'keys', 'values', 'copy', 'clear', + 'append', 'extend', 'insert', 'remove', 'index', 'count', 'sort', 'reverse', 'add', 'discard', + 'union', 'intersection', 'difference', 'split', 'rsplit', 'join', 'strip', 'lstrip', 'rstrip', + 'replace', 'startswith', 'endswith', 'lower', 'upper', 'format', 'encode', 'decode', 'find', + 'rfind', 'splitlines', 'read', 'readline', 'readlines', 'write', 'writelines', 'close', 'seek', + 'flush', 'group', 'groups', 'match', 'search', 'sub', 'findall', 'fullmatch', +]); + +/** + * Scala's collection / Option / Future methods and the `java.*` calls Scala + * code makes. On Play, ~2,500 `map` / `get` / `foreach` calls on untyped + * values landed on unrelated project methods. + */ +const SCALA_STD_METHODS: ReadonlySet = new Set([ + 'map', 'flatMap', 'filter', 'filterNot', 'foreach', 'fold', 'foldLeft', 'foldRight', 'reduce', + 'collect', 'collectFirst', 'find', 'exists', 'forall', 'count', 'get', 'getOrElse', 'orElse', + 'orNull', 'isEmpty', 'nonEmpty', 'isDefined', 'contains', 'headOption', 'head', 'tail', 'last', + 'lastOption', 'take', 'drop', 'groupBy', 'sortBy', 'sorted', 'mkString', 'toList', 'toSeq', + 'toSet', 'toMap', 'toVector', 'toArray', 'zip', 'zipWithIndex', 'size', 'length', 'update', + 'updated', 'recover', 'recoverWith', 'transform', 'andThen', 'compose', 'onComplete', 'asScala', + 'asJava', 'getOrElseUpdate', 'put', 'remove', 'add', 'append', 'replace', 'split', 'trim', + 'startsWith', 'endsWith', 'format', 'equals', 'hashCode', 'toString', 'set', 'build', 'execute', + 'of', 'encode', 'decode', 'max', 'min', +]); + +/** + * `java.lang` / `java.util` / streams and the Android framework calls Java + * code makes on values whose type the resolver cannot see — a lambda + * parameter, a `var`, a call result, an inherited field. On Play ~1,100 + * test-map `put` calls landed on a test class's `RejectingMap::put`. Names + * Java projects commonly declare themselves (`post`, `of`, `apply`, `run`, + * `start`, `getId`) stay out. + */ +const JAVA_STD_METHODS: ReadonlySet = new Set([ + 'equals', 'hashCode', 'toString', 'getClass', 'compareTo', + 'get', 'put', 'add', 'remove', 'contains', 'containsKey', 'containsValue', 'size', 'isEmpty', + 'clear', 'iterator', 'hasNext', 'next', 'stream', 'forEach', 'keySet', 'values', 'entrySet', + 'addAll', 'removeAll', 'putAll', 'putIfAbsent', 'getOrDefault', 'computeIfAbsent', 'indexOf', + 'toArray', 'subList', + 'length', 'charAt', 'substring', 'trim', 'split', 'startsWith', 'endsWith', 'equalsIgnoreCase', + 'toLowerCase', 'toUpperCase', 'replace', 'format', 'valueOf', + 'map', 'filter', 'orElse', 'orElseGet', 'orElseThrow', 'ifPresent', 'isPresent', 'collect', + 'findFirst', 'anyMatch', 'allMatch', + 'postDelayed', 'removeCallbacks', 'obtainMessage', 'getResources', 'getSystemService', + 'findViewById', 'setVisibility', 'startActivity', +]); + /** * A request handler the web framework dispatches to: a Django / DRF / Flask * view's `get` / `post` / …, a controller's `index` / `store` / `update` / @@ -5891,6 +5945,9 @@ function stdMethodNames(language: string): ReadonlySet | null { case 'csharp': return CSHARP_STD_METHODS; case 'vbnet': return VBNET_STD_METHODS; case 'dart': return DART_STD_METHODS; + case 'python': return PYTHON_STD_METHODS; + case 'scala': return SCALA_STD_METHODS; + case 'java': return JAVA_STD_METHODS; default: return null; } } @@ -19734,11 +19791,12 @@ export function matchMethodCall( // `json` of its `MockedResponse`. !isUnnamedTestDouble(targetMethods[0]!, objectOrClass!, ref, context) && !((ref.language === 'lua' || ref.language === 'luau') && isLuaLibraryCall(objectOrClass!, methodName!, ref, targetMethods[0]!)) && - // Rust / Go / Kotlin / C# / VB.NET: a standard-library method name on - // an untyped receiver (`sym.map(…)`, `w.Header().Get(…)`, - // `reader.Value.ToString()`) is the library type's. + // Rust / Go / Kotlin / C# / VB.NET / Python / Scala / Java: a + // standard-library method name on an untyped receiver (`sym.map(…)`, + // `w.Header().Get(…)`, `reader.Value.ToString()`, + // `options.setdefault(…)`) is the library type's. !(isStdMethodName(ref.language, methodName!) && - !/^(?:self|Self|this|base)$/.test(objectOrClass!) && !receiverNamesOwner(receiverLink(objectOrClass!), targetMethods[0]!, context)) && + !/^(?:self|Self|this|base|cls)$/.test(objectOrClass!) && !receiverNamesOwner(receiverLink(objectOrClass!), targetMethods[0]!, context)) && !(UNTYPED_RECEIVER_LANGUAGES.has(ref.language) && !/^(?:self|self\.class|this|super|weak_?self|strong_?self)$/i.test(objectOrClass!) && !sharesReceiverWord(objectOrClass!, targetMethods[0]!) && !(ref.language === 'objc' && objcReceiverReaches(objectOrClass!, targetMethods[0]!, context)) && @@ -19770,7 +19828,7 @@ export function matchMethodCall( // Same-file candidates first, so a score tie (`score > bestScore` keeps // the first seen) resolves to the call site's own file rather than the // first-indexed duplicate (#1079). - const std = isStdMethodName(ref.language, methodName!) && !/^(?:self|Self|this|base)$/.test(objectOrClass!); + const std = isStdMethodName(ref.language, methodName!) && !/^(?:self|Self|this|base|cls)$/.test(objectOrClass!); for (const method of preferCallSiteFile(withinCppTranslationUnit(targetMethods, ref, context), ref.filePath)) { if (std && !receiverNamesOwner(receiverLink(objectOrClass!), method, context)) continue; // The owner type's own name — not its namespace (`eShop.ClientApp…`