Skip to content
Merged
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 @@ -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.
Expand Down
128 changes: 128 additions & 0 deletions __tests__/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
35 changes: 34 additions & 1 deletion src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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',
Expand Down