From 0bcdb5dd61424c05f6812224fd5dfa2c6435a9a0 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Wed, 30 Sep 2026 21:33:29 +0300 Subject: [PATCH 1/2] fix(scala): resolve Outer.Inner(...) apply to the Scala type, never a guessed Java constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Scala `Outer.Inner(args)` call (case-class / companion apply) misses the qualified-name lookup — Scala nodes are named `Outer::Inner`, the ref `Outer.Inner` — and fell through to matchMethodCall's Strategy 3. With no Scala method of that name, Strategy 3 widens to every language and picks by receiver-word overlap, so `WebSocket.Accepted(...)` or `ApplicationLoader.Context(...)` landed on a same-worded Java constructor (`play.mvc::WebSocket::Accepted::Accepted`). A second route to the same target: in `def f(): T = T.Inner(x)` the local-declaration pattern reads the return annotation as `T`'s type (`Inner`), and resolveMethodOnType hands back the Java `Inner::Inner` constructor. - Before Strategy 3, a Scala call with a capitalized member resolves to the Scala type `Outer::Inner` when it is unambiguous (every match in one file — class + companion — or one in the call site's file). The calls->class edge is stored as `instantiates`, like a bare `Foo(x)` apply. - The typed-receiver path no longer hands a Scala ref another language's constructor; a Java class is constructed from Scala via the import / `new` paths, not this guess. Real Java static / instance methods are still offered. (Strategy 3's cross-language pick of that constructor is already ruled out on main.) Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/resolution.test.ts | 87 ++++++++++++++++++++++++++++++++++ src/resolution/name-matcher.ts | 31 +++++++++++- 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8611ba779..cc06fa2f2 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 Scala `Outer.Inner(...)` call now reaches the Scala case class it builds, not a Java constructor. A companion `apply` such as `RemoteNode.Obfuscated("x")` was linked to an unrelated Java class's constructor that merely shared the words in its name; it now links to the Scala type of that name, and a Java constructor is never guessed that way. - 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__/resolution.test.ts b/__tests__/resolution.test.ts index 14b5198f4..15639947c 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -5194,6 +5194,93 @@ object Main { // Bar has no onlyOther() — must not mis-attach to the same-named Other::onlyOther. expect(callerNamesOf('Other::onlyOther')).toEqual([]); }); + + describe('Scala Outer.Inner(args) apply vs a Java constructor', () => { + function write(rel: string, body: string): void { + fs.mkdirSync(path.dirname(path.join(tempDir, rel)), { recursive: true }); + fs.writeFileSync(path.join(tempDir, rel), body); + } + function edgesFrom(name: string): { kind: string; qn: string; lang: string }[] { + const from = cg.getNodesByKind('method').find((n) => n.name === name); + if (!from) return []; + return cg + .getOutgoingEdges(from.id) + .filter((e) => e.kind !== 'contains' && e.kind !== 'references') + .map((e) => { + const t = cg.getNode(e.target)!; + return { kind: e.kind, qn: t.qualifiedName, lang: t.language }; + }); + } + + beforeEach(() => { + write('java/com/acme/http/WebSocket.java', `package com.acme.http; +public abstract class WebSocket { + public static WebSocket fromString(String s) { return null; } + public static class Accepted { + public Accepted(String flow) {} + } +} +`); + // A second Java class sharing the method names, so the receiver-word + // strategy (not a unique-name shortcut) is what decides. + write('java/com/acme/http/Other.java', `package com.acme.http; +public class Other { + public static Other fromString(String s) { return null; } + public static class Accepted { + public Accepted(String flow) {} + } +} +`); + }); + + it('resolves WebSocket.Accepted(...) to the Scala case class, never the Java constructor', async () => { + write('scala/app/WebSocket.scala', `package app +trait WebSocket +object WebSocket { + final case class Accepted[In, Out](flow: String, sub: Option[String]) +} +`); + write('scala/app/Use.scala', `package app +class Use { + def accept(): Unit = { WebSocket.Accepted("f", None) } +} +`); + cg = await CodeGraph.init(tempDir, { index: true }); + const out = edgesFrom('accept'); + expect(out.filter((e) => e.lang === 'java')).toEqual([]); + expect(out).toContainEqual({ kind: 'instantiates', qn: 'WebSocket::Accepted', lang: 'scala' }); + }); + + it('does not take a Java constructor from a return-type annotation read as the receiver type', async () => { + // `def f(): WebSocket = WebSocket.Accepted(...)` — the local-declaration + // pattern reads `WebSocket = WebSocket.Accepted` as the receiver's type. + write('scala/app/WebSocket.scala', `package app +trait WebSocket +object WebSocket { + final case class Accepted(flow: String) extends WebSocket +} +`); + write('scala/app/Use.scala', `package app +class Use { + def accept(flow: String): WebSocket = WebSocket.Accepted(flow) +} +`); + cg = await CodeGraph.init(tempDir, { index: true }); + const out = edgesFrom('accept'); + expect(out.filter((e) => e.lang === 'java')).toEqual([]); + expect(out).toContainEqual({ kind: 'instantiates', qn: 'WebSocket::Accepted', lang: 'scala' }); + }); + + it('still resolves a Scala call to a real Java static method', async () => { + write('scala/app/Use.scala', `package app +class Use { + def load(): Unit = { WebSocket.fromString("y") } +} +`); + cg = await CodeGraph.init(tempDir, { index: true }); + expect(edgesFrom('load')).toContainEqual({ kind: 'calls', qn: 'com.acme.http::WebSocket::fromString', lang: 'java' }); + }); + }); }); describe('Scala companion object vs extends resolution', () => { diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 0862d9814..0f935af81 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -19281,7 +19281,11 @@ export function matchMethodCall( if (!target || (target.qualifiedName.startsWith(`${inferredType}::`) && target.filePath !== awaited.filePath)) return null; return { ...typedMatch, original: ref }; } - return typedMatch; + // A Scala call never lands on another language's constructor here: a + // `def f(): T = T.Inner(...)` return annotation reads as `T`'s type and + // hands `Inner::Inner` over. Fall through to the Scala type lookup. + const target = ref.language === 'scala' ? context.getNodeById?.(typedMatch.targetNodeId) : null; + if (!target || !isOtherLanguageConstructor(target, ref.language)) return typedMatch; } if (awaited) return null; // A known JS/TS builtin receiver is external when it has no project @@ -19643,6 +19647,22 @@ export function matchMethodCall( if (strat2) return strat2; } + // Scala `Outer.Inner(args)`: a case-class / companion `apply` on the nested + // Scala type `Outer::Inner` (Scala qualified names carry no package). Taken + // only when every match sits in one file (a class and its companion share + // the name), or else from the call site's own file. + if (ref.language === 'scala' && dotMatch && /^[A-Z]/.test(methodName!)) { + const typeQn = `${objectOrClass!.replace(/\./g, '::')}::${methodName}`; + const types = context + .getNodesByQualifiedName(typeQn) + .filter((n) => n.language === 'scala' && (n.kind === 'class' || n.kind === 'trait')); + const oneFile = types.length > 0 && types.every((n) => n.filePath === types[0]!.filePath); + const chosen = oneFile ? types[0] : types.find((n) => n.filePath === ref.filePath); + if (chosen) { + return { original: ref, targetNodeId: chosen.id, confidence: 0.85, resolvedBy: 'qualified-name' }; + } + } + // Strategy 3: Find methods by name across the codebase, match by receiver // name similarity with the containing class. Handles abbreviated variable // names like permissionEngine → PermissionRuleEngine. @@ -23015,6 +23035,15 @@ export function pythonAttrRefResolves(ref: UnresolvedRef, context: ResolutionCon const receiver = dot > 0 ? ref.referenceName.slice(0, dot) : ''; if (!receiver.includes('.')) return false; return !!matchPythonAttrCall(receiver, ref.referenceName.slice(dot + 1), ref, context); +/** + * A constructor node of another language: a method named like its owner + * (`pkg::Outer::Inner::Inner`), which is how the Java/Kotlin extractors record + * an explicit constructor. + */ +function isOtherLanguageConstructor(node: Node, language: string): boolean { + if (node.language === language) return false; + const segs = node.qualifiedName.split('::'); + return segs.length >= 2 && segs[segs.length - 1] === segs[segs.length - 2]; } /** Go builtin/primitive field types that can never carry a project method. */ From bc583081552113bcaaf76ea9900017852674d510 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Sun, 27 Sep 2026 17:31:28 +0300 Subject: [PATCH 2/2] fix(scala): let Outer.Inner(...) reach a nested object now indexed as a module Upstream #2029 indexes a Scala `object` as kind `module`, so the Outer.Inner(...) apply lookup, which accepted only class/trait, stopped resolving a call to a nested object that defines `apply` and left it without an edge. Accept a Scala `module` too, and rank it after a class or trait of the same name so a case class still wins over its companion object whatever the declaration order. Co-Authored-By: Claude Opus 5.5 --- __tests__/resolution.test.ts | 41 ++++++++++++++++++++++++++++++++++ src/resolution/name-matcher.ts | 8 +++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index 15639947c..02b33e88c 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -5280,6 +5280,47 @@ class Use { cg = await CodeGraph.init(tempDir, { index: true }); expect(edgesFrom('load')).toContainEqual({ kind: 'calls', qn: 'com.acme.http::WebSocket::fromString', lang: 'java' }); }); + + it('resolves Outer.Inner(...) to a nested Scala object that defines apply', async () => { + // A Scala `object` is indexed as a `module`, not a class. + write('scala/app/WebSocket.scala', `package app +object WebSocket { + object Accepted { + def apply(flow: String): String = flow + } +} +`); + write('scala/app/Use.scala', `package app +class Use { + def accept(): Unit = { WebSocket.Accepted("f") } +} +`); + cg = await CodeGraph.init(tempDir, { index: true }); + const out = edgesFrom('accept'); + expect(out.filter((e) => e.lang === 'java')).toEqual([]); + expect(out).toContainEqual({ kind: 'calls', qn: 'WebSocket::Accepted', lang: 'scala' }); + }); + + it('prefers the case class over its companion object declared first', async () => { + write('scala/app/WebSocket.scala', `package app +object WebSocket { + object Accepted { + def apply(): Accepted = Accepted("default") + } + final case class Accepted(flow: String) +} +`); + write('scala/app/Use.scala', `package app +class Use { + def accept(): Unit = { WebSocket.Accepted("f") } +} +`); + cg = await CodeGraph.init(tempDir, { index: true }); + const out = edgesFrom('accept'); + expect(out.filter((e) => e.lang === 'java')).toEqual([]); + expect(out).toContainEqual({ kind: 'instantiates', qn: 'WebSocket::Accepted', lang: 'scala' }); + expect(out).not.toContainEqual({ kind: 'calls', qn: 'WebSocket::Accepted', lang: 'scala' }); + }); }); }); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 0f935af81..df6984a7f 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -19650,12 +19650,14 @@ export function matchMethodCall( // Scala `Outer.Inner(args)`: a case-class / companion `apply` on the nested // Scala type `Outer::Inner` (Scala qualified names carry no package). Taken // only when every match sits in one file (a class and its companion share - // the name), or else from the call site's own file. + // the name), or else from the call site's own file. A Scala `object` is a + // `module`; a class / trait of the same name is preferred over it. if (ref.language === 'scala' && dotMatch && /^[A-Z]/.test(methodName!)) { const typeQn = `${objectOrClass!.replace(/\./g, '::')}::${methodName}`; const types = context .getNodesByQualifiedName(typeQn) - .filter((n) => n.language === 'scala' && (n.kind === 'class' || n.kind === 'trait')); + .filter((n) => n.language === 'scala' && (n.kind === 'class' || n.kind === 'trait' || n.kind === 'module')) + .sort((a, b) => Number(a.kind === 'module') - Number(b.kind === 'module')); const oneFile = types.length > 0 && types.every((n) => n.filePath === types[0]!.filePath); const chosen = oneFile ? types[0] : types.find((n) => n.filePath === ref.filePath); if (chosen) { @@ -23035,6 +23037,8 @@ export function pythonAttrRefResolves(ref: UnresolvedRef, context: ResolutionCon const receiver = dot > 0 ? ref.referenceName.slice(0, dot) : ''; if (!receiver.includes('.')) return false; return !!matchPythonAttrCall(receiver, ref.referenceName.slice(dot + 1), ref, context); +} + /** * A constructor node of another language: a method named like its owner * (`pkg::Outer::Inner::Inner`), which is how the Java/Kotlin extractors record