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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- `codegraph sync` and the file watcher now update the files that import through a module that re-exports what it imports, like a package's `__init__.py` or an `index.ts` barrel, when that module is deleted or added, and in Python also the files that reach it through another package re-exporting it. Before, deleting `shop/__init__.py` left every `from shop import Order` linked to the class it re-exported, and deleting `components/index.ts` left every `import { Button } from './components'` linked, until each importing file changed or the project was re-indexed. When such a module came back, the imports stayed linked the way they were while it was missing, and in a large Python project an import of a module whose name is as common as `models` or `test`, like `from django.test import TestCase`, stayed unlinked. Python, JavaScript, TypeScript, Vue, Svelte and Astro.

- In Kotlin, a call on a typed receiver now links to an extension function declared in a package, like `value.shout()` or the infix `a join b` with `value: Foo` and `fun Foo.shout()` under `package demo`, including one declared in another file of the package. Before, it linked only when the file had no `package` line, so the usual Kotlin project got no edge. On JetBrains Exposed, 1,020 such calls now link (`Table.selectAll()` to `FieldSet.selectAll`, `transaction.throwUnsupportedException(…)`), and none that linked before is lost. A member of the receiver's class still wins over an extension of the same name.

#### MCP / indexing

- Full indexes of large projects no longer sometimes run two to three times slower than usual. A timing clash between two background database maintenance steps could switch reference resolution from several threads to one partway through.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Call resolution the fork adds:
- 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 calls on a typed receiver link to extension functions declared in a package, infix ones included (`value.shout()`, `a join b`); on JetBrains Exposed that adds 1,020 call links and removes none.
- Kotlin multi-dollar strings keep their interpolation threshold.
- C++ typedef and using aliases follow their declaration scope, including nested owners and inherited aliases. Pointer, array and function members are not supertypes. Visible class-scoped aliases and declared complex receivers keep their method owners; unsubstituted template parameters and ambiguous owners stay unresolved.
- C++ `->` calls on a `std::unique_ptr`, `std::shared_ptr` or `std::optional` reach the type it holds, looked up through the caller's enclosing classes and their bases, its namespaces, `using` directives and aliases. A held type the project does not declare leaves the call unresolved, as a plain pointer to one does; upstream guesses a callee from the receiver's name there. A partial class template specialization is indexed as an anonymous class, so a call to a member only it declares stays unresolved.
Expand Down
51 changes: 51 additions & 0 deletions __tests__/kotlin-extension-call-typed-receiver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect, it } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { CodeGraph } from '../src';

// An extension function's qualified name is `Foo::shout` whatever its package, while the class
// it extends is `pkg::Foo`: the call on a typed receiver has to find it by the receiver's type.
it('links a call on a typed receiver to an extension function declared in a package', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-kotlin-ext-pkg-'));
let cg: CodeGraph | undefined;
try {
fs.writeFileSync(path.join(dir, 'Foo.kt'), `package demo

class Foo
class Bar
class Baz { fun shout(): String = "member" }
`);
fs.writeFileSync(path.join(dir, 'Ext.kt'), `package demo

fun Foo.shout(): String = "x"
infix fun Foo.join(other: Foo): Foo = this
fun Baz.shout(loud: Boolean): String = "extension"
fun Bar.whisper(): String = "y"
`);
fs.writeFileSync(path.join(dir, 'Use.kt'), `package demo

fun byParameter(value: Foo): String = value.shout()
fun byConstructor(): String {
val f = Foo()
return f.shout()
}
fun byInfix(a: Foo, b: Foo): Foo = a join b
fun memberWins(value: Baz): String = value.shout()
fun otherType(value: Bar): String = value.shout()
fun unrelatedExtension(value: Foo): String = value.whisper()
`);
cg = await CodeGraph.init(dir, { index: true });
const targets = (name: string) => cg!.getOutgoingEdges(cg!.getNodesInFile('Use.kt').find((n) => n.name === name)!.id)
.filter((e) => e.kind === 'calls').map((e) => cg!.getNode(e.target)!.qualifiedName + '@' + cg!.getNode(e.target)!.filePath);
expect(targets('byParameter')).toEqual(['Foo::shout@Ext.kt']);
expect(targets('byConstructor')).toEqual(['Foo::shout@Ext.kt']);
expect(targets('byInfix')).toEqual(['Foo::join@Ext.kt']);
expect(targets('memberWins')).toEqual(['demo::Baz::shout@Foo.kt']);
expect(targets('otherType')).toEqual([]);
expect(targets('unrelatedExtension')).toEqual([]);
} finally {
cg?.close();
fs.rmSync(dir, { recursive: true, force: true });
}
});
6 changes: 3 additions & 3 deletions __tests__/kotlin-infix-calls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,13 @@ 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 class links through the typed receiver (`value and mask` gives
// the same result): the extension is named `Int::and` whatever its package.
['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', []],
`, 'custom', ['Int::and']],
['nested-comment this receiver', `package app
class Spec {
infix fun apply(other: Spec): Spec = this
Expand Down
7 changes: 7 additions & 0 deletions codegraph-kernel/src/resolve/kotlin_receiver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ impl KernelResolver {

fn kotlin_receiver_member(&mut self, hit: &KotlinReceiverHit, name: &str, r: &ResolveRefIn) -> Res<Option<Arc<KNode>>> {
if let Some(member) = self.match_bound_type_member(&hit.ty, name, &hit.site)? { return Ok(Some(member.node)); }
self.kotlin_extension_member(hit, name, r)
}

/// The one visible extension function `name` whose receiver type accepts `hit`'s type: its
/// qualified name carries the receiver as written (`Foo::shout`, never `pkg::Foo::shout`), so
/// it cannot be found as a member of the receiver's class by qualified name.
pub(super) fn kotlin_extension_member(&mut self, hit: &KotlinReceiverHit, name: &str, r: &ResolveRefIn) -> Res<Option<Arc<KNode>>> {
let candidates: Vec<_> = self.nodes_by_name(name)?.iter().filter(|n| n.language == "kotlin" && n.kind == "method").cloned().collect();
let mut matching = Vec::new();
for candidate in candidates {
Expand Down
10 changes: 9 additions & 1 deletion codegraph-kernel/src/resolve/method_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,15 @@ if inferred.is_none() && r.language == "dart" {
if let Some(b) = &binding {
bsite.line = b.line;
}
return self.match_bound_type_member(&t, &method_name, &bsite);
let member = self.match_bound_type_member(&t, &method_name, &bsite)?;
if member.is_none() && r.language == "kotlin" {
// An extension function is no member of the receiver's class, and in a
// package its qualified name does not say which class it extends.
let hit = super::kotlin_receiver::KotlinReceiverHit { ty: t.clone(), site: bsite, heuristic: false };
return Ok(self.kotlin_extension_member(&hit, &method_name, r)?
.map(|node| KCand { node, confidence: 0.9, resolved_by: "instance-method" }));
}
return Ok(member);
}
}

Expand Down
Loading