fix(scala): an Outer.Inner(...) call reaches the Scala case class or object it builds

Scala qualified names carry no package (Outer::Inner), so a call written
Outer.Inner(args), a case-class or companion apply, found no type by its
written name. It fell through to the name strategies, and under a return
type annotation (def f(): WebSocket = WebSocket.Accepted(flow)) the
local-declaration reader took WebSocket for the receiver's type and handed
over an unrelated Java constructor, Other::Accepted::Accepted.

Before the receiver-word strategy, a Scala call whose member is
capitalized now resolves to the Scala type Outer::Inner when that is
unambiguous: every match in one file (a class and its companion), or one
in the call's own file; a class or trait wins over a same-named object.
It is recorded as instantiates for a class, as a call for an object. The
typed-receiver path no longer hands a Scala call another language's
constructor. Real Java static methods are still reached from Scala.

Validation:
- resolution.test.ts: 5 cases, 4 red on main; the fifth (a Scala call
  to a real Java static) passes on both and guards the Java side.
- A/B against main 81559dcc (with #1950 and #1936): scalachess +77/-0,
  http4s +371/-1 (the one a retarget from a wrong defaults::Host to the
  Origin.Host case class), playframework +291/-1 (the one a retarget:
  Writeable's HttpEntity.Strict(...) from the Java play.http constructor
  to the Scala case class), play-samples +2 (two case-class applies #1950
  unlinked from Java constructors); CMAK, click and gin identical. Every
  addition is a Scala case-class or object apply; sampled all right
  (Uci.Move, Header.Raw, H2Frame.Data, RemoteNode.Ip,
  ApplicationLoader.Context, Request.Connection over headers.Connection).
- Cost (http4s, median of 3 interleaved fresh indexes, on main
  69af19ac): wall 8.6 s on main vs 7.8 s, process CPU 27.5 s vs
  26.2 s, within noise. The A/B gave the same edges on both bases.

Original PR #1951 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
danusha2345
2026-10-27 15:59:08 +00:00
committed by Colby Mchenry
co-authored by Claude Opus 5.5
parent 81559dcc37
commit b6379e6d5e
3 changed files with 163 additions and 1 deletions
+1
View File
@@ -225,6 +225,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In TypeScript and JavaScript, a class in a `.tsx` or `.jsx` file now links its base class's and interfaces' methods to its overrides, as a class in a `.ts` or `.js` file already did. `codegraph_explore`'s call path, callers and impact now follow a call through the base into the override, like outline's `BaseEmail.send()` reaching each email's `render()`. This also works across file types, such as an interface in a `.ts` file implemented in a `.tsx` one. Before, these classes had no override links at all. Re-index projects with `.tsx` or `.jsx` files after upgrading. - In TypeScript and JavaScript, a class in a `.tsx` or `.jsx` file now links its base class's and interfaces' methods to its overrides, as a class in a `.ts` or `.js` file already did. `codegraph_explore`'s call path, callers and impact now follow a call through the base into the override, like outline's `BaseEmail.send()` reaching each email's `render()`. This also works across file types, such as an interface in a `.ts` file implemented in a `.tsx` one. Before, these classes had no override links at all. Re-index projects with `.tsx` or `.jsx` files after upgrading.
- In Ruby, a class method, `def self.build` or a `def` inside `class << self`, is now told apart from an instance method of the same name. A call on the class, like `Widget.build`, links to the class method; a call on an instance, like `widget.build` after `widget = Widget.new`, links to the instance method; and a call without a receiver links to the one that matches the method it is written in. Before, both were stored as instance methods under one name, so a call linked to whichever the class declared first: in a service object with `def self.call` and `def call`, `card.call` on an instance linked to the class method. Re-index Ruby projects after upgrading. - In Ruby, a class method, `def self.build` or a `def` inside `class << self`, is now told apart from an instance method of the same name. A call on the class, like `Widget.build`, links to the class method; a call on an instance, like `widget.build` after `widget = Widget.new`, links to the instance method; and a call without a receiver links to the one that matches the method it is written in. Before, both were stored as instance methods under one name, so a call linked to whichever the class declared first: in a service object with `def self.call` and `def call`, `card.call` on an instance linked to the class method. Re-index Ruby projects after upgrading.
- In Java, Kotlin, C# and Scala, each overload of an interface or base-class method now links only to the overrides with the same parameters. Before, every overload linked to every method of that name in the implementing class, so `ExampleService.putTairValue(String)` also led to `putTairValue(List<String>, double)`, and `codegraph_explore`'s call path, callers and impact went into overloads that call never runs. Where a parameter is a type parameter, like `save(T entity)` implemented as `save(String entity)`, or the class is written in another language than its interface, the number of parameters decides; a method declared once on each side links as before. Re-index Java, Kotlin, C# and Scala projects after upgrading. Thanks @hxj1225 for the report and @danusha2345 for the pull request. (#2663) - In Java, Kotlin, C# and Scala, each overload of an interface or base-class method now links only to the overrides with the same parameters. Before, every overload linked to every method of that name in the implementing class, so `ExampleService.putTairValue(String)` also led to `putTairValue(List<String>, double)`, and `codegraph_explore`'s call path, callers and impact went into overloads that call never runs. Where a parameter is a type parameter, like `save(T entity)` implemented as `save(String entity)`, or the class is written in another language than its interface, the number of parameters decides; a method declared once on each side links as before. Re-index Java, Kotlin, C# and Scala projects after upgrading. Thanks @hxj1225 for the report and @danusha2345 for the pull request. (#2663)
- In Scala, a call like `RemoteNode.Obfuscated("x")` or `Uci.Move(from, to)` now links to the case class or nested object it builds. Before, it linked to nothing, or, under a return type like `: WebSocket`, to an unrelated Java constructor whose name only shared words with it. Re-index Scala projects after upgrading. Thanks @danusha2345.
- In Java and TypeScript, a class that implements or extends a parameterized type, like `implements Store<String>`, `extends Base<Integer>`, an interface's `extends Store<T>` or TypeScript's `implements Store<string>`, is now linked to that type, so its methods get their interface-to-implementation links and `codegraph_explore` follows a call on the interface into the class. Before, such a supertype was read with its type arguments and matched nothing. Re-index Java and TypeScript projects after upgrading. Thanks @danusha2345, and @paiyahooJay for the earlier pull request. (#2705) - In Java and TypeScript, a class that implements or extends a parameterized type, like `implements Store<String>`, `extends Base<Integer>`, an interface's `extends Store<T>` or TypeScript's `implements Store<string>`, is now linked to that type, so its methods get their interface-to-implementation links and `codegraph_explore` follows a call on the interface into the class. Before, such a supertype was read with its type arguments and matched nothing. Re-index Java and TypeScript projects after upgrading. Thanks @danusha2345, and @paiyahooJay for the earlier pull request. (#2705)
- In Java and C#, a method called on a newly constructed object, like `new SqlSessionFactoryBuilder().build(reader)` or `new JsonSerializer().Serialize(writer, value)`, now links to that class's own or inherited method. When several packages or namespaces declare a class of that name, it links to the one the file imports, and to none when the file could mean either. Re-index Java and C# projects after upgrading. - In Java and C#, a method called on a newly constructed object, like `new SqlSessionFactoryBuilder().build(reader)` or `new JsonSerializer().Serialize(writer, value)`, now links to that class's own or inherited method. When several packages or namespaces declare a class of that name, it links to the one the file imports, and to none when the file could mean either. Re-index Java and C# projects after upgrading.
- In Java, a `record` is now indexed as a class, with its methods, its constructors, a field for each component and the accessor each component gets, like `info.remoteAddress()`. Before, a record had no symbol of its own and its methods were filed under the package or the class around it, so a call on a record-typed value linked to nothing or to the wrong method, and records were missing from callers, impact and implementations. A call like `Circle.unit()` on a type that a sealed interface's `permits` list names first now links too. Re-index Java projects after upgrading. Thanks @danusha2345. - In Java, a `record` is now indexed as a class, with its methods, its constructors, a field for each component and the accessor each component gets, like `info.remoteAddress()`. Before, a record had no symbol of its own and its methods were filed under the package or the class around it, so a call on a record-typed value linked to nothing or to the wrong method, and records were missing from callers, impact and implementations. A call like `Circle.unit()` on a type that a sealed interface's `permits` list names first now links too. Re-index Java projects after upgrading. Thanks @danusha2345.
+128
View File
@@ -5225,6 +5225,134 @@ object Main {
// Bar has no onlyOther() — must not mis-attach to the same-named Other::onlyOther. // Bar has no onlyOther() — must not mis-attach to the same-named Other::onlyOther.
expect(callerNamesOf('Other::onlyOther')).toEqual([]); 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' });
});
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' });
});
});
}); });
describe('Scala companion object vs extends resolution', () => { describe('Scala companion object vs extends resolution', () => {
+34 -1
View File
@@ -20103,7 +20103,11 @@ export function matchMethodCall(
if (!target || (target.qualifiedName.startsWith(`${inferredType}::`) && target.filePath !== awaited.filePath)) return null; if (!target || (target.qualifiedName.startsWith(`${inferredType}::`) && target.filePath !== awaited.filePath)) return null;
return { ...typedMatch, original: ref }; 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; if (awaited) return null;
// A known JS/TS builtin receiver is external when it has no project // A known JS/TS builtin receiver is external when it has no project
@@ -20473,6 +20477,24 @@ export function matchMethodCall(
if (strat2) return strat2; 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. 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' || 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) {
return { original: ref, targetNodeId: chosen.id, confidence: 0.85, resolvedBy: 'qualified-name' };
}
}
// Strategy 3: Find methods by name across the codebase, match by receiver // Strategy 3: Find methods by name across the codebase, match by receiver
// name similarity with the containing class. Handles abbreviated variable // name similarity with the containing class. Handles abbreviated variable
// names like permissionEngine → PermissionRuleEngine. // names like permissionEngine → PermissionRuleEngine.
@@ -24136,6 +24158,17 @@ export function pythonAttrRefResolves(ref: UnresolvedRef, context: ResolutionCon
return !!matchPythonAttrCall(receiver, ref.referenceName.slice(dot + 1), ref, context); 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. */ /** Go builtin/primitive field types that can never carry a project method. */
const GO_BUILTIN_FIELD_TYPES = new Set([ const GO_BUILTIN_FIELD_TYPES = new Set([
'string', 'bool', 'byte', 'rune', 'error', 'any', 'string', 'bool', 'byte', 'rune', 'error', 'any',