Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In JavaScript and TypeScript, a default import now links to what the module's `export default` statement exports, wherever the statement sits in the file. Before, a module that declares its default, like `export default function Vans() {…}` or `export default class Store {…}`, had its default import linked to the first function it exports instead: in a React Router data-router app, where a page exports its `loader` or `action` above the page component, `import Vans, { loader as vansLoader } from './pages/Vans'` made the route `/vans` and every use of `Vans` lead to the loader. A component file that also exports a styled or memoized component no longer hands its default import to that component, and an anonymous default, like `export default function () {…}` or `export default () => …`, is no longer linked to a function exported beside it or nested inside it. A module loaded with `require('./x').default` is read the same way. Re-index JavaScript and TypeScript projects after upgrading.
- In React and Vue, a tag for a component the file imports from a package, like `<Button>` from antd, `<Typography>` from `@mui/material/Typography`, `<Link>` from react-router-dom or `<Card>` from ant-design-vue, no longer links to a component, function or class in your repository that only shares its name. Before, the tag linked to whatever the repository had by that name, so `codegraph_explore`, callers and impact followed renders that never happen: in SigNoz, antd's `<Button>` tags showed up as callers of a styled link on its 404 page, and in Material UI dashboards like mantis and berry, `<Typography>` and `<Button>` tags showed up as callers of the theme's style overrides. A package here is one that a `package.json` above the file lists and that no file in the repository answers to. A tag imported through a path alias like `components/Modal`, from a workspace package of the repository, or under a name no `package.json` lists still links as before. Re-index React and Vue projects after upgrading.
- In React with TypeScript, a component that renders a generic component and passes it type arguments, like outline's `<PaginatedList<Document> items={…} />` or excalidraw's `<DropdownMenuItemContentRadio<"contain" | "overlap"> …>`, now links to the component it renders. Before, a tag followed by type arguments was skipped, so the generic component looked unused by the screens that render it, and `codegraph_explore`'s call paths, callers and impact stopped short of it. Re-index React projects after upgrading.
- 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)

## [1.6.2] - 2026-10-03

Expand Down
119 changes: 119 additions & 0 deletions __tests__/library-method-calls.test.ts
Original file line number Diff line number Diff line change
@@ -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<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',
};

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']);
});
});
68 changes: 63 additions & 5 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5079,6 +5079,60 @@ const CSHARP_STD_METHODS: ReadonlySet<string> = new Set([
/** The same .NET names as VB.NET writes them — in any case. */
const VBNET_STD_METHODS: ReadonlySet<string> = 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<string> = 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<string> = 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<string> = 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` /
Expand Down Expand Up @@ -5125,6 +5179,9 @@ function stdMethodNames(language: string): ReadonlySet<string> | 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;
}
}
Expand Down Expand Up @@ -11815,11 +11872,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)) &&
Expand All @@ -11845,7 +11903,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(targetMethods, ref.filePath)) {
if (std && !receiverNamesOwner(receiverLink(objectOrClass!), method, context)) continue;
// The owner type's own name — not its namespace (`eShop.ClientApp…`
Expand Down