From c20283e2ce8b07f5842bb6cd7c8dcfbc320536ab Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Wed, 30 Sep 2026 22:21:17 +0000 Subject: [PATCH] fix(resolution): a call to an overload of the calling method reaches it (#2212) A method that delegates to another overload of itself - `toInstant(instant)` returning `toInstant(instant, Instant.EPOCH)`, `DeserializeXNode(value)` returning `DeserializeXNode(value, null)` - bound to itself, since every overload shares the name: the fuller overload never listed its convenience overloads among its callers, and its impact missed them. When a name match lands on the calling method itself, in a language that overloads by arity (C#, Java, Kotlin, Swift, C++, Scala, Dart, VB.NET), the call's argument count is read and compared with the method's parameters (defaults and varargs / `params` / `...` counted); if they cannot take it, the one same-owner overload they fit is the target. Real recursion (`depth(n - 1)`) keeps its self-edge. Self-loops -> overload edges: commons-lang 637 -> 457, Newtonsoft 303 -> 196, halo 173 -> 116, okhttp 248 -> 208, Alamofire 147 -> 120, gson 74 -> 55, AutoMapper 71 -> 57 (edge counts unchanged; overloads share a qualified name, so only the target moves). Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/self-overload-calls.test.ts | 82 +++++++++++++++++++++++++++ src/resolution/name-matcher.ts | 39 ++++++++++++- 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 __tests__/self-overload-calls.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 58a270a00..b0370b308 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 +- A method that calls another overload of itself now links to that overload rather than to itself. An example is `toInstant(instant)` returning `toInstant(instant, Instant.EPOCH)`. The fuller overload now lists its convenience overloads among its callers, so its impact includes them. This applies to Java, C#, Kotlin, Swift, C++, Scala, Dart and VB.NET. Real recursion is unchanged. - In a project with a React front end, the React rules for hooks (`use…`) and contexts (`…Context`, `…Provider`) no longer apply to backend code. In halo, a Spring app with a React UI, Java imports such as `SecurityContext` and `ObjectProvider` had been handled as React contexts. - In a project with a Vue front end, the backend's calls no longer land on Vue components. mealie's Python `QueryFilterBuilder(…)` had been linking to the `QueryFilterBuilder.vue` component. The Vue rules (compiler macros, Nuxt auto-imports, components by name) now apply only to the app's scripts. - Production code no longer links to a same-named symbol inside a test suite, since tests aren't built into the program. Before, typeorm's `Record` types reached a test entity called `Record`, and okhttp's sample `@Override` annotations reached a test's nested `Override` class. Test-support code a project ships, such as a `testing/` folder or a `*-test` module, stays in reach. diff --git a/__tests__/self-overload-calls.test.ts b/__tests__/self-overload-calls.test.ts new file mode 100644 index 000000000..c0b121c5b --- /dev/null +++ b/__tests__/self-overload-calls.test.ts @@ -0,0 +1,82 @@ +/** + * A method calling its own name with arguments its own parameters cannot + * take calls another overload — `toInstant(instant)` delegating to + * `toInstant(instant, Instant.EPOCH)` — not itself: the self-edge hid the + * convenience overloads from the full overload's callers (commons-lang had + * 180 such, Newtonsoft 107). + */ +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-self-overload-')); + const files: Record = { + 'src/main/java/app/Instants.java': `package app; + +public class Instants { + public static Object toInstant(final Object instant) { + return toInstant(instant, null); + } + + public static Object toInstant(final Object instant, final Object defaultInstant) { + return instant != null ? instant : defaultInstant; + } + + public static int depth(final int n) { + return n <= 0 ? 0 : depth(n - 1); + } +} +`, + 'src/Json/Convert.cs': `namespace App +{ + public static class Convert + { + public static string DeserializeXNode(string value) + { + return DeserializeXNode(value, null); + } + + public static string DeserializeXNode(string value, string root) + { + return value + root; + } + } +} +`, + }; + 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 }); +}); + +/** `callerLine -> targetLine` of the calls between same-named methods of a file. */ +const selfNamed = (file: string, name: string) => { + const nodes = cg.getNodesInFile(file).filter((n) => n.name === name); + const ids = new Set(nodes.map((n) => n.id)); + return cg.getOutgoingEdgesFrom([...ids]).filter((e) => e.kind === 'calls' && ids.has(e.target)) + .map((e) => `${cg.getNode(e.source)!.startLine} -> ${cg.getNode(e.target)!.startLine}`); +}; + +describe('a method calling its own name', () => { + it('Java: reaches the overload the arguments fit; real recursion stays', () => { + expect(selfNamed('src/main/java/app/Instants.java', 'toInstant')).toEqual(['4 -> 8']); + expect(selfNamed('src/main/java/app/Instants.java', 'depth')).toEqual(['12 -> 12']); + }); + + it('C#: reaches the overload the arguments fit', () => { + expect(selfNamed('src/Json/Convert.cs', 'DeserializeXNode')).toEqual(['5 -> 10']); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 866cdb41d..e91a20ec4 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -7962,7 +7962,44 @@ export function matchReference( ref: UnresolvedRef, context: ResolutionContext ): ResolvedRef | null { - return gateLanguageMatch(matchReferenceInner(ref, context), ref, context); + const result = gateLanguageMatch(matchReferenceInner(ref, context), ref, context); + return result ? retargetSelfOverload(result, ref, context) : result; +} + +/** Languages whose methods overload by arity. */ +const OVERLOADING_LANGUAGES: ReadonlySet = new Set(['csharp', 'java', 'kotlin', 'swift', 'cpp', 'scala', 'dart', 'vbnet']); + +/** + * A call a method makes to its own name, with arguments its own parameters + * cannot take, is to another overload of it: Newtonsoft's + * `DeserializeXNode(value)` body `return DeserializeXNode(value, null);` + * bound to itself, so the two-argument overload never saw the one-argument + * one among its callers. The same-owner overload the argument count fits. + */ +function retargetSelfOverload(result: ResolvedRef, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef { + if (result.targetNodeId !== ref.fromNodeId || ref.referenceKind !== 'calls' || !OVERLOADING_LANGUAGES.has(ref.language)) return result; + const self = context.getNodeById?.(ref.fromNodeId); + if (!self || (self.kind !== 'method' && self.kind !== 'function')) return result; + const name = self.name; + const args = cppParenListAfter(ref.filePath, ref.line, Math.max(0, ref.column), name, context); + if (args === null) return result; + const argc = args.trim() === '' ? 0 : splitCppTopLevel(args).length; + const arity = (n: Node): { min: number; max: number } | null => { + const list = cppParenListAfter(n.filePath, n.startLine, 0, name, context); + if (list === null) return null; + const params = splitCppTopLevel(list).filter((p) => p !== '' && p !== 'void'); + const variadic = params.some((p) => /\.\.\.|\bparams\s|\bvararg\s/.test(p.replace(/<[^<>]*>/g, ''))); + const min = params.filter((p) => !/=/.test(p) && !/\.\.\.|\bparams\s|\bvararg\s/.test(p.replace(/<[^<>]*>/g, ''))).length; + return { min, max: variadic ? Infinity : params.length }; + }; + const own = arity(self); + if (!own || (argc >= own.min && argc <= own.max)) return result; + const owner = self.qualifiedName.slice(0, Math.max(0, self.qualifiedName.lastIndexOf('::'))); + const fits = context.getNodesInFile(self.filePath).filter((n) => + n.id !== self.id && n.name === name && (n.kind === 'method' || n.kind === 'function') && + n.qualifiedName.slice(0, Math.max(0, n.qualifiedName.lastIndexOf('::'))) === owner && + ((a) => a !== null && argc >= a.min && argc <= a.max)(arity(n))); + return fits.length === 1 ? { ...result, targetNodeId: fits[0]!.id } : result; } function matchReferenceInner(