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 @@ -100,6 +100,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes

- **Code written without a space before a name resolves like spaced code.** Where one token ends exactly where the next begins, the resolver read the syntax at that point as the earlier token: in `x=1;local function helper()` it saw the `;`, so the Lua local read as global and calls from other files linked to it, and in `if ($x instanceof Foo)$x->bar();` it saw the `)`, so the PHP guard did not type `$x`. It now reads the token that starts there, the same way the TypeScript side already did. On json, jq and CPython the graph is unchanged.
- **A Kotlin infix call on a parenthesized receiver now reaches the right class.** `(b) put 1`, `(b)put(1)` and `((b)) put (1)` linked to whichever class had a `put`, so with `b: A` and another class `B` that also declared `put`, the call landed on `B::put`. They now follow what `b` holds, as `b put 1` already did. An extension on a project class called through a typed receiver is still not linked, as before; the parenthesized form no longer guesses it by name.
- **A Vite plugin that spreads another plugin no longer turns off a React Router app's file routes.** Any `...` in a root `vite.config`, `react-router.config` or `remix.config` was taken for custom route configuration, so React Router's own RSC playground, which merges `...mdx({ … })` into a plugin entry, got no file routes. A spread of a call inside a `plugins` list is now ignored, since it only builds a plugin; a spread of anything else (`...shared`, `...makeConfig()` at the top level, `[...plugins]`, or inside a plugin call's arguments) still turns file routes off. The playground goes from 0 to 12 file routes.
- **React Router's own `flatRoutes() satisfies RouteConfig` template now gets file routes, and RSC route modules link to their `ServerComponent`.** An `app/routes.ts` written as `export default flatRoutes() satisfies RouteConfig;` (or `as RouteConfig`), the form React Router's templates use, was taken for custom configuration, so the app got no file routes at all. In React Router's RSC framework mode a route module exports `ServerComponent` instead of a default component; such a page now gets its route and links to that component, both for files under `app/routes/` and for modules listed in `app/routes.ts`. A module that exports both keeps its default component.
- **An inherited method no longer links to a same-named class in another package.** When a call on a `Child` reached `m()` through `Child extends Base`, the lookup past the first parent matched any class called `Base` in the project, so with an unrelated `Base` in another package the call could land on that package's `Base.m()`, or skip a grandparent's `m()` for it. The lookup now follows the parent the declaration names, for Java, Kotlin, C#, Scala and Swift, where a Java or Kotlin receiver comes from a factory call, or a C#, Scala or Swift receiver is typed. In Scala, a same-named object or a package block in the same file is told apart too; in Swift the other `Base` is one declared in another file, since modules are not modeled. Dart, TypeScript, Python and C++ are unchanged: their wrong links did not reproduce, or Dart's member lookup matches parents by name elsewhere.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ Dispatch and framework coverage the fork adds, by kind:

Call resolution the fork adds:

- Kotlin infix calls keep their call edge when a comment sits inside the expression.
- Kotlin infix calls keep their call edge when a comment sits inside the expression, and a parenthesized receiver (`(b) put 1`) is typed like a bare one.
- Kotlin receiver inference follows bounded chains of declared returns and verified receiver-preserving methods. Properties initialized by typed factory calls keep compatible imported extensions. Explicit casts, single-type `when` branches, filtered collection elements and bound generic factory arguments also supply receiver types.
- Kotlin chains on instance and extension receivers use the declared return type, including nested and multiline calls; upstream covers class and companion-factory chains. An imported return-type guess for an unknown receiver keeps confidence at most 0.7, and so does a Rust chain match without a proved receiver type.
- Kotlin multi-dollar strings keep their interpolation threshold.
Expand Down
28 changes: 27 additions & 1 deletion __tests__/kotlin-infix-calls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,14 @@ it.each([
infix fun Int.and(other: Int): Int = this
fun numeric(value: Int, mask: Int) = (value) and mask
`, 'numeric', []],
// A parenthesized receiver is typed like a bare one, and an extension on a
// project class is not linked through a typed receiver (`value and mask`
// gives the same result), so no edge is better than a guess by name.
['project numeric extension', `package project
class Int
infix fun Int.and(other: Int): Int = this
fun custom(value: Int, mask: Int) = (value) and mask
`, 'custom', ['Int::and']],
`, 'custom', []],
['nested-comment this receiver', `package app
class Spec {
infix fun apply(other: Spec): Spec = this
Expand All @@ -158,3 +161,26 @@ class Spec {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it('types a parenthesized infix receiver by what it holds, not by the method name', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-kotlin-infix-paren-'));
let graph: CodeGraph | undefined;
try {
fs.writeFileSync(path.join(dir, 'Ops.kt'), `package app
class Before { infix fun put(v: Int): Before = this }
class A { infix fun put(v: Int): A = this }
class After { infix fun put(v: Int): After = this }
fun plain(b: A) = b put 1
fun paren(b: A) = (b) put 1
fun parenTight(b: A) = (b)put(1)
fun parenArg(b: A) = ((b)) put (1)
`);
graph = await CodeGraph.init(dir, { index: true });
const targets = (name: string) => graph!.getOutgoingEdges(graph!.getNodesInFile('Ops.kt').find((n) => n.name === name)!.id)
.filter((e) => e.kind === 'calls').map((e) => graph!.getNode(e.target)!.qualifiedName);
for (const name of ['plain', 'paren', 'parenTight', 'parenArg']) expect(targets(name), name).toEqual(['app::A::put']);
} finally {
graph?.close();
fs.rmSync(dir, { recursive: true, force: true });
}
});
10 changes: 9 additions & 1 deletion codegraph-kernel/src/kotlin/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ impl<'t> Walker<'t> {
if func.kind() != "simple_identifier" || is_literal_receiver(lhs.kind()) { return; }
let caller = self.top_row();
let name = self.text(func);
let receiver = if lhs.kind() == "simple_identifier" { self.text(lhs) } else { "" };
// `(b) put 1` calls `put` on what `b` holds, as `b put 1` does.
let mut receiver_node = lhs;
while receiver_node.kind() == "parenthesized_expression" {
match named_kids(receiver_node).filter(|n| !n.is_extra()).collect::<Vec<_>>().as_slice() {
[inner] => receiver_node = *inner,
_ => break,
}
}
let receiver = if receiver_node.kind() == "simple_identifier" { self.text(receiver_node) } else { "" };
let callee = if !receiver.is_empty() && receiver != "this" && receiver != "super" {
format!("{receiver}.{name}")
} else { name.to_string() };
Expand Down
Loading