Skip to content

Commit fe7a520

Browse files
Nest top-level extensions under their receiver type
Port #973's extension symbols: a top-level extension function or property is owned by its receiver type within the extension's own package, so `fun String.foo()` in `sample` is `sample/String#foo().` and `kotlin.collections.forEachIndexed` on `Iterable` is `kotlin/collections/Iterable#forEachIndexed().`. This applies to source declarations and to references to library extensions. Extension disambiguators count the same-named members of the receiver class (when it is in the extension's package) followed by the same-named extensions on that receiver in the package, so a member `MyClass#foo().` and an extension `MyClass#foo(+1).` stay distinct. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a153c0-ec1f-774a-993c-bca3256017ce
1 parent 62dfea4 commit fe7a520

7 files changed

Lines changed: 103 additions & 18 deletions

File tree

‎scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,10 @@ class KotlinAnalysisIndexer(
140140
}
141141

142142
private fun indexFile(ktFile: KtFile): Document {
143-
val cache = SymbolsCache()
144143
val text = ktFile.text
145144
val lines = LineIndex(text)
146145
val builder = ScipDocumentBuilder()
147-
analyze(ktFile) { ktFile.accept(IndexingVisitor(this, cache, lines, builder)) }
146+
analyze(ktFile) { ktFile.accept(IndexingVisitor(this, SymbolsCache(this), lines, builder)) }
148147
val relativePath =
149148
ScipShardPaths.relativePath(sourceroot, Paths.get(ktFile.virtualFilePath))
150149
return builder.build("kotlin", relativePath, text)

‎scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt‎

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
77
import org.jetbrains.kotlin.analysis.api.symbols.KaClassLikeSymbol
88
import org.jetbrains.kotlin.analysis.api.symbols.KaConstructorSymbol
99
import org.jetbrains.kotlin.analysis.api.symbols.KaFunctionSymbol
10+
import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol
1011
import org.jetbrains.kotlin.analysis.api.symbols.KaPackageSymbol
1112
import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol
1213
import org.jetbrains.kotlin.analysis.api.symbols.KaTypeAliasSymbol
1314
import org.jetbrains.kotlin.analysis.api.types.KaClassType
1415
import org.jetbrains.kotlin.name.ClassId
1516
import org.jetbrains.kotlin.name.FqName
17+
import org.jetbrains.kotlin.name.Name
1618
import org.jetbrains.kotlin.psi.KtAnonymousInitializer
1719
import org.jetbrains.kotlin.psi.KtBlockExpression
20+
import org.jetbrains.kotlin.psi.KtCallableDeclaration
1821
import org.jetbrains.kotlin.psi.KtClassBody
1922
import org.jetbrains.kotlin.psi.KtClassOrObject
2023
import org.jetbrains.kotlin.psi.KtConstructor
@@ -45,12 +48,13 @@ import org.scip_code.scip_java.shared.LocalSymbolsCache as SharedLocalSymbolsCac
4548
* Computes SCIP symbol strings for Kotlin declarations and resolved references.
4649
*
4750
* Declarations with Kotlin source are classified from their PSI structure, which keeps the
48-
* version-sensitive Analysis API surface minimal. References that resolve to declarations without
49-
* Kotlin source (classpath or JDK symbols) fall back to `ClassId`/`CallableId`-derived symbols.
51+
* version-sensitive Analysis API surface minimal; only extension receivers are resolved. References
52+
* that resolve to declarations without Kotlin source (classpath or JDK symbols) fall back to
53+
* `ClassId`/`CallableId`-derived symbols.
5054
*
5155
* Instantiate one cache per indexed file: local symbols are numbered per SCIP document.
5256
*/
53-
class SymbolsCache {
57+
class SymbolsCache(private val session: KaSession) {
5458
private val globals = HashMap<PsiElement, Symbol>()
5559
private val packages = HashMap<FqName, Symbol>()
5660
private val locals =
@@ -297,9 +301,61 @@ class SymbolsCache {
297301
is KtPropertyAccessor -> symbolForDeclaration(declaration.property)
298302
else ->
299303
declaration.containingClassOrObject?.let { symbolForDeclaration(it) }
304+
?: sourceExtensionReceiver(declaration)?.let {
305+
extensionOwnerSymbol(declaration.containingKtFile.packageFqName, it)
306+
}
300307
?: packageSymbol(declaration.containingKtFile.packageFqName)
301308
}
302309

310+
/**
311+
* Top-level extensions are owned by their receiver type, nested in the extension's own package
312+
* (`sample/String#foo().` for `fun String.foo()` in `sample`), like scip-kotlinc.
313+
*/
314+
private fun extensionOwnerSymbol(packageFqName: FqName, receiver: ClassId): Symbol =
315+
Symbol.createGlobal(
316+
packageSymbol(packageFqName),
317+
ScipSymbolDescriptor(Kind.TYPE, receiver.shortClassName.asString()),
318+
)
319+
320+
private fun sourceExtensionReceiver(declaration: KtDeclaration): ClassId? {
321+
if (declaration !is KtCallableDeclaration || declaration.receiverTypeReference == null) {
322+
return null
323+
}
324+
return with(session) { declaration.symbol as? KaCallableSymbol }?.let(::receiverClassId)
325+
}
326+
327+
private fun receiverClassId(callable: KaCallableSymbol): ClassId? =
328+
with(session) { (callable.receiverParameter?.returnType as? KaClassType)?.classId }
329+
330+
/**
331+
* The disambiguator of a top-level extension function: like scip-kotlinc, it counts the
332+
* same-named members of the receiver class (when it is in the extension's package) followed by
333+
* the same-named extensions on that receiver in the package.
334+
*/
335+
private fun extensionDisambiguator(
336+
function: KaCallableSymbol,
337+
packageFqName: FqName,
338+
name: Name,
339+
receiver: ClassId,
340+
): String {
341+
val extensions =
342+
with(session) {
343+
findTopLevelCallables(packageFqName, name)
344+
.filter { it is KaNamedFunctionSymbol && receiverClassId(it) == receiver }
345+
.toList()
346+
}
347+
val index = extensions.indexOf(function)
348+
if (index < 0) return "()"
349+
val members =
350+
if (receiver.packageFqName != packageFqName) 0
351+
else
352+
with(session) {
353+
findClass(receiver)?.declaredMemberScope?.callables(name)?.count() ?: 0
354+
}
355+
val count = members + index
356+
return if (count == 0) "()" else "(+$count)"
357+
}
358+
303359
private fun scipDescriptor(declaration: KtDeclaration): ScipSymbolDescriptor =
304360
when {
305361
declaration is KtObjectDeclaration && declaration.isObjectLiteral() ->
@@ -342,6 +398,17 @@ class SymbolsCache {
342398
*/
343399
private fun methodDisambiguator(function: KtNamedFunction): String {
344400
val name = function.name ?: return "()"
401+
if (function.containingClassOrObject == null) {
402+
sourceExtensionReceiver(function)?.let { receiver ->
403+
val symbol = with(session) { function.symbol }
404+
return extensionDisambiguator(
405+
symbol,
406+
function.containingKtFile.packageFqName,
407+
Name.identifier(name),
408+
receiver,
409+
)
410+
}
411+
}
345412
var count = 0
346413
var found = false
347414
for (sibling in siblingDeclarations(function)) {
@@ -390,7 +457,10 @@ class SymbolsCache {
390457
*/
391458
private fun externalCallableSymbol(session: KaSession, target: KaCallableSymbol): Symbol {
392459
val callableId = target.callableId ?: return Symbol.NONE
393-
val owner = callableId.classId?.let(::classSymbol) ?: packageSymbol(callableId.packageName)
460+
val owner =
461+
callableId.classId?.let(::classSymbol)
462+
?: receiverClassId(target)?.let { extensionOwnerSymbol(callableId.packageName, it) }
463+
?: packageSymbol(callableId.packageName)
394464
val name = callableId.callableName.asString()
395465
val descriptor =
396466
when (target) {
@@ -411,6 +481,14 @@ class SymbolsCache {
411481
with(session) {
412482
val classId = callableId.classId
413483
if (classId == null) {
484+
receiverClassId(target)?.let { receiver ->
485+
return extensionDisambiguator(
486+
target,
487+
callableId.packageName,
488+
callableId.callableName,
489+
receiver,
490+
)
491+
}
414492
findTopLevelCallables(callableId.packageName, callableId.callableName).toList()
415493
} else {
416494
val classSymbol = findClass(classId) ?: return "()"

‎scip-kotlin-analysis/src/test/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexerTest.kt‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ class KotlinAnalysisIndexerTest {
623623

624624
@Test
625625
fun `extension receiver type reference`(@TempDir path: Path) {
626+
// String is from the kotlin package; our extension in sample gets
627+
// symbol sample/String#foo(). — distinct from kotlin/String#foo().
628+
// This means extensions on cross-package types never collide with
629+
// the receiver class's own methods in the symbol table.
626630
val document =
627631
compileScip(
628632
path,
@@ -638,7 +642,7 @@ class KotlinAnalysisIndexerTest {
638642
arrayOf(
639643
scipOccurrence {
640644
role = DEFINITION
641-
symbol = "sample/foo()."
645+
symbol = "sample/String#foo()."
642646
range {
643647
startLine = 2
644648
startCharacter = 11
@@ -674,7 +678,7 @@ class KotlinAnalysisIndexerTest {
674678
},
675679
scipOccurrence {
676680
role = REFERENCE
677-
symbol = "sample/foo()."
681+
symbol = "sample/String#foo()."
678682
range {
679683
startLine = 3
680684
startCharacter = 23
@@ -703,7 +707,7 @@ class KotlinAnalysisIndexerTest {
703707
arrayOf(
704708
scipOccurrence {
705709
role = DEFINITION
706-
symbol = "sample/asString."
710+
symbol = "sample/Int#asString."
707711
range {
708712
startLine = 2
709713
startCharacter = 8
@@ -739,7 +743,7 @@ class KotlinAnalysisIndexerTest {
739743
},
740744
scipOccurrence {
741745
role = REFERENCE
742-
symbol = "sample/asString."
746+
symbol = "sample/Int#asString."
743747
range {
744748
startLine = 3
745749
startCharacter = 15
@@ -753,7 +757,11 @@ class KotlinAnalysisIndexerTest {
753757

754758
@Test
755759
fun `extension overload disambiguator`(@TempDir path: Path) {
756-
// A member foo() and an extension foo() on the same class produce distinct symbols.
760+
// When a class already has a member named foo() and an extension also
761+
// adds foo(), the extension is counted after the member in the combined
762+
// sibling list (class members + same-package extensions on the same
763+
// receiver type), so the extension gets the (+1) disambiguator and the
764+
// two produce distinct symbols.
757765
val document =
758766
compileScip(
759767
path,
@@ -787,7 +795,7 @@ class KotlinAnalysisIndexerTest {
787795
},
788796
scipOccurrence {
789797
role = DEFINITION
790-
symbol = "sample/foo()."
798+
symbol = "sample/MyClass#foo(+1)."
791799
range {
792800
startLine = 5
793801
startCharacter = 12

‎scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@
162162
// signature_documentation
163163
// > local val total: Int
164164
// ^^^^^^^ reference local 3
165-
// ^^^^^ reference scip-java maven . . kotlin/collections/sumOf(+66).
165+
// ^^^^^ reference scip-java maven . . kotlin/collections/Iterable#sumOf(+3).
166166
// ^^^^^^^^^^^^^^ definition local 5
167167
// kind Parameter
168168
// display_name <destruct>

‎scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@
130130
// ^^^^ reference scip-java maven . . snapshots/Suit#
131131
// ^^^^^^^ reference scip-java maven . . snapshots/Suit#entries.
132132
// ^^^^^^^ reference scip-java maven . . snapshots/Suit#entries.get().
133-
// ^^^^ reference scip-java maven . . kotlin/collections/find(+9).
133+
// ^^^^ reference scip-java maven . . kotlin/collections/Iterable#find().
134134
// ^^^^^^^^^^^^^^^^^^^^^^^ definition local 0
135135
// kind Parameter
136136
// display_name it

‎scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
// ^ reference scip-java maven . . snapshots/Container#mapItems().[R]
101101
// ^^^^^ reference scip-java maven . . snapshots/Container#items.
102102
// ^^^^^ reference scip-java maven . . snapshots/Container#items.get().
103-
// ^^^^^^^^^^ reference scip-java maven . . kotlin/collections/mapNotNull(+1).
103+
// ^^^^^^^^^^ reference scip-java maven . . kotlin/collections/Iterable#mapNotNull().
104104
// ^^^^^^^^^ reference scip-java maven . . snapshots/Container#mapItems().(transform)
105105
// ⌃ enclosing_range_end scip-java maven . . snapshots/Container#mapItems().[R]
106106
// ⌃ enclosing_range_end scip-java maven . . snapshots/Container#mapItems().(transform)
@@ -138,7 +138,7 @@
138138
// ^ reference scip-java maven . . snapshots/firstOrSelf().[T]
139139
// ^ reference scip-java maven . . snapshots/firstOrSelf().[T]
140140
// ^^^^^ reference scip-java maven . . snapshots/firstOrSelf().(items)
141-
// ^^^^^^^^^^^ reference scip-java maven . . kotlin/collections/firstOrNull(+19).
141+
// ^^^^^^^^^^^ reference scip-java maven . . kotlin/collections/List#firstOrNull().
142142
// ^^^^^^^^ reference scip-java maven . . snapshots/firstOrSelf().(fallback)
143143
// ⌃ enclosing_range_end scip-java maven . . snapshots/firstOrSelf().[T]
144144
// ⌃ enclosing_range_end scip-java maven . . snapshots/firstOrSelf().(items)

‎scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Lambdas.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
// > public get(): Unit
1919
// ^^^^^^^^^^^ reference scip-java maven . . kotlin/collections/arrayListOf().
2020
// ^^^^^^ reference scip-java maven . . kotlin/String#
21-
// ^^^^^^^^^^^^^^ reference scip-java maven . . kotlin/collections/forEachIndexed(+9).
21+
// ^^^^^^^^^^^^^^ reference scip-java maven . . kotlin/collections/Iterable#forEachIndexed().
2222
// ^ definition local 0
2323
// kind Parameter
2424
// display_name i
@@ -51,7 +51,7 @@
5151
// signature_documentation
5252
// > public get(): ByteArray
5353
// ^^^ reference scip-java maven . . kotlin/run(+1).
54-
// ^^^^^^^^^^^ reference scip-java maven . . kotlin/text/toByteArray().
54+
// ^^^^^^^^^^^ reference scip-java maven . . kotlin/text/String#toByteArray().
5555
// ⌃ enclosing_range_end scip-java maven . . snapshots/y.
5656
// ⌃ enclosing_range_end scip-java maven . . snapshots/y.get().
5757

0 commit comments

Comments
 (0)