mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 22:43:17 +08:00
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 main81559dcc(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 main69af19ac): 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:
committed by
Colby Mchenry
co-authored by
Claude Opus 5.5
parent
81559dcc37
commit
b6379e6d5e
@@ -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 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 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 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.
|
||||
|
||||
@@ -5225,6 +5225,134 @@ 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' });
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -20103,7 +20103,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
|
||||
@@ -20473,6 +20477,24 @@ 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. 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
|
||||
// name similarity with the containing class. Handles abbreviated variable
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
const GO_BUILTIN_FIELD_TYPES = new Set([
|
||||
'string', 'bool', 'byte', 'rune', 'error', 'any',
|
||||
|
||||
Reference in New Issue
Block a user