Skip to content
Open
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 @@ -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<User>("/me")` or `await api.get<T>(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(<App />)` or `render(<Card />)`, 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)
Expand Down
128 changes: 128 additions & 0 deletions __tests__/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5194,6 +5194,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 @@ -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
Expand Down Expand Up @@ -19643,6 +19647,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 @@ -23017,6 +23039,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