From dd2ca3c5e881a137d990ef395e587c0e7a47db58 Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Fri, 3 Jul 2026 14:52:12 +0200 Subject: [PATCH 01/18] Update to Kotlin 2.2.20 For scip-kotlinc: * CheckerContext.containingFile was renamed to containingFileSymbol. * FirCallableSymbol<*>.directOverriddenSymbolsSafe now takes context by context parameter. * FirCallableSymbol.callableId was made nullable and is replaced by FirCallableSymbol.callableIdForRendering for rendering purpose. * Anonymous objects are now considered as locals. Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/3f4756486010cce6163b8b409441cd1e51278008 --- gradle/libs.versions.toml | 2 +- .../scip_java/kotlinc/AnalyzerCheckers.kt | 54 +++++++----------- .../kotlinc/ScipTextDocumentBuilder.kt | 12 ++-- .../scip_java/kotlinc/ScipVisitor.kt | 56 +++++++++---------- .../scip_java/kotlinc/test/AnalyzerTest.kt | 20 +++---- 5 files changed, 65 insertions(+), 79 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 93d930c20..54c8d6f36 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ gradle-api = "8.11.1" junit-jupiter = "5.11.4" kctfork = "0.7.1" kotest = "6.2.1" -kotlin = "2.2.0" +kotlin = "2.2.20" kotlinx-serialization = "1.11.0" lombok = "1.18.46" maven-plugin-annotations = "3.15.2" diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt index 37ece4b57..c091f0b25 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt @@ -134,7 +134,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio if (names != null) { eachFqNameElement(fqName, source.treeStructure, names) { fqName, name -> - visitor?.visitPackage(fqName, name, context) + visitor?.visitPackage(fqName, name) } } } @@ -161,13 +161,13 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio ) if (klass != null) { - visitor?.visitClassReference(klass, name, context) + visitor?.visitClassReference(klass, name) } else if (callables.isNotEmpty()) { for (callable in callables) { - visitor?.visitCallableReference(callable, name, context) + visitor?.visitCallableReference(callable, name) } } else { - visitor?.visitPackage(fqName, name, context) + visitor?.visitPackage(fqName, name) } } } @@ -179,7 +179,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirClassLikeDeclaration) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] val objectKeyword = if (declaration is FirAnonymousObject) { @@ -192,7 +192,6 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio visitor?.visitClassOrObject( declaration, objectKeyword ?: getIdentifier(source), - context, enclosingSource = source, ) @@ -201,7 +200,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio val superSymbol = superType.toClassLikeSymbol(context.session) val superSource = superType.source if (superSymbol != null && superSource != null) { - visitor?.visitClassReference(superSymbol, superSource, context) + visitor?.visitClassReference(superSymbol, superSource) } } } @@ -212,7 +211,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirConstructor) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] if (declaration.isPrimary) { @@ -238,14 +237,12 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio visitor?.visitPrimaryConstructor( declaration, constructorKeyboard ?: objectKeyword ?: getIdentifier(klassSource), - context, enclosingSource = source, ) } else { visitor?.visitSecondaryConstructor( declaration, getIdentifier(source), - context, enclosingSource = source, ) } @@ -256,12 +253,11 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirSimpleFunction) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitNamedFunction( declaration, getIdentifier(source), - context, enclosingSource = source, ) @@ -270,7 +266,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio if ( klass != null && klassSource != null && klassSource.kind !is KtFakeSourceElementKind ) { - visitor?.visitClassReference(klass, getIdentifier(klassSource), context) + visitor?.visitClassReference(klass, getIdentifier(klassSource)) } } } @@ -280,9 +276,9 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirAnonymousFunction) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] - visitor?.visitNamedFunction(declaration, source, context, enclosingSource = source) + visitor?.visitNamedFunction(declaration, source, enclosingSource = source) } } @@ -290,12 +286,11 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirProperty) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitProperty( declaration, getIdentifier(source), - context, enclosingSource = source, ) @@ -304,7 +299,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio if ( klass != null && klassSource != null && klassSource.kind !is KtFakeSourceElementKind ) { - visitor?.visitClassReference(klass, getIdentifier(klassSource), context) + visitor?.visitClassReference(klass, getIdentifier(klassSource)) } } } @@ -313,12 +308,11 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirValueParameter) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitParameter( declaration, getIdentifier(source), - context, enclosingSource = source, ) @@ -327,7 +321,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio if ( klass != null && klassSource != null && klassSource.kind !is KtFakeSourceElementKind ) { - visitor?.visitClassReference(klass, getIdentifier(klassSource), context) + visitor?.visitClassReference(klass, getIdentifier(klassSource)) } } } @@ -336,12 +330,11 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirTypeParameter) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitTypeParameter( declaration, getIdentifier(source), - context, enclosingSource = source, ) } @@ -351,12 +344,11 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirTypeAlias) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitTypeAlias( declaration, getIdentifier(source), - context, enclosingSource = source, ) } @@ -367,7 +359,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirPropertyAccessor) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] val identifierSource = if (declaration.isGetter) { @@ -385,7 +377,6 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio visitor?.visitPropertyAccessor( declaration, identifierSource, - context, enclosingSource = source, ) } @@ -401,12 +392,11 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio return } - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitSimpleNameExpression( calleeReference, getIdentifier(calleeReference.source ?: source), - context, ) val resolvedSymbol = calleeReference.resolvedSymbol @@ -420,7 +410,6 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio visitor?.visitClassReference( referencedKlass, getIdentifier(calleeReference.source ?: source), - context, ) } } @@ -432,14 +421,12 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio visitor?.visitCallableReference( it, getIdentifier(calleeReference.source ?: source), - context, ) } resolvedSymbol.setterSymbol?.let { visitor?.visitCallableReference( it, getIdentifier(calleeReference.source ?: source), - context, ) } } @@ -454,13 +441,12 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio val source = typeRef.source ?: return val classSymbol = expression.conversionTypeRef.toClassLikeSymbol(context.session) ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitClassReference( classSymbol, getIdentifier(expression.conversionTypeRef.source ?: source), - context, ) } } diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt index f767dda2b..ffb9a139a 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt @@ -39,26 +39,26 @@ class ScipTextDocumentBuilder( fun build(): Document = documentBuilder.build("kotlin", relativePath(), fileText) + context(context: CheckerContext) fun emitScipData( firBasedSymbol: FirBasedSymbol<*>?, symbol: Symbol, element: KtSourceElement, isDefinition: Boolean, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { documentBuilder.addOccurrence(occurrence(symbol, element, isDefinition, enclosingSource)) if (isDefinition) { - documentBuilder.addSymbol(symbolInformation(firBasedSymbol, symbol, element, context)) + documentBuilder.addSymbol(symbolInformation(firBasedSymbol, symbol, element)) } } @OptIn(SymbolInternals::class) + context(context: CheckerContext) private fun symbolInformation( firBasedSymbol: FirBasedSymbol<*>?, symbol: Symbol, element: KtSourceElement, - context: CheckerContext, ): SymbolInformation { val supers = when (firBasedSymbol) { @@ -68,7 +68,7 @@ class ScipTextDocumentBuilder( .mapNotNull { it.toClassLikeSymbol(firBasedSymbol.moduleData.session) } .flatMap { cache[it] } is FirFunctionSymbol<*> -> - firBasedSymbol.directOverriddenSymbolsSafe(context).flatMap { cache[it] } + firBasedSymbol.directOverriddenSymbolsSafe().flatMap { cache[it] } else -> emptyList() } return symbolInformation { @@ -184,13 +184,13 @@ class ScipTextDocumentBuilder( } companion object { - @OptIn(SymbolInternals::class) + @OptIn(SymbolInternals::class, RenderingInternals::class) private fun displayName(firBasedSymbol: FirBasedSymbol<*>): String = when (firBasedSymbol) { is FirClassSymbol -> firBasedSymbol.classId.shortClassName.asString() is FirPropertyAccessorSymbol -> firBasedSymbol.fir.propertySymbol.name.asString() is FirFunctionSymbol -> firBasedSymbol.callableId.callableName.asString() - is FirPropertySymbol -> firBasedSymbol.callableId.callableName.asString() + is FirPropertySymbol -> firBasedSymbol.callableIdForRendering.callableName.asString() is FirVariableSymbol -> firBasedSymbol.name.asString() else -> firBasedSymbol.toString() } diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt index 80b9866e5..78075f131 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt @@ -33,10 +33,10 @@ class ScipVisitor( fun build(): Document = documentBuilder.build() + context(context: CheckerContext) private fun Sequence?.emitAll( element: KtSourceElement, isDefinition: Boolean, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ): List? = this?.onEach { (firBasedSymbol, symbol) -> @@ -45,7 +45,6 @@ class ScipVisitor( symbol, element, isDefinition, - context, enclosingSource, ) } @@ -55,132 +54,133 @@ class ScipVisitor( private fun Sequence.with(firBasedSymbol: FirBasedSymbol<*>?) = this.map { SymbolDescriptorPair(firBasedSymbol, it) } - fun visitPackage(pkg: FqName, element: KtSourceElement, context: CheckerContext) { - cache[pkg].with(null).emitAll(element, isDefinition = false, context) + context(context: CheckerContext) + fun visitPackage(pkg: FqName, element: KtSourceElement) { + cache[pkg].with(null).emitAll(element, isDefinition = false) } + context(context: CheckerContext) fun visitClassReference( firClassSymbol: FirClassLikeSymbol<*>, element: KtSourceElement, - context: CheckerContext, ) { - cache[firClassSymbol].with(firClassSymbol).emitAll(element, isDefinition = false, context) + cache[firClassSymbol].with(firClassSymbol).emitAll(element, isDefinition = false) } + context(context: CheckerContext) fun visitCallableReference( firClassSymbol: FirCallableSymbol<*>, element: KtSourceElement, - context: CheckerContext, ) { - cache[firClassSymbol].with(firClassSymbol).emitAll(element, isDefinition = false, context) + cache[firClassSymbol].with(firClassSymbol).emitAll(element, isDefinition = false) } + context(context: CheckerContext) fun visitClassOrObject( firClass: FirClassLikeDeclaration, element: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firClass.symbol] .with(firClass.symbol) - .emitAll(element, isDefinition = true, context, enclosingSource) + .emitAll(element, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitPrimaryConstructor( firConstructor: FirConstructor, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firConstructor.symbol] .with(firConstructor.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitSecondaryConstructor( firConstructor: FirConstructor, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firConstructor.symbol] .with(firConstructor.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitNamedFunction( firFunction: FirFunction, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firFunction.symbol] .with(firFunction.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitProperty( firProperty: FirProperty, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firProperty.symbol] .with(firProperty.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitParameter( firParameter: FirValueParameter, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firParameter.symbol] .with(firParameter.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitTypeParameter( firTypeParameter: FirTypeParameter, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firTypeParameter.symbol] .with(firTypeParameter.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitTypeAlias( firTypeAlias: FirTypeAlias, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firTypeAlias.symbol] .with(firTypeAlias.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitPropertyAccessor( firPropertyAccessor: FirPropertyAccessor, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firPropertyAccessor.symbol] .with(firPropertyAccessor.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitSimpleNameExpression( firResolvedNamedReference: FirResolvedNamedReference, source: KtSourceElement, - context: CheckerContext, ) { cache[firResolvedNamedReference.resolvedSymbol] .with(firResolvedNamedReference.resolvedSymbol) - .emitAll(source, isDefinition = false, context) + .emitAll(source, isDefinition = false) } } diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt index 36e94ad56..55b422c31 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt @@ -502,7 +502,7 @@ class AnalyzerTest { }, scipOccurrence { role = DEFINITION - symbol = "sample/``#" + symbol = "local 1" range { startLine = 7 startCharacter = 12 @@ -518,7 +518,7 @@ class AnalyzerTest { }, scipOccurrence { role = DEFINITION - symbol = "sample/``#``()." + symbol = "local 2" range { startLine = 7 startCharacter = 12 @@ -544,7 +544,7 @@ class AnalyzerTest { }, scipOccurrence { role = DEFINITION - symbol = "sample/``#foo()." + symbol = "local 3" range { startLine = 8 startCharacter = 21 @@ -560,7 +560,7 @@ class AnalyzerTest { }, scipOccurrence { role = DEFINITION - symbol = "sample/``#" + symbol = "local 5" range { startLine = 10 startCharacter = 12 @@ -576,7 +576,7 @@ class AnalyzerTest { }, scipOccurrence { role = DEFINITION - symbol = "sample/``#``()." + symbol = "local 6" range { startLine = 10 startCharacter = 12 @@ -602,7 +602,7 @@ class AnalyzerTest { }, scipOccurrence { role = DEFINITION - symbol = "sample/``#foo()." + symbol = "local 7" range { startLine = 11 startCharacter = 21 @@ -627,25 +627,25 @@ class AnalyzerTest { signatureText = "public abstract interface Interface : Any" }, scipSymbol { - symbol = "sample/``#" + symbol = "local 1" displayName = "" signatureText = "object : Interface" addOverriddenSymbols("sample/Interface#") }, scipSymbol { - symbol = "sample/``#foo()." + symbol = "local 3" displayName = "foo" signatureText = "public open override fun foo(): Unit" addOverriddenSymbols("sample/Interface#foo().") }, scipSymbol { - symbol = "sample/``#" + symbol = "local 5" displayName = "" signatureText = "object : Interface" addOverriddenSymbols("sample/Interface#") }, scipSymbol { - symbol = "sample/``#foo()." + symbol = "local 7" displayName = "foo" signatureText = "public open override fun foo(): Unit" addOverriddenSymbols("sample/Interface#foo().") From 08e29904b024f125578f0e692ab5ed551717588f Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Fri, 3 Jul 2026 15:14:32 +0200 Subject: [PATCH 02/18] scip-kotlinc: Populate SymbolInformation.Kind This will allow us to distinguish between method and constructors, and get better kind information for locals. Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/6affaf7a525cea82a018cccf8d58bbedcef8a8e1 --- .../kotlinc/ScipTextDocumentBuilder.kt | 20 +++++++++++++++++++ .../scip_java/kotlinc/test/AnalyzerTest.kt | 19 ++++++++++++++++++ .../scip_java/kotlinc/test/ScipBuilders.kt | 2 ++ .../scip_java/kotlinc/test/ScipSymbolsTest.kt | 3 +++ 4 files changed, 44 insertions(+) diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt index ffb9a139a..ee4f962d8 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt @@ -5,10 +5,13 @@ import java.nio.file.Paths import org.jetbrains.kotlin.KtSourceElement import org.jetbrains.kotlin.KtSourceFile import org.jetbrains.kotlin.fir.FirElement +import org.jetbrains.kotlin.fir.FirPackageDirective import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext import org.jetbrains.kotlin.fir.analysis.checkers.directOverriddenSymbolsSafe import org.jetbrains.kotlin.fir.analysis.checkers.toClassLikeSymbol import org.jetbrains.kotlin.fir.analysis.getChild +import org.jetbrains.kotlin.fir.declarations.* +import org.jetbrains.kotlin.fir.declarations.utils.isInterface import org.jetbrains.kotlin.fir.renderer.* import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol import org.jetbrains.kotlin.fir.symbols.SymbolInternals @@ -19,6 +22,7 @@ import org.jetbrains.kotlin.text import org.scip_code.scip.Document import org.scip_code.scip.Occurrence import org.scip_code.scip.SymbolInformation +import org.scip_code.scip.SymbolInformation.Kind import org.scip_code.scip.SymbolRole import org.scip_code.scip.relationship import org.scip_code.scip.signature @@ -84,6 +88,7 @@ class ScipTextDocumentBuilder( } docComment(firBasedSymbol.fir)?.let { documentation += it } } + this.kind = scipKind(firBasedSymbol?.fir) for (parent in supers) { relationships += relationship { this.symbol = parent.toString() @@ -159,6 +164,21 @@ class ScipTextDocumentBuilder( return stripKdoc(kdoc).ifEmpty { null } } + private fun scipKind(element: FirElement?): Kind = + when (element) { + is FirClass if element.isInterface -> Kind.Interface + is FirClassLikeDeclaration -> Kind.Class + is FirConstructor -> Kind.Constructor + is FirTypeParameter -> Kind.TypeParameter + is FirValueParameter -> Kind.Parameter + is FirField -> Kind.Field + is FirProperty -> Kind.Property + is FirVariable -> Kind.Variable + is FirCallableDeclaration -> Kind.Method + is FirPackageDirective -> Kind.Package + else -> Kind.UNRECOGNIZED + } + /** Strips the `/**`, leading `*`s, and `*/` from a kdoc block, returning just the body text. */ private fun stripKdoc(kdoc: String): String { if (kdoc.isEmpty()) return kdoc diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt index 55b422c31..338b41b2d 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt @@ -15,6 +15,7 @@ import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.junit.jupiter.api.io.TempDir import org.scip_code.scip.Document +import org.scip_code.scip.SymbolInformation.Kind import org.scip_code.scip_java.kotlinc.* @OptIn(ExperimentalCompilerApi::class) @@ -107,11 +108,13 @@ class AnalyzerTest { arrayOf( scipSymbol { symbol = "sample/Banana#" + kind = Kind.Class displayName = "Banana" signatureText = "public final class Banana : Any" }, scipSymbol { symbol = "sample/Banana#foo()." + kind = Kind.Method displayName = "foo" signatureText = "public final fun foo(): Unit" }, @@ -279,21 +282,25 @@ class AnalyzerTest { arrayOf( scipSymbol { symbol = "sample/foo()." + kind = Kind.Method displayName = "foo" signatureText = "public final fun foo(): Unit" }, scipSymbol { symbol = "local 0" + kind = Kind.Class displayName = "LocalClass" signatureText = "local final class LocalClass : Any" }, scipSymbol { symbol = "local 1" + kind = Kind.Constructor displayName = "LocalClass" signatureText = "public constructor(): LocalClass" }, scipSymbol { symbol = "local 2" + kind = Kind.Method displayName = "localClassMethod" signatureText = "public final fun localClassMethod(): Unit" }, @@ -410,22 +417,26 @@ class AnalyzerTest { arrayOf( scipSymbol { symbol = "sample/Interface#" + kind = Kind.Interface displayName = "Interface" signatureText = "public abstract interface Interface : Any" }, scipSymbol { symbol = "sample/Interface#foo()." + kind = Kind.Method displayName = "foo" signatureText = "public abstract fun foo(): Unit\n" }, scipSymbol { symbol = "sample/Class#" + kind = Kind.Class displayName = "Class" signatureText = "public final class Class : Interface" addOverriddenSymbols("sample/Interface#") }, scipSymbol { symbol = "sample/Class#foo()." + kind = Kind.Method displayName = "foo" signatureText = "public open override fun foo(): Unit" addOverriddenSymbols("sample/Interface#foo().") @@ -623,29 +634,34 @@ class AnalyzerTest { arrayOf( scipSymbol { symbol = "sample/Interface#" + kind = Kind.Interface displayName = "Interface" signatureText = "public abstract interface Interface : Any" }, scipSymbol { symbol = "local 1" + kind = Kind.Class displayName = "" signatureText = "object : Interface" addOverriddenSymbols("sample/Interface#") }, scipSymbol { symbol = "local 3" + kind = Kind.Method displayName = "foo" signatureText = "public open override fun foo(): Unit" addOverriddenSymbols("sample/Interface#foo().") }, scipSymbol { symbol = "local 5" + kind = Kind.Class displayName = "" signatureText = "object : Interface" addOverriddenSymbols("sample/Interface#") }, scipSymbol { symbol = "local 7" + kind = Kind.Method displayName = "foo" signatureText = "public open override fun foo(): Unit" addOverriddenSymbols("sample/Interface#foo().") @@ -1300,6 +1316,7 @@ class AnalyzerTest { arrayOf( scipSymbol { symbol = "hello/sample/Apple#" + kind = Kind.Class displayName = "Apple" signatureText = "public final class Apple : Any" } @@ -1385,11 +1402,13 @@ class AnalyzerTest { arrayOf( scipSymbol { symbol = "sample/Banana#" + kind = Kind.Class displayName = "Banana" signatureText = "public final class Banana : Any" }, scipSymbol { symbol = "sample/Banana#foo()." + kind = Kind.Method displayName = "foo" signatureText = "public final fun foo(): Unit" }, diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipBuilders.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipBuilders.kt index 1e8ee95a7..a7d549dd7 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipBuilders.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipBuilders.kt @@ -76,6 +76,7 @@ class ScipOccurrenceBuilder { @ScipBuilderDsl class ScipSymbolInformationBuilder { var symbol: String = "" + var kind: SymbolInformation.Kind = SymbolInformation.Kind.UnspecifiedKind var displayName: String = "" var signatureText: String? = null private val docs = mutableListOf() @@ -95,6 +96,7 @@ class ScipSymbolInformationBuilder { internal fun build(): SymbolInformation = symbolInformation { symbol = this@ScipSymbolInformationBuilder.symbol + kind = this@ScipSymbolInformationBuilder.kind if (this@ScipSymbolInformationBuilder.displayName.isNotEmpty()) { displayName = this@ScipSymbolInformationBuilder.displayName } diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt index 048d33cee..e0865cd2f 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt @@ -3,6 +3,7 @@ package org.scip_code.scip_java.kotlinc.test import com.tschuchort.compiletesting.SourceFile import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.junit.jupiter.api.TestFactory +import org.scip_code.scip.SymbolInformation.Kind import org.scip_code.scip_java.kotlinc.* import org.scip_code.scip_java.kotlinc.test.ExpectedSymbols.ScipData import org.scip_code.scip_java.kotlinc.test.ExpectedSymbols.SymbolCacheData @@ -775,12 +776,14 @@ class ScipSymbolsTest { listOf( scipSymbol { symbol = "x." + kind = Kind.Property displayName = "x" signatureText = "public final val x: String" documentation("hello world\n test content") }, scipSymbol { symbol = "getX()." + kind = Kind.Method displayName = "x" signatureText = "public get(): String" documentation("hello world\n test content") From c88a8e7e12db2e49a4f7b25a9edac3fc43984ee6 Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Fri, 3 Jul 2026 15:24:09 +0200 Subject: [PATCH 03/18] scip-kotlinc: Rework getters and setters to be property children This changes the symbols of getters and setters to be `x.get().` and `x.set().` instead of `getX().` and `setX().`. Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/7b4bd701b8cfc83855f0e336fb81588d04a19741 --- .../scip_java/kotlinc/SymbolsCache.kt | 12 +++------- .../scip_java/kotlinc/test/ScipSymbolsTest.kt | 24 +++++++++---------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt index 57f5b1b69..c7eb0f9a6 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt @@ -14,7 +14,6 @@ import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol import org.jetbrains.kotlin.fir.symbols.SymbolInternals import org.jetbrains.kotlin.fir.symbols.impl.* import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly import org.scip_code.scip_java.kotlinc.ScipSymbolDescriptor.Kind import org.scip_code.scip_java.shared.LocalSymbolsCache as SharedLocalSymbolsCache @@ -115,6 +114,7 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { return getSymbol(symbol.containingDeclarationSymbol, locals) is FirValueParameterSymbol -> return getSymbol(symbol.containingDeclarationSymbol, locals) + is FirPropertyAccessorSymbol -> return getSymbol(symbol.propertySymbol, locals) is FirCallableSymbol -> { val session = symbol.fir.moduleData.session return symbol.getContainingSymbol(session)?.let { getSymbol(it, locals) } @@ -142,15 +142,9 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { symbol is FirClassLikeSymbol -> ScipSymbolDescriptor(Kind.TYPE, symbol.classId.shortClassName.asString()) symbol is FirPropertyAccessorSymbol && symbol.isSetter -> - ScipSymbolDescriptor( - Kind.METHOD, - "set" + symbol.propertySymbol.fir.name.toString().capitalizeAsciiOnly(), - ) + ScipSymbolDescriptor(Kind.METHOD, "set") symbol is FirPropertyAccessorSymbol && symbol.isGetter -> - ScipSymbolDescriptor( - Kind.METHOD, - "get" + symbol.propertySymbol.fir.name.toString().capitalizeAsciiOnly(), - ) + ScipSymbolDescriptor(Kind.METHOD, "get") symbol is FirConstructorSymbol -> ScipSymbolDescriptor(Kind.METHOD, "", methodDisambiguator(symbol)) symbol is FirFunctionSymbol -> diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt index e0865cd2f..b4c7035b8 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt @@ -276,7 +276,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "getX()." + symbol = "x.get()." range { startLine = 0 startCharacter = 4 @@ -287,7 +287,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "setX()." + symbol = "x.set()." range { startLine = 0 startCharacter = 4 @@ -328,7 +328,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "setX()." + symbol = "x.set()." range { startLine = 0 startCharacter = 4 @@ -342,7 +342,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "getX()." + symbol = "x.get()." range { startLine = 1 startCharacter = 4 @@ -388,7 +388,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "getX()." + symbol = "x.get()." range { startLine = 0 startCharacter = 4 @@ -402,7 +402,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "setX()." + symbol = "x.set()." range { startLine = 1 startCharacter = 4 @@ -449,7 +449,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "getX()." + symbol = "x.get()." range { startLine = 1 startCharacter = 4 @@ -465,7 +465,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "setX()." + symbol = "x.set()." range { startLine = 2 startCharacter = 4 @@ -528,7 +528,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "Test#getSample()." + symbol = "Test#sample.get()." range { startLine = 0 startCharacter = 15 @@ -542,7 +542,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "Test#setSample()." + symbol = "Test#sample.set()." range { startLine = 0 startCharacter = 15 @@ -576,7 +576,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = REFERENCE - symbol = "Test#getSample()." + symbol = "Test#sample.get()." range { startLine = 2 startCharacter = 16 @@ -782,7 +782,7 @@ class ScipSymbolsTest { documentation("hello world\n test content") }, scipSymbol { - symbol = "getX()." + symbol = "x.get()." kind = Kind.Method displayName = "x" signatureText = "public get(): String" From bce15aea0d92cfa8f4458d90dd839967fdfa9ec5 Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Fri, 3 Jul 2026 15:30:47 +0200 Subject: [PATCH 04/18] scip-kotlinc: Add enclosing_symbol field This will allow us to get the parent symbol for locals as well. For non-locals, it should be equal to the current symbol minus the last segment. Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/63c9b6582e081f62b2cc736c6c640b75b334185a --- .../kotlinc/ScipTextDocumentBuilder.kt | 1 + .../scip_java/kotlinc/test/AnalyzerTest.kt | 18 ++++++++++++++++++ .../scip_java/kotlinc/test/ScipBuilders.kt | 2 ++ .../scip_java/kotlinc/test/ScipSymbolsTest.kt | 2 ++ 4 files changed, 23 insertions(+) diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt index ee4f962d8..5e4cb6fdd 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt @@ -89,6 +89,7 @@ class ScipTextDocumentBuilder( docComment(firBasedSymbol.fir)?.let { documentation += it } } this.kind = scipKind(firBasedSymbol?.fir) + this.enclosingSymbol = context.containingDeclarations.lastOrNull()?.let {cache[it].last().toString() } ?: "" for (parent in supers) { relationships += relationship { this.symbol = parent.toString() diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt index 338b41b2d..ade938cfe 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt @@ -109,12 +109,14 @@ class AnalyzerTest { scipSymbol { symbol = "sample/Banana#" kind = Kind.Class + enclosingSymbol = "sample/" displayName = "Banana" signatureText = "public final class Banana : Any" }, scipSymbol { symbol = "sample/Banana#foo()." kind = Kind.Method + enclosingSymbol = "sample/Banana#" displayName = "foo" signatureText = "public final fun foo(): Unit" }, @@ -283,24 +285,28 @@ class AnalyzerTest { scipSymbol { symbol = "sample/foo()." kind = Kind.Method + enclosingSymbol = "sample/" displayName = "foo" signatureText = "public final fun foo(): Unit" }, scipSymbol { symbol = "local 0" kind = Kind.Class + enclosingSymbol = "sample/foo()." displayName = "LocalClass" signatureText = "local final class LocalClass : Any" }, scipSymbol { symbol = "local 1" kind = Kind.Constructor + enclosingSymbol = "local 0" displayName = "LocalClass" signatureText = "public constructor(): LocalClass" }, scipSymbol { symbol = "local 2" kind = Kind.Method + enclosingSymbol = "local 0" displayName = "localClassMethod" signatureText = "public final fun localClassMethod(): Unit" }, @@ -418,18 +424,21 @@ class AnalyzerTest { scipSymbol { symbol = "sample/Interface#" kind = Kind.Interface + enclosingSymbol = "sample/" displayName = "Interface" signatureText = "public abstract interface Interface : Any" }, scipSymbol { symbol = "sample/Interface#foo()." kind = Kind.Method + enclosingSymbol = "sample/Interface#" displayName = "foo" signatureText = "public abstract fun foo(): Unit\n" }, scipSymbol { symbol = "sample/Class#" kind = Kind.Class + enclosingSymbol = "sample/" displayName = "Class" signatureText = "public final class Class : Interface" addOverriddenSymbols("sample/Interface#") @@ -437,6 +446,7 @@ class AnalyzerTest { scipSymbol { symbol = "sample/Class#foo()." kind = Kind.Method + enclosingSymbol = "sample/Class#" displayName = "foo" signatureText = "public open override fun foo(): Unit" addOverriddenSymbols("sample/Interface#foo().") @@ -635,12 +645,14 @@ class AnalyzerTest { scipSymbol { symbol = "sample/Interface#" kind = Kind.Interface + enclosingSymbol = "sample/" displayName = "Interface" signatureText = "public abstract interface Interface : Any" }, scipSymbol { symbol = "local 1" kind = Kind.Class + enclosingSymbol = "local 0" displayName = "" signatureText = "object : Interface" addOverriddenSymbols("sample/Interface#") @@ -648,6 +660,7 @@ class AnalyzerTest { scipSymbol { symbol = "local 3" kind = Kind.Method + enclosingSymbol = "local 1" displayName = "foo" signatureText = "public open override fun foo(): Unit" addOverriddenSymbols("sample/Interface#foo().") @@ -655,6 +668,7 @@ class AnalyzerTest { scipSymbol { symbol = "local 5" kind = Kind.Class + enclosingSymbol = "local 4" displayName = "" signatureText = "object : Interface" addOverriddenSymbols("sample/Interface#") @@ -662,6 +676,7 @@ class AnalyzerTest { scipSymbol { symbol = "local 7" kind = Kind.Method + enclosingSymbol = "local 5" displayName = "foo" signatureText = "public open override fun foo(): Unit" addOverriddenSymbols("sample/Interface#foo().") @@ -1317,6 +1332,7 @@ class AnalyzerTest { scipSymbol { symbol = "hello/sample/Apple#" kind = Kind.Class + enclosingSymbol = "hello/sample/" displayName = "Apple" signatureText = "public final class Apple : Any" } @@ -1403,12 +1419,14 @@ class AnalyzerTest { scipSymbol { symbol = "sample/Banana#" kind = Kind.Class + enclosingSymbol = "sample/" displayName = "Banana" signatureText = "public final class Banana : Any" }, scipSymbol { symbol = "sample/Banana#foo()." kind = Kind.Method + enclosingSymbol = "sample/Banana#" displayName = "foo" signatureText = "public final fun foo(): Unit" }, diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipBuilders.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipBuilders.kt index a7d549dd7..fe8bdebc7 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipBuilders.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipBuilders.kt @@ -77,6 +77,7 @@ class ScipOccurrenceBuilder { class ScipSymbolInformationBuilder { var symbol: String = "" var kind: SymbolInformation.Kind = SymbolInformation.Kind.UnspecifiedKind + var enclosingSymbol: String = "" var displayName: String = "" var signatureText: String? = null private val docs = mutableListOf() @@ -97,6 +98,7 @@ class ScipSymbolInformationBuilder { internal fun build(): SymbolInformation = symbolInformation { symbol = this@ScipSymbolInformationBuilder.symbol kind = this@ScipSymbolInformationBuilder.kind + enclosingSymbol = this@ScipSymbolInformationBuilder.enclosingSymbol if (this@ScipSymbolInformationBuilder.displayName.isNotEmpty()) { displayName = this@ScipSymbolInformationBuilder.displayName } diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt index b4c7035b8..0bc8349bf 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt @@ -777,6 +777,7 @@ class ScipSymbolsTest { scipSymbol { symbol = "x." kind = Kind.Property + enclosingSymbol = "_root_/" displayName = "x" signatureText = "public final val x: String" documentation("hello world\n test content") @@ -784,6 +785,7 @@ class ScipSymbolsTest { scipSymbol { symbol = "x.get()." kind = Kind.Method + enclosingSymbol = "x." displayName = "x" signatureText = "public get(): String" documentation("hello world\n test content") From d2c4575c4cc8f64266ed4cba46b10a1892d1dd9d Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Fri, 3 Jul 2026 15:31:59 +0200 Subject: [PATCH 05/18] scip-kotlinc: Ignore FirFileSymbols without warning Reduces the debug log clutter. Ported from https://github.com//mozsearch/semanticdb-kotlinc/commit/4b7afc21337cba75c2c7b724043bcc9204eb9eb0 --- .../main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt index c7eb0f9a6..04469bd1b 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt @@ -158,6 +158,7 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { symbol is FirValueParameterSymbol -> ScipSymbolDescriptor(Kind.PARAMETER, symbol.name.toString()) symbol is FirVariableSymbol -> ScipSymbolDescriptor(Kind.TERM, symbol.name.toString()) + symbol is FirFileSymbol -> ScipSymbolDescriptor.NONE else -> { err.println("unknown symbol kind ${symbol.javaClass.simpleName}") ScipSymbolDescriptor.NONE From 111cd713894d4fa4344809dede0eea63b9de1d20 Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Fri, 3 Jul 2026 15:33:14 +0200 Subject: [PATCH 06/18] scip-kotlinc: Clear AnalyzerCheckers.visitors after consuming them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should reduce memory usage a bit and avoids lots of “given file is not under the sourceroot” clutter when running the tests. Ported from https://github.com//mozsearch/semanticdb-kotlinc/commit/65b1898de26c02929f1fbd445a3c9c92e8fd3bab --- .../org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt index 54ea38fa6..af74b5786 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt @@ -45,6 +45,7 @@ class PostAnalysisExtension( } catch (e: Exception) { handleException(e) } + AnalyzerCheckers.visitors.clear() } private fun scipShardPathForFile(file: KtSourceFile): Path? { From 0118cd10dbbc06d12d716e04589173173dd2fc64 Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Fri, 3 Jul 2026 15:43:13 +0200 Subject: [PATCH 07/18] Update to Kotlin 2.3.10 scip-kotlinc changes: getContainingSymbol was moved from o.j.k.fir.analysis.checkers to org.jetbrains.kotlin.fir.resolve. CompilerPluginRegistrar now has a virtual pluginId which must match the CommandLineProcessor. Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/cde86db9a62d29afa680de072ace1c60d117c37f --- gradle/libs.versions.toml | 4 ++-- .../org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt | 2 +- .../scip_java/kotlinc/AnalyzerCommandLineProcessor.kt | 4 +++- .../org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt | 2 ++ .../kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt | 2 +- .../test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt | 2 ++ 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 54c8d6f36..0ee952e62 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,9 +2,9 @@ clikt = "5.1.0" gradle-api = "8.11.1" junit-jupiter = "5.11.4" -kctfork = "0.7.1" +kctfork = "0.12.1" kotest = "6.2.1" -kotlin = "2.2.20" +kotlin = "2.3.10" kotlinx-serialization = "1.11.0" lombok = "1.18.46" maven-plugin-annotations = "3.15.2" diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt index c091f0b25..b56bb9180 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt @@ -12,7 +12,6 @@ import org.jetbrains.kotlin.fir.analysis.checkers.declaration.* import org.jetbrains.kotlin.fir.analysis.checkers.expression.ExpressionCheckers import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirQualifiedAccessExpressionChecker import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirTypeOperatorCallChecker -import org.jetbrains.kotlin.fir.analysis.checkers.getContainingClassSymbol import org.jetbrains.kotlin.fir.analysis.checkers.toClassLikeSymbol import org.jetbrains.kotlin.fir.analysis.extensions.FirAdditionalCheckersExtension import org.jetbrains.kotlin.fir.declarations.* @@ -20,6 +19,7 @@ import org.jetbrains.kotlin.fir.expressions.FirQualifiedAccessExpression import org.jetbrains.kotlin.fir.expressions.FirTypeOperatorCall import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference import org.jetbrains.kotlin.fir.resolve.calls.FirSyntheticFunctionSymbol +import org.jetbrains.kotlin.fir.resolve.getContainingClassSymbol import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider import org.jetbrains.kotlin.fir.resolve.toClassLikeSymbol import org.jetbrains.kotlin.fir.symbols.impl.FirAnonymousObjectSymbol diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCommandLineProcessor.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCommandLineProcessor.kt index 45bb987b2..601511099 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCommandLineProcessor.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCommandLineProcessor.kt @@ -15,9 +15,11 @@ val KEY_SOURCES = CompilerConfigurationKey(VAL_SOURCES) const val VAL_TARGET = "targetroot" val KEY_TARGET = CompilerConfigurationKey(VAL_TARGET) +const val PLUGIN_ID = "scip-kotlinc" + @OptIn(ExperimentalCompilerApi::class) class AnalyzerCommandLineProcessor : CommandLineProcessor { - override val pluginId: String = "scip-kotlinc" + override val pluginId: String = PLUGIN_ID override val pluginOptions: Collection = listOf( CliOption( diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt index 289a0cf4e..0b05005e4 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt @@ -26,6 +26,8 @@ class AnalyzerRegistrar(private val callback: (Document) -> Unit = {}) : Compile ) } + override val pluginId = PLUGIN_ID + override val supportsK2: Boolean get() = true } diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt index 04469bd1b..73e4be90c 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt @@ -2,13 +2,13 @@ package org.scip_code.scip_java.kotlinc import java.lang.System.err import org.jetbrains.kotlin.fir.analysis.checkers.declaration.isLocalMember -import org.jetbrains.kotlin.fir.analysis.checkers.getContainingSymbol import org.jetbrains.kotlin.fir.declarations.DirectDeclarationsAccess import org.jetbrains.kotlin.fir.declarations.FirClass import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin import org.jetbrains.kotlin.fir.declarations.utils.memberDeclarationNameOrNull import org.jetbrains.kotlin.fir.packageFqName import org.jetbrains.kotlin.fir.resolve.getContainingDeclaration +import org.jetbrains.kotlin.fir.resolve.getContainingSymbol import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol import org.jetbrains.kotlin.fir.symbols.SymbolInternals diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt index 4576e9893..0d3da7c4a 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt @@ -200,6 +200,8 @@ fun scipVisitorAnalyzer( ) } + override val pluginId = PLUGIN_ID + override val supportsK2: Boolean get() = true } From a46128427cf51539bea6a9605b87f7a99389eed1 Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Fri, 3 Jul 2026 15:52:32 +0200 Subject: [PATCH 08/18] scip-kotlinc: Fix compiler warnings - Remove unnecessary cast to FirClass in SymbolsCache (FirClassSymbol.fir already returns FirClass) - Use parameterless toClassLikeSymbol() overload in AnalyzerCheckers - Remove unnecessary inline modifier from test snippet Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/eb683777e27a40bee4771b5b1200aaf83ef71bae --- .../kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt | 2 +- .../main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt | 2 +- .../kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt index b56bb9180..3d8cf2278 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt @@ -405,7 +405,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio resolvedSymbol is FirSyntheticFunctionSymbol ) { val referencedKlass = - resolvedSymbol.resolvedReturnType.toClassLikeSymbol(context.session) + resolvedSymbol.resolvedReturnType.toClassLikeSymbol() if (referencedKlass != null) { visitor?.visitClassReference( referencedKlass, diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt index 73e4be90c..ebd12e8a2 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt @@ -173,7 +173,7 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { val siblings = when (val containingSymbol = symbol.getContainingSymbol(session)) { is FirClassSymbol -> - (containingSymbol.fir as FirClass).declarations.map { it.symbol } + containingSymbol.fir.declarations.map { it.symbol } is FirFileSymbol -> containingSymbol.fir.declarations.map { it.symbol } null -> symbol.moduleData.session.symbolProvider.getTopLevelCallableSymbols( diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt index ade938cfe..7e36e926f 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt @@ -1451,7 +1451,7 @@ class AnalyzerTest { * Example method docstring * **/ - inline fun docstrings(msg: String): Int { return msg.length } + fun docstrings(msg: String): Int { return msg.length } """ .trimIndent(), ) From ffb3accd7bf375f7d3bdb632c9d803f6fc3a9a8d Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Fri, 3 Jul 2026 15:57:15 +0200 Subject: [PATCH 09/18] scip-kotlinc: Fix PostAnalysisExtension to use the compilation's message collector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously handleException constructed a fresh CompilerConfiguration() to retrieve the message collector key, which always fell back to PrintingMessageCollector(System.err) — ignoring whatever collector the actual compilation was configured with. Pass the real CompilerConfiguration into PostAnalysisExtension so that exception messages are routed through the same collector as all other diagnostics. Also change the severity from EXCEPTION to WARNING, since EXCEPTION is treated as isError=true by the Kotlin compiler and would cause the build to fail — contrary to the plugin's intentional "log-and-continue" behaviour. The exception test now captures compiler output via result.messages and asserts that the warning is actually emitted with the expected content, rather than only checking the exit code. Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/e6b72f5267de9f28813594a5cd290437b4d67d56 --- .../scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt | 1 + .../scip_java/kotlinc/PostAnalysisExtension.kt | 12 ++++++------ .../scip_code/scip_java/kotlinc/test/AnalyzerTest.kt | 4 ++++ .../org/scip_code/scip_java/kotlinc/test/Utils.kt | 1 + 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt index 0b05005e4..5833d8883 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt @@ -19,6 +19,7 @@ class AnalyzerRegistrar(private val callback: (Document) -> Unit = {}) : Compile FirExtensionRegistrarAdapter.registerExtension(AnalyzerFirExtensionRegistrar(options)) IrGenerationExtension.registerExtension( PostAnalysisExtension( + configuration = configuration, sourceRoot = options.sourceroot, targetRoot = options.targetroot, callback = callback, diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt index af74b5786..fb01151a5 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt @@ -25,6 +25,7 @@ import org.scip_code.scip_java.shared.ScipShardWriter * outside the source root are skipped with a stderr warning. */ class PostAnalysisExtension( + private val configuration: CompilerConfiguration, private val sourceRoot: Path, private val targetRoot: Path, private val callback: (Document) -> Unit, @@ -61,11 +62,10 @@ class PostAnalysisExtension( } private val messageCollector = - CompilerConfiguration() - .get( - CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, - PrintingMessageCollector(System.err, MessageRenderer.PLAIN_FULL_PATHS, false), - ) + configuration.get( + CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, + PrintingMessageCollector(System.err, MessageRenderer.PLAIN_FULL_PATHS, false) + ) private fun handleException(e: Exception) { val writer = @@ -74,7 +74,7 @@ class PostAnalysisExtension( val buf = StringBuffer() override fun close() = - messageCollector.report(CompilerMessageSeverity.EXCEPTION, buf.toString()) + messageCollector.report(CompilerMessageSeverity.WARNING, buf.toString()) override fun flush() = Unit diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt index 7e36e926f..c8f2bd055 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt @@ -7,6 +7,7 @@ import io.kotest.assertions.fail import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain import java.io.File import java.nio.file.Path import kotlin.test.Test @@ -807,6 +808,7 @@ class AnalyzerTest { compilerPluginRegistrars = listOf(AnalyzerRegistrar { throw Exception("sample text") }) verbose = false + messageOutputStream = java.io.OutputStream.nullOutputStream() pluginOptions = listOf( PluginOption("scip-kotlinc", "sourceroot", path.toString()), @@ -818,6 +820,8 @@ class AnalyzerTest { .compile() result.exitCode shouldBe KotlinCompilation.ExitCode.OK + result.messages shouldContain "Exception in scip-kotlin compiler plugin:" + result.messages shouldContain "sample text" } @Test diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt index 0d3da7c4a..5622fe594 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt @@ -193,6 +193,7 @@ fun scipVisitorAnalyzer( ) IrGenerationExtension.registerExtension( PostAnalysisExtension( + configuration = configuration, sourceRoot = sourceroot, targetRoot = Paths.get(""), callback = hook, From c4fdd4d878f1f6be2ef230fa705211f82688fb12 Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Fri, 3 Jul 2026 16:06:37 +0200 Subject: [PATCH 10/18] scip-kotlinc: Add test for lambda parameters This is a test for 88f6272784c5e56eb135019a4b0dda5dd0808017. Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/2c1f17047f1cb4ed15e0f53332737f82cbbcdc8b --- .../scip_java/kotlinc/test/AnalyzerTest.kt | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt index c8f2bd055..86621cb6f 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt @@ -315,6 +315,88 @@ class AnalyzerTest { document.symbolsList.shouldContainAll(*symbols) } + @Test + fun `lambda parameters`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + fun use() { + val f = { n: Int -> n * 2 } + } + """ + ) + + val occurrences = + arrayOf( + // val f is a local variable — gets local0 via isLocalMember + scipOccurrence { + role = DEFINITION + symbol = "local 0" + range { startLine = 3; startCharacter = 8; endLine = 3; endCharacter = 9 } + enclosingRange { + startLine = 3 + startCharacter = 4 + endLine = 3 + endCharacter = 31 + } + }, + // n is a lambda parameter — gets local1 via the owner == Symbol.NONE path + // (the containing FirAnonymousFunction is skipped, yielding Symbol.NONE as owner) + scipOccurrence { + role = DEFINITION + symbol = "local 1" + range { startLine = 3; startCharacter = 14; endLine = 3; endCharacter = 15 } + enclosingRange { + startLine = 3 + startCharacter = 14 + endLine = 3 + endCharacter = 20 + } + }, + // explicit type annotation on n emits a class reference + scipOccurrence { + role = REFERENCE + symbol = "kotlin/Int#" + range { startLine = 3; startCharacter = 17; endLine = 3; endCharacter = 20 } + }, + // reference to n in the lambda body uses the same local symbol + scipOccurrence { + role = REFERENCE + symbol = "local 1" + range { startLine = 3; startCharacter = 24; endLine = 3; endCharacter = 25 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/use()." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "use" + signatureText = "public final fun use(): Unit" + }, + scipSymbol { + symbol = "local 0" + kind = Kind.Property + enclosingSymbol = "sample/use()." + displayName = "f" + signatureText = "local val f: (Int) -> Int" + }, + scipSymbol { + symbol = "local 1" + kind = Kind.Parameter + displayName = "n" + signatureText = "n: Int" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + @Test fun overrides(@TempDir path: Path) { val document = From b426a817b7378ed3351c491b996ac8aea173a754 Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Fri, 3 Jul 2026 16:18:26 +0200 Subject: [PATCH 11/18] scip-kotlinc: Add tests for local functions and user-defined class return types - local functions: verifies that named functions declared inside a function body receive local symbols (local0, local1), that explicit return type references are emitted (kotlin/Int#), and that call-site references resolve to the same local symbol. Also adds a SemanticdbSymbolsTest entry confirming the locals counter increments correctly. - user-defined class as return type: verifies that SemanticSimple- FunctionChecker emits a REFERENCE occurrence for a user-defined class appearing in the return-type position (sample/MyClass#), exercising the returnTypeRef path that built-in types do not reach. Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/ff23a7ec482dcd7ab2d79ac3004ffb91e2d92d60 --- .../scip_java/kotlinc/test/AnalyzerTest.kt | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt index 86621cb6f..8508d2284 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt @@ -397,6 +397,142 @@ class AnalyzerTest { document.symbolsList.shouldContainAll(*symbols) } + @Test + fun `local functions`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + fun outer() { + fun inner() {} + fun innerWithReturnType(): Int = 42 + inner() + } + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/outer()." + range { startLine = 2; startCharacter = 4; endLine = 2; endCharacter = 9 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 6 + endCharacter = 1 + } + }, + // inner() — local named function gets a local symbol + scipOccurrence { + role = DEFINITION + symbol = "local 0" + range { startLine = 3; startCharacter = 8; endLine = 3; endCharacter = 13 } + enclosingRange { + startLine = 3 + startCharacter = 4 + endLine = 3 + endCharacter = 18 + } + }, + // innerWithReturnType() — local named function with explicit return type + scipOccurrence { + role = DEFINITION + symbol = "local 1" + range { startLine = 4; startCharacter = 8; endLine = 4; endCharacter = 27 } + enclosingRange { + startLine = 4 + startCharacter = 4 + endLine = 4 + endCharacter = 39 + } + }, + // Int return-type reference + scipOccurrence { + role = REFERENCE + symbol = "kotlin/Int#" + range { startLine = 4; startCharacter = 31; endLine = 4; endCharacter = 34 } + }, + // call site inner() references the same local symbol + scipOccurrence { + role = REFERENCE + symbol = "local 0" + range { startLine = 5; startCharacter = 4; endLine = 5; endCharacter = 9 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "local 0" + kind = Kind.Method + enclosingSymbol = "sample/outer()." + displayName = "inner" + signatureText = "local final fun inner(): Unit" + }, + scipSymbol { + symbol = "local 1" + kind = Kind.Method + enclosingSymbol = "sample/outer()." + displayName = "innerWithReturnType" + signatureText = "local final fun innerWithReturnType(): Int" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `user-defined class as return type`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + class MyClass + + fun bar(): MyClass = MyClass() + """ + ) + + val occurrences = + arrayOf ( + scipOccurrence { + role = DEFINITION + symbol = "sample/bar()." + range { startLine = 4; startCharacter = 4; endLine = 4; endCharacter = 7 } + enclosingRange { + startLine = 4 + startCharacter = 0 + endLine = 4 + endCharacter = 30 + } + }, + // MyClass in the return type position generates a class reference + scipOccurrence { + role = REFERENCE + symbol = "sample/MyClass#" + range { startLine = 4; startCharacter = 11; endLine = 4; endCharacter = 18 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/bar()." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "bar" + signatureText = "public final fun bar(): MyClass" + }) + document.symbolsList.shouldContainAll(*symbols) + } + @Test fun overrides(@TempDir path: Path) { val document = From 4f522978885a12028443aa872b60a880f07086db Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Fri, 3 Jul 2026 16:20:29 +0200 Subject: [PATCH 12/18] scip-kotlinc: Fix displayName() for type aliases and type parameters displayName() in SemanticdbTextDocumentBuilder fell through to `firBasedSymbol.toString()` for two symbol types, producing strings like "FirTypeAliasSymbol sample/MyAlias" and "FirTypeParameterSymbol T" instead of the short name. - Broaden the FirClassSymbol branch to FirClassLikeSymbol so that FirTypeAliasSymbol (a subtype of FirClassLikeSymbol but not of FirClassSymbol) also uses classId.shortClassName. - Add an explicit FirTypeParameterSymbol branch that returns symbol.name.asString(). Add tests that exercise the corrected paths: - typealias: first test for SemanticTypeAliasChecker; verifies the DEFINITION occurrence and SymbolInformation (including the now-correct displayName "MyAlias"). Notes that type-alias references in value declarations are not currently tracked (the property checker resolves aliases to their expansion). - type parameters: first test for SemanticTypeParameterChecker; verifies the DEFINITION occurrence and SymbolInformation (displayName "T"). Notes that T in type-annotation positions does not produce REFERENCE occurrences because toClassLikeSymbol() returns null for type parameters. Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/20b1d20d53232a00388a6036cf39caf484a71b99 --- .../kotlinc/ScipTextDocumentBuilder.kt | 4 +- .../scip_java/kotlinc/test/AnalyzerTest.kt | 110 ++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt index 5e4cb6fdd..6054b63a9 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt @@ -168,6 +168,7 @@ class ScipTextDocumentBuilder( private fun scipKind(element: FirElement?): Kind = when (element) { is FirClass if element.isInterface -> Kind.Interface + is FirTypeAlias -> Kind.TypeAlias is FirClassLikeDeclaration -> Kind.Class is FirConstructor -> Kind.Constructor is FirTypeParameter -> Kind.TypeParameter @@ -208,11 +209,12 @@ class ScipTextDocumentBuilder( @OptIn(SymbolInternals::class, RenderingInternals::class) private fun displayName(firBasedSymbol: FirBasedSymbol<*>): String = when (firBasedSymbol) { - is FirClassSymbol -> firBasedSymbol.classId.shortClassName.asString() + is FirClassLikeSymbol -> firBasedSymbol.classId.shortClassName.asString() is FirPropertyAccessorSymbol -> firBasedSymbol.fir.propertySymbol.name.asString() is FirFunctionSymbol -> firBasedSymbol.callableId.callableName.asString() is FirPropertySymbol -> firBasedSymbol.callableIdForRendering.callableName.asString() is FirVariableSymbol -> firBasedSymbol.name.asString() + is FirTypeParameterSymbol -> firBasedSymbol.name.asString() else -> firBasedSymbol.toString() } } diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt index 8508d2284..b09005bfa 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt @@ -17,6 +17,7 @@ import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.junit.jupiter.api.io.TempDir import org.scip_code.scip.Document import org.scip_code.scip.SymbolInformation.Kind +import org.scip_code.scip.symbolInformation import org.scip_code.scip_java.kotlinc.* @OptIn(ExperimentalCompilerApi::class) @@ -533,6 +534,115 @@ class AnalyzerTest { document.symbolsList.shouldContainAll(*symbols) } + @Test + fun `typealias`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + typealias MyAlias = Int + val x: MyAlias = 42 + """ + ) + + val occurrences = + arrayOf ( + scipOccurrence { + role = DEFINITION + symbol = "sample/MyAlias#" + range { startLine = 2; startCharacter = 10; endLine = 2; endCharacter = 17 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 23 + } + }, + // Note: val x: MyAlias does not emit a REFERENCE for sample/MyAlias# because the + // property checker resolves the type alias to its expansion (Int). + scipOccurrence { + role = DEFINITION + symbol = "sample/x." + range { startLine = 3; startCharacter = 4; endLine = 3; endCharacter = 5 } + enclosingRange { + startLine = 3 + startCharacter = 0 + endLine = 3 + endCharacter = 19 + } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/MyAlias#" + kind = Kind.TypeAlias + enclosingSymbol = "sample/" + displayName = "MyAlias" + signatureText = "public final typealias MyAlias = Int\n" + }) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `type parameters`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + fun identity(x: T): T = x + """ + ) + + val occurrences = + arrayOf ( + scipOccurrence { + role = DEFINITION + symbol = "sample/identity()." + range { startLine = 2; startCharacter = 8; endLine = 2; endCharacter = 16 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 29 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/identity().[T]" + range { startLine = 2; startCharacter = 5; endLine = 2; endCharacter = 6 } + enclosingRange { + startLine = 2 + startCharacter = 5 + endLine = 2 + endCharacter = 6 + } + }, + // Note: T in type-annotation positions (x: T and return type : T) does not produce + // REFERENCE occurrences. The checkers use toClassLikeSymbol() to detect type + // references, which returns null for type parameters, so those usages are not + // currently tracked. + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/identity().[T]" + kind = Kind.TypeParameter + enclosingSymbol = "sample/identity()." + displayName = "T" + signatureText = "T" + }) + document.symbolsList.shouldContainAll(*symbols) + } + @Test fun overrides(@TempDir path: Path) { val document = From 1d83c74e6caeadaa69796bc554afad1068e9553a Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Fri, 3 Jul 2026 17:12:41 +0200 Subject: [PATCH 13/18] scip-kotlinc: Fix extension receiver, enum entry, and is/as type occurrences - Fix SemanticSimpleFunctionChecker and SemanticPropertyChecker to emit REFERENCE occurrences for extension receiver types - Add SemanticEnumEntryChecker to emit DEFINITION occurrences for enum entries (previously missing from AnalyzerDeclarationCheckers) - Fix semanticdbKind() to return Kind.ENUM_MEMBER for FirEnumEntry (FirEnumEntry extends FirVariable, which would otherwise map to Kind.LOCAL) - Fix SemanticClassReferenceExpressionChecker (is/as operators) to use the already-extracted typeRef and source locals consistently rather than re-accessing expression.conversionTypeRef on each line - Extract emitTypeRef() helper to reduce duplication across checkers; use it for supertype references in SemanticClassLikeChecker so they get the same fake-source guard as other type reference emissions - Add tests for extension receivers, enum entries, named/unnamed companion objects, string template references, and is/as type references Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/d823d350cee7d182706781e6e8c5c32582960d9b --- .../scip_java/kotlinc/AnalyzerCheckers.kt | 118 ++-- .../kotlinc/ScipTextDocumentBuilder.kt | 1 + .../scip_java/kotlinc/ScipVisitor.kt | 11 + .../scip_java/kotlinc/SymbolsCache.kt | 81 ++- .../scip_java/kotlinc/test/AnalyzerTest.kt | 595 +++++++++++++++++- 5 files changed, 754 insertions(+), 52 deletions(-) diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt index 3d8cf2278..e1ff7a631 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt @@ -11,11 +11,14 @@ import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext import org.jetbrains.kotlin.fir.analysis.checkers.declaration.* import org.jetbrains.kotlin.fir.analysis.checkers.expression.ExpressionCheckers import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirQualifiedAccessExpressionChecker +import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirResolvedQualifierChecker import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirTypeOperatorCallChecker import org.jetbrains.kotlin.fir.analysis.checkers.toClassLikeSymbol import org.jetbrains.kotlin.fir.analysis.extensions.FirAdditionalCheckersExtension import org.jetbrains.kotlin.fir.declarations.* +import org.jetbrains.kotlin.fir.declarations.utils.isCompanion import org.jetbrains.kotlin.fir.expressions.FirQualifiedAccessExpression +import org.jetbrains.kotlin.fir.expressions.FirResolvedQualifier import org.jetbrains.kotlin.fir.expressions.FirTypeOperatorCall import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference import org.jetbrains.kotlin.fir.resolve.calls.FirSyntheticFunctionSymbol @@ -24,6 +27,7 @@ import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider import org.jetbrains.kotlin.fir.resolve.toClassLikeSymbol import org.jetbrains.kotlin.fir.symbols.impl.FirAnonymousObjectSymbol import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol +import org.jetbrains.kotlin.fir.types.FirTypeRef import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName @@ -36,6 +40,15 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio element.treeStructure .findChildByType(element.lighterASTNode, KtTokens.IDENTIFIER) ?.toKtLightSourceElement(element.treeStructure) ?: element + + context(context: CheckerContext) + private fun ScipVisitor.emitTypeRef(typeRef: FirTypeRef) { + val klass = typeRef.toClassLikeSymbol(context.session) + val source = typeRef.source + if (klass != null && source != null && source.kind !is KtFakeSourceElementKind) { + visitClassReference(klass, getIdentifier(source)) + } + } } override val declarationCheckers: DeclarationCheckers @@ -48,8 +61,12 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio Set = setOf(SemanticQualifiedAccessExpressionChecker()) + override val resolvedQualifierCheckers: + Set = + setOf(SemanticResolvedQualifierChecker()) + override val typeOperatorCallCheckers: - Set = + Set = setOf(SemanticClassReferenceExpressionChecker()) } @@ -71,6 +88,8 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio override val typeAliasCheckers: Set = setOf(SemanticTypeAliasChecker()) override val propertyAccessorCheckers: Set = setOf(SemanticPropertyAccessorChecker()) + override val enumEntryCheckers: Set = + setOf(SemanticEnumEntryChecker()) } private class SemanticFileChecker(private val sourceroot: Path) : @@ -189,19 +208,29 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio } else { null } + val identifierSource = getIdentifier(source) + // For unnamed companion objects, getIdentifier() falls back to source (no IDENTIFIER + // token). Use the 'companion' keyword as the range instead. The COMPANION_KEYWORD is + // inside a MODIFIER_LIST child, so we use findDescendantByType instead of + // findChildByType. + val companionKeyword = + if (identifierSource === source && declaration is FirRegularClass && declaration.isCompanion) { + source + .treeStructure + .findDescendantByType(source.lighterASTNode, KtTokens.COMPANION_KEYWORD) + ?.toKtLightSourceElement(source.treeStructure) + } else { + null + } visitor?.visitClassOrObject( declaration, - objectKeyword ?: getIdentifier(source), - enclosingSource = source, + objectKeyword ?: companionKeyword ?: identifierSource, + enclosingSource = source ) if (declaration is FirClass) { for (superType in declaration.superTypeRefs) { - val superSymbol = superType.toClassLikeSymbol(context.session) - val superSource = superType.source - if (superSymbol != null && superSource != null) { - visitor?.visitClassReference(superSymbol, superSource) - } + visitor?.emitTypeRef(superType) } } } @@ -260,14 +289,8 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio getIdentifier(source), enclosingSource = source, ) - - val klass = declaration.returnTypeRef.toClassLikeSymbol(context.session) - val klassSource = declaration.returnTypeRef.source - if ( - klass != null && klassSource != null && klassSource.kind !is KtFakeSourceElementKind - ) { - visitor?.visitClassReference(klass, getIdentifier(klassSource)) - } + visitor?.emitTypeRef(declaration.returnTypeRef) + declaration.receiverParameter?.typeRef?.let { visitor?.emitTypeRef(it) } } } @@ -293,14 +316,8 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio getIdentifier(source), enclosingSource = source, ) - - val klass = declaration.returnTypeRef.toClassLikeSymbol(context.session) - val klassSource = declaration.returnTypeRef.source - if ( - klass != null && klassSource != null && klassSource.kind !is KtFakeSourceElementKind - ) { - visitor?.visitClassReference(klass, getIdentifier(klassSource)) - } + visitor?.emitTypeRef(declaration.returnTypeRef) + declaration.receiverParameter?.typeRef?.let { visitor?.emitTypeRef(it) } } } @@ -315,14 +332,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio getIdentifier(source), enclosingSource = source, ) - - val klass = declaration.returnTypeRef.toClassLikeSymbol(context.session) - val klassSource = declaration.returnTypeRef.source - if ( - klass != null && klassSource != null && klassSource.kind !is KtFakeSourceElementKind - ) { - visitor?.visitClassReference(klass, getIdentifier(klassSource)) - } + visitor?.emitTypeRef(declaration.returnTypeRef) } } @@ -382,6 +392,33 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio } } + private class SemanticEnumEntryChecker : FirEnumEntryChecker(MppCheckerKind.Common) { + context(context: CheckerContext, reporter: DiagnosticReporter) + override fun check(declaration: FirEnumEntry) { + val source = declaration.source ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return + val visitor = visitors[ktFile] + visitor?.visitEnumEntry( + declaration, + getIdentifier(source), + enclosingSource = source, + ) + } + } + + private class SemanticResolvedQualifierChecker : + FirResolvedQualifierChecker(MppCheckerKind.Common) { + context(context: CheckerContext, reporter: DiagnosticReporter) + override fun check(expression: FirResolvedQualifier) { + val symbol = expression.symbol ?: return + val source = expression.source ?: return + if (source.kind is KtFakeSourceElementKind) return + val ktFile = context.containingFileSymbol?.sourceFile ?: return + val visitor = visitors[ktFile] + visitor?.visitClassReference(symbol, getIdentifier(source)) + } + } + private class SemanticQualifiedAccessExpressionChecker : FirQualifiedAccessExpressionChecker(MppCheckerKind.Common) { context(context: CheckerContext, reporter: DiagnosticReporter) @@ -394,10 +431,8 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] - visitor?.visitSimpleNameExpression( - calleeReference, - getIdentifier(calleeReference.source ?: source), - ) + val identifierSource = getIdentifier(calleeReference.source ?: source) + visitor?.visitSimpleNameExpression(calleeReference, identifierSource) val resolvedSymbol = calleeReference.resolvedSymbol if ( @@ -409,7 +444,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio if (referencedKlass != null) { visitor?.visitClassReference( referencedKlass, - getIdentifier(calleeReference.source ?: source), + identifierSource, ) } } @@ -420,13 +455,13 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio resolvedSymbol.getterSymbol?.let { visitor?.visitCallableReference( it, - getIdentifier(calleeReference.source ?: source), + identifierSource, ) } resolvedSymbol.setterSymbol?.let { visitor?.visitCallableReference( it, - getIdentifier(calleeReference.source ?: source), + identifierSource, ) } } @@ -439,14 +474,13 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio override fun check(expression: FirTypeOperatorCall) { val typeRef = expression.conversionTypeRef val source = typeRef.source ?: return - val classSymbol = - expression.conversionTypeRef.toClassLikeSymbol(context.session) ?: return + val classSymbol = typeRef.toClassLikeSymbol(context.session) ?: return val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitClassReference( classSymbol, - getIdentifier(expression.conversionTypeRef.source ?: source), + getIdentifier(source), ) } } diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt index 6054b63a9..b3e315d61 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt @@ -175,6 +175,7 @@ class ScipTextDocumentBuilder( is FirValueParameter -> Kind.Parameter is FirField -> Kind.Field is FirProperty -> Kind.Property + is FirEnumEntry -> Kind.EnumMember is FirVariable -> Kind.Variable is FirCallableDeclaration -> Kind.Method is FirPackageDirective -> Kind.Package diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt index 78075f131..5525e607c 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt @@ -174,6 +174,17 @@ class ScipVisitor( .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) + fun visitEnumEntry( + firEnumEntry: FirEnumEntry, + source: KtSourceElement, + enclosingSource: KtSourceElement? = null, + ) { + cache[firEnumEntry.symbol] + .with(firEnumEntry.symbol) + .emitAll(source, isDefinition = true, enclosingSource) + } + context(context: CheckerContext) fun visitSimpleNameExpression( firResolvedNamedReference: FirResolvedNamedReference, diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt index ebd12e8a2..4f759c3b4 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt @@ -6,6 +6,8 @@ import org.jetbrains.kotlin.fir.declarations.DirectDeclarationsAccess import org.jetbrains.kotlin.fir.declarations.FirClass import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin import org.jetbrains.kotlin.fir.declarations.utils.memberDeclarationNameOrNull +import org.jetbrains.kotlin.fir.types.classId +import org.jetbrains.kotlin.fir.types.coneType import org.jetbrains.kotlin.fir.packageFqName import org.jetbrains.kotlin.fir.resolve.getContainingDeclaration import org.jetbrains.kotlin.fir.resolve.getContainingSymbol @@ -117,8 +119,29 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { is FirPropertyAccessorSymbol -> return getSymbol(symbol.propertySymbol, locals) is FirCallableSymbol -> { val session = symbol.fir.moduleData.session - return symbol.getContainingSymbol(session)?.let { getSymbol(it, locals) } - ?: getSymbol(symbol.packageFqName()) + val containingSymbol = symbol.getContainingSymbol(session) + // For top-level extension functions/properties (containingSymbol is file or null), + // use the receiver type as a synthetic parent within the package + // (e.g. sample/String#foo().). + if (containingSymbol == null || containingSymbol is FirFileSymbol) { + val receiverClassId = + symbol.fir + .receiverParameter + ?.typeRef + ?.coneType + ?.classId + if (receiverClassId != null) { + val packageSymbol = getSymbol(symbol.packageFqName()) + return Symbol.createGlobal( + packageSymbol, + ScipSymbolDescriptor( + Kind.TYPE, receiverClassId.shortClassName.asString() + ) + ) + } + } + containingSymbol?.let { return getSymbol(it, locals) } + return getSymbol(symbol.packageFqName()) } is FirClassLikeSymbol -> { val session = symbol.fir.moduleData.session @@ -174,12 +197,48 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { when (val containingSymbol = symbol.getContainingSymbol(session)) { is FirClassSymbol -> containingSymbol.fir.declarations.map { it.symbol } - is FirFileSymbol -> containingSymbol.fir.declarations.map { it.symbol } - null -> - symbol.moduleData.session.symbolProvider.getTopLevelCallableSymbols( - symbol.packageFqName(), - symbol.name, - ) + is FirFileSymbol, null -> { + // For top-level extension functions, siblings are the receiver class members + // (if in the same package) followed by other extension functions on the same + // receiver type in this package. This ensures consistent disambiguation + // when both a class member and an extension share the same parent namespace + // (e.g. sample/MyClass#foo(). vs sample/MyClass#foo(+1).). + val receiverClassId = + symbol.fir + .receiverParameter + ?.typeRef + ?.coneType + ?.classId + if (receiverClassId != null) { + val receiverClass = + session.symbolProvider.getClassLikeSymbolByClassId(receiverClassId) + as? FirClassSymbol<*> + val classMembers = + if (receiverClass?.packageFqName() == symbol.packageFqName()) { + receiverClass.fir.declarations.map { it.symbol } + } else { + emptyList() + } + val extensionFns = + session.symbolProvider + .getTopLevelCallableSymbols(symbol.packageFqName(), symbol.name) + .filter { + it is FirFunctionSymbol<*> && + it.fir + .receiverParameter + ?.typeRef + ?.coneType + ?.classId == receiverClassId + } + classMembers + extensionFns + } else if (containingSymbol is FirFileSymbol) { + containingSymbol.fir.declarations.map { it.symbol } + } else { + session.symbolProvider.getTopLevelCallableSymbols( + symbol.packageFqName(), symbol.name + ) + } + } else -> return "()" } @@ -196,7 +255,11 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { } } - if (count == 0 || !found) return "()" + if (!found) { + err.println("methodDisambiguator: ${symbol.callableId} not found in sibling list") + return "()" + } + if (count == 0) return "()" return "(+${count})" } diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt index b09005bfa..e59974546 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt @@ -1791,10 +1791,603 @@ class AnalyzerTest { document.assertDocumentation("sample/docstrings().", "Example method docstring") } + @Test + fun `extension receiver type reference`(@TempDir path: Path) { + // String is from the kotlin package; our extension in sample gets + // symbol sample/String#foo(). — distinct from kotlin/String#foo(). + // This means extensions on cross-package types never collide with + // the receiver class's own methods in the symbol table. + val document = + compileScip( + path, + """ + package sample + + fun String.foo(): Int = 42 + fun use(s: String) = s.foo() + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/String#foo()." + range { startLine = 2; startCharacter = 11; endLine = 2; endCharacter = 14 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 26 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "kotlin/String#" + range { startLine = 2; startCharacter = 4; endLine = 2; endCharacter = 10 } + }, + scipOccurrence { + role = REFERENCE + symbol = "kotlin/Int#" + range { startLine = 2; startCharacter = 18; endLine = 2; endCharacter = 21 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/String#foo()." + range { startLine = 3; startCharacter = 23; endLine = 3; endCharacter = 26 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/String#foo()." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "foo" + signatureText = "public final fun String.foo(): Int" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `extension property receiver type reference`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + val Int.asString: String get() = this.toString() + fun use() = 42.asString + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Int#asString." + range { startLine = 2; startCharacter = 8; endLine = 2; endCharacter = 16 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 48 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "kotlin/Int#" + range { startLine = 2; startCharacter = 4; endLine = 2; endCharacter = 7 } + }, + scipOccurrence { + role = REFERENCE + symbol = "kotlin/String#" + range { startLine = 2; startCharacter = 18; endLine = 2; endCharacter = 24 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Int#asString." + range { startLine = 3; startCharacter = 15; endLine = 3; endCharacter = 23 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/Int#asString." + kind = Kind.Property + enclosingSymbol = "sample/" + displayName = "asString" + signatureText = "public final val Int.asString: String" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `extension overload disambiguator`(@TempDir path: Path) { + // When a class already has a member named foo() and an extension also + // adds foo(), the extension is counted after the member in the combined + // sibling list (class members + same-package extensions on the same + // receiver type), so the extension gets the (+1) disambiguator and the + // two produce distinct symbols. + val document = + compileScip( + path, + """ + package sample + + class MyClass { + fun foo() {} + } + fun MyClass.foo(x: Int) {} + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/MyClass#foo()." + range { startLine = 3; startCharacter = 8; endLine = 3; endCharacter = 11 } + enclosingRange { + startLine = 3 + startCharacter = 4 + endLine = 3 + endCharacter = 16 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/MyClass#foo(+1)." + range { startLine = 5; startCharacter = 12; endLine = 5; endCharacter = 15 } + enclosingRange { + startLine = 5 + startCharacter = 0 + endLine = 5 + endCharacter = 26 + } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + } + + @Test + fun `enum entry definitions`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + enum class Color { RED, GREEN, BLUE } + + fun useEnum(): Color = Color.RED + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Color#" + range { startLine = 2; startCharacter = 11; endLine = 2; endCharacter = 16 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 37 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Color#RED." + range { startLine = 2; startCharacter = 19; endLine = 2; endCharacter = 22 } + enclosingRange { + startLine = 2 + startCharacter = 19 + endLine = 2 + endCharacter = 23 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Color#GREEN." + range { startLine = 2; startCharacter = 24; endLine = 2; endCharacter = 29 } + enclosingRange { + startLine = 2 + startCharacter = 24 + endLine = 2 + endCharacter = 30 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Color#BLUE." + range { startLine = 2; startCharacter = 31; endLine = 2; endCharacter = 35 } + enclosingRange { + startLine = 2 + startCharacter = 31 + endLine = 2 + endCharacter = 35 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Color#" + range { startLine = 4; startCharacter = 23; endLine = 4; endCharacter = 28 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Color#RED." + range { startLine = 4; startCharacter = 29; endLine = 4; endCharacter = 32 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/Color#" + kind = Kind.Class + enclosingSymbol = "sample/" + displayName = "Color" + addOverriddenSymbols("kotlin/Enum#") + signatureText = "public final enum class Color : Enum" + }, + scipSymbol { + symbol = "sample/Color#RED." + kind = Kind.EnumMember + enclosingSymbol = "sample/Color#" + displayName = "RED" + signatureText = "public final static enum entry RED: Color" + }, + scipSymbol { + symbol = "sample/Color#GREEN." + kind = Kind.EnumMember + enclosingSymbol = "sample/Color#" + displayName = "GREEN" + signatureText = "public final static enum entry GREEN: Color" + }, + scipSymbol { + symbol = "sample/Color#BLUE." + kind = Kind.EnumMember + enclosingSymbol = "sample/Color#" + displayName = "BLUE" + signatureText = "public final static enum entry BLUE: Color" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `named object declarations`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + object MySingleton { + fun hello(): String = "hi" + } + fun use() = MySingleton.hello() + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/MySingleton#" + range { startLine = 2; startCharacter = 7; endLine = 2; endCharacter = 18 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 4 + endCharacter = 1 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/MySingleton#hello()." + range { startLine = 3; startCharacter = 8; endLine = 3; endCharacter = 13 } + enclosingRange { + startLine = 3 + startCharacter = 4 + endLine = 3 + endCharacter = 30 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/MySingleton#" + range { startLine = 5; startCharacter = 12; endLine = 5; endCharacter = 23 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/MySingleton#hello()." + range { startLine = 5; startCharacter = 24; endLine = 5; endCharacter = 29 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/MySingleton#" + kind = Kind.Class + enclosingSymbol = "sample/" + displayName = "MySingleton" + signatureText = "public final object MySingleton : Any" + }, + scipSymbol { + symbol = "sample/MySingleton#hello()." + kind = Kind.Method + enclosingSymbol = "sample/MySingleton#" + displayName = "hello" + signatureText = "public final fun hello(): String" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `companion object`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + class Foo { + companion object Factory { + fun create(): Foo = Foo() + } + } + fun use() = Foo.Factory.create() + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Foo#" + range { startLine = 2; startCharacter = 6; endLine = 2; endCharacter = 9 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 6 + endCharacter = 1 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Foo#Factory#" + range { startLine = 3; startCharacter = 21; endLine = 3; endCharacter = 28 } + enclosingRange { + startLine = 3 + startCharacter = 4 + endLine = 5 + endCharacter = 5 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Foo#Factory#create()." + range { startLine = 4; startCharacter = 12; endLine = 4; endCharacter = 18 } + enclosingRange { + startLine = 4 + startCharacter = 8 + endLine = 4 + endCharacter = 33 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Foo#" + range { startLine = 7; startCharacter = 12; endLine = 7; endCharacter = 15 } + }, + // Foo.Factory is a FirResolvedQualifier spanning the full qualifier expression + scipOccurrence { + role = REFERENCE + symbol = "sample/Foo#Factory#" + range { startLine = 7; startCharacter = 12; endLine = 7; endCharacter = 23 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Foo#Factory#create()." + range { startLine = 7; startCharacter = 24; endLine = 7; endCharacter = 30 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/Foo#" + kind = Kind.Class + enclosingSymbol = "sample/" + displayName = "Foo" + signatureText = "public final class Foo : Any" + }, + scipSymbol { + symbol = "sample/Foo#Factory#" + kind = Kind.Class + enclosingSymbol = "sample/Foo#" + displayName = "Factory" + signatureText = "public final companion object Factory : Any" + }, + scipSymbol { + symbol = "sample/Foo#Factory#create()." + kind = Kind.Method + enclosingSymbol = "sample/Foo#Factory#" + displayName = "create" + signatureText = "public final fun create(): Foo" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `unnamed companion object`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + class Bar { + companion object { + fun instance(): Bar = Bar() + } + } + fun use() = Bar.instance() + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Bar#" + range { startLine = 2; startCharacter = 6; endLine = 2; endCharacter = 9 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 6 + endCharacter = 1 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Bar#Companion#instance()." + range { startLine = 4; startCharacter = 12; endLine = 4; endCharacter = 20 } + enclosingRange { + startLine = 4 + startCharacter = 8 + endLine = 4 + endCharacter = 35 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Bar#" + range { startLine = 7; startCharacter = 12; endLine = 7; endCharacter = 15 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Bar#Companion#instance()." + range { startLine = 7; startCharacter = 16; endLine = 7; endCharacter = 24 } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Bar#Companion#" + range { startLine = 3; startCharacter = 4; endLine = 3; endCharacter = 13 } + enclosingRange { + startLine = 3 + startCharacter = 4 + endLine = 5 + endCharacter = 5 + } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/Bar#" + kind = Kind.Class + enclosingSymbol = "sample/" + displayName = "Bar" + signatureText = "public final class Bar : Any" + }, + scipSymbol { + symbol = "sample/Bar#Companion#" + kind = Kind.Class + enclosingSymbol = "sample/Bar#" + displayName = "Companion" + signatureText = "public final companion object Companion : Any" + }, + scipSymbol { + symbol = "sample/Bar#Companion#instance()." + kind = Kind.Method + enclosingSymbol = "sample/Bar#Companion#" + displayName = "instance" + signatureText = "public final fun instance(): Bar" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `string template references`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + fun greet(name: String) = "Hello, ${'$'}name!" + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/greet()." + range { startLine = 2; startCharacter = 4; endLine = 2; endCharacter = 9 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 41 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/greet().(name)" + range { startLine = 2; startCharacter = 10; endLine = 2; endCharacter = 14 } + enclosingRange { + startLine = 2 + startCharacter = 10 + endLine = 2 + endCharacter = 22 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/greet().(name)" + range { startLine = 2; startCharacter = 35; endLine = 2; endCharacter = 39 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/greet()." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "greet" + signatureText = "public final fun greet(name: String): String" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + private fun Document.assertDocumentation(symbol: String, expectedDocumentation: String) { val info = this.symbolsList.find { it.symbol == symbol } - ?: fail("no SymbolInformation for symbol $symbol") + ?: fail("no scipSymbol for symbol $symbol") val obtainedDocumentation = info.documentationList.joinToString("\n").trim() assertEquals(expectedDocumentation, obtainedDocumentation) } From 43493f3266512072a620aea1376807c7f7f53ecd Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Fri, 3 Jul 2026 17:22:24 +0200 Subject: [PATCH 14/18] scip-kotlinc: Add tests for multiple supertypes and overload disambiguators - multiple supertype references: verifies that SemanticClassLikeChecker emits REFERENCE occurrences for every entry in superTypeRefs, and that SymbolInformation.overriddenSymbols is populated for all supertypes - three-way overload disambiguator: extends the existing two-overload coverage to three, verifying the (+1)/(+2) suffix counting logic in methodDisambiguator() Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/24b64333c0ac45cd0a6bb1021423881133d2e755 --- .../scip_java/kotlinc/test/AnalyzerTest.kt | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt index e59974546..5d6c7dd93 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt @@ -1097,6 +1097,13 @@ class AnalyzerTest { else -> x as Float } } + + class Wrapper + fun classify(x: Any) { + if (x is Wrapper) {} + val s = x as? String + val w = x as Wrapper + } """, ) @@ -1122,6 +1129,36 @@ class AnalyzerTest { endCharacter = 26 } }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Wrapper#" + range { + startLine = 11 + startCharacter = 13 + endLine = 11 + endCharacter = 20 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "kotlin/String#" + range { + startLine = 12 + startCharacter = 18 + endLine = 12 + endCharacter = 24 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Wrapper#" + range { + startLine = 13 + startCharacter = 17 + endLine = 13 + endCharacter = 24 + } + }, ) document.occurrencesList.shouldContainAll(*occurrences) } @@ -2384,6 +2421,166 @@ class AnalyzerTest { document.symbolsList.shouldContainAll(*symbols) } + @Test + fun `multiple supertype references`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + interface Named + interface Speakable + class Person : Named, Speakable + fun use(p: Person): Named = p + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Person#" + range { startLine = 4; startCharacter = 6; endLine = 4; endCharacter = 12 } + enclosingRange { + startLine = 4 + startCharacter = 0 + endLine = 4 + endCharacter = 31 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Named#" + range { startLine = 4; startCharacter = 15; endLine = 4; endCharacter = 20 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Speakable#" + range { startLine = 4; startCharacter = 22; endLine = 4; endCharacter = 31 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Named#" + range { startLine = 5; startCharacter = 20; endLine = 5; endCharacter = 25 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/Person#" + kind = Kind.Class + enclosingSymbol = "sample/" + displayName = "Person" + addOverriddenSymbols("sample/Named#") + addOverriddenSymbols("sample/Speakable#") + signatureText = "public final class Person : Named, Speakable" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `three-way overload disambiguator`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + fun add(x: Int, y: Int): Int = x + y + fun add(x: Double, y: Double): Double = x + y + fun add(x: String, y: String): String = x + y + fun use() { + add(1, 2) + add(1.0, 2.0) + add("a", "b") + } + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/add()." + range { startLine = 2; startCharacter = 4; endLine = 2; endCharacter = 7 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 36 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/add(+1)." + range { startLine = 3; startCharacter = 4; endLine = 3; endCharacter = 7 } + enclosingRange { + startLine = 3 + startCharacter = 0 + endLine = 3 + endCharacter = 45 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/add(+2)." + range { startLine = 4; startCharacter = 4; endLine = 4; endCharacter = 7 } + enclosingRange { + startLine = 4 + startCharacter = 0 + endLine = 4 + endCharacter = 45 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/add()." + range { startLine = 6; startCharacter = 4; endLine = 6; endCharacter = 7 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/add(+1)." + range { startLine = 7; startCharacter = 4; endLine = 7; endCharacter = 7 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/add(+2)." + range { startLine = 8; startCharacter = 4; endLine = 8; endCharacter = 7 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/add()." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "add" + signatureText = "public final fun add(x: Int, y: Int): Int" + }, + scipSymbol { + symbol = "sample/add(+1)." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "add" + signatureText = "public final fun add(x: Double, y: Double): Double" + }, + scipSymbol { + symbol = "sample/add(+2)." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "add" + signatureText = "public final fun add(x: String, y: String): String" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + private fun Document.assertDocumentation(symbol: String, expectedDocumentation: String) { val info = this.symbolsList.find { it.symbol == symbol } From 6cd3fe9b660112c10959b6d31fd3ab7a62bf34a2 Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Fri, 3 Jul 2026 17:24:25 +0200 Subject: [PATCH 15/18] scip-kotlinc: Fix misleading LineMap docstrings The "non-0-based" phrasing predates this branch but is misleading: line helpers return 1-based values (callers subtract 1 for protobuf), while column helpers already return 0-based values. Make the docstrings say what they actually return. Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/305951777e98bfe80dfa5a2cf159fd940dab3bec --- .../kotlin/org/scip_code/scip_java/kotlinc/LineMap.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/LineMap.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/LineMap.kt index c2da05fdf..afd51bd0a 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/LineMap.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/LineMap.kt @@ -10,22 +10,22 @@ class LineMap(private val file: FirFile) { private fun offsetToLineAndCol(offset: Int): Pair? = file.sourceFileLinesMapping?.getLineAndColumnByOffset(offset) - /** Returns the non-0-based line number for a given offset */ + /** Returns the 1-based line number for a given offset (subtract 1 for protobuf). */ fun lineNumberForOffset(offset: Int): Int = file.sourceFileLinesMapping?.getLineByOffset(offset)?.let { it + 1 } ?: 0 - /** Returns the non-0-based column number for a given offset */ + /** Returns the 0-based column number for a given offset. */ fun columnForOffset(offset: Int): Int = offsetToLineAndCol(offset)?.second ?: 0 - /** Returns the non-0-based start character */ + /** Returns the 0-based start character. */ fun startCharacter(element: KtSourceElement): Int = offsetToLineAndCol(element.startOffset)?.second ?: 0 - /** Returns the non-0-based end character */ + /** Returns the 0-based end character. */ fun endCharacter(element: KtSourceElement): Int = startCharacter(element) + nameForOffset(element).length - /** Returns the non-0-based line number */ + /** Returns the 1-based line number (subtract 1 for protobuf). */ fun lineNumber(element: KtSourceElement): Int = file.sourceFileLinesMapping?.getLineByOffset(element.startOffset)?.let { it + 1 } ?: 0 From f5de3249595fb348bfea8710c20b2c7102144ecb Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Fri, 3 Jul 2026 17:28:51 +0200 Subject: [PATCH 16/18] scip-kotlinc: Extend enclosing_range test coverage Add two test cases: - enum entry with body (Op.PLUS { override ... }): the body is modeled as a synthetic anonymous subclass, so the entry gets a multi-line enclosing_range and the overridden member surfaces as a local symbol rather than a sample/Op#PLUS.apply() global. Asserts both. - multi-line generic class declaration: general coverage we lacked for generic classes. Asserts enclosing_range for the class, its type parameter, and a member. Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/979f1f392a82c0d3cf8c17673ee3af20bb6b73c3 --- .../scip_java/kotlinc/test/AnalyzerTest.kt | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt index 5d6c7dd93..09dd17009 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt @@ -643,6 +643,44 @@ class AnalyzerTest { document.symbolsList.shouldContainAll(*symbols) } + @Test + fun `generic class declaration`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + class Box(val value: T) { + fun unwrap(): T = value + } + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Box#" + range { startLine = 2; startCharacter = 6; endLine = 2; endCharacter = 9 } + enclosingRange { startLine = 2; endLine = 4; endCharacter = 1 } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Box#[T]" + range { startLine = 2; startCharacter = 10; endLine = 2; endCharacter = 11 } + enclosingRange { startLine = 2; startCharacter = 10; endLine = 2; endCharacter = 11 } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Box#unwrap()." + range { startLine = 3; startCharacter = 8; endLine = 3; endCharacter = 14 } + enclosingRange { startLine = 3; startCharacter = 4; endLine = 3; endCharacter = 27 } + } + ) + document.occurrencesList.shouldContainAll(*occurrences) + } + @Test fun overrides(@TempDir path: Path) { val document = @@ -2101,6 +2139,71 @@ class AnalyzerTest { ) document.symbolsList.shouldContainAll(*symbols) } + + @Test + fun `enum entry with body`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + enum class Op { + PLUS { + override fun apply(a: Int, b: Int) = a + b + }; + + abstract fun apply(a: Int, b: Int): Int + } + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Op#PLUS." + range { startLine = 3; startCharacter = 4; endLine = 3; endCharacter = 8 } + // Body-having enum entry: enclosing_range covers PLUS through the + // trailing `};` that terminates the entry list. + enclosingRange { startLine = 3; startCharacter = 4; endLine = 5; endCharacter = 6 } + }, + // An enum entry with a body is modeled as a synthetic anonymous subclass, + // so its overridden member is a local symbol (local2), not a global + // sample/Op#PLUS.apply(). It still gets an enclosing_range spanning the + // function declaration. + scipOccurrence { + role = DEFINITION + symbol = "local 2" + range { startLine = 4; startCharacter = 21; endLine = 4; endCharacter = 26 } + enclosingRange { startLine = 4; startCharacter = 8; endLine = 4; endCharacter = 50 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + // The entry body becomes an anonymous class enclosed by the entry... + scipSymbol { + symbol = "local 0" + kind = Kind.Class + enclosingSymbol = "sample/Op#PLUS." + displayName = "" + addOverriddenSymbols("sample/Op#") + signatureText = "object : Op" + }, + // ...and the override is a method of that anonymous class. + scipSymbol { + symbol = "local 2" + kind = Kind.Method + enclosingSymbol = "local 0" + displayName = "apply" + addOverriddenSymbols("sample/Op#apply().") + signatureText = "public open override fun apply(a: Int, b: Int): Int" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } @Test fun `named object declarations`(@TempDir path: Path) { From 85d815c69251f0f7d0ee21404d1aa0d492393a58 Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Fri, 3 Jul 2026 17:36:07 +0200 Subject: [PATCH 17/18] Update to Kotlin 2.3.20 Changes in scip-kotlinc: - Rename FirSimpleFunction -> FirNamedFunction in SemanticSimpleFunctionChecker - Replace removed isLocalMember with isLocalDeclaredInBlock (moved within org.jetbrains.kotlin.fir.analysis.checkers.declaration) Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/832bf44ec114c14d2c65826a920d057d120dae0c --- gradle/libs.versions.toml | 2 +- .../org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt | 2 +- .../kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ee952e62..3f942792b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ gradle-api = "8.11.1" junit-jupiter = "5.11.4" kctfork = "0.12.1" kotest = "6.2.1" -kotlin = "2.3.10" +kotlin = "2.3.20" kotlinx-serialization = "1.11.0" lombok = "1.18.46" maven-plugin-annotations = "3.15.2" diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt index e1ff7a631..d3d0259ce 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt @@ -280,7 +280,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio private class SemanticSimpleFunctionChecker : FirSimpleFunctionChecker(MppCheckerKind.Common) { context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirSimpleFunction) { + override fun check(declaration: FirNamedFunction) { val source = declaration.source ?: return val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt index 4f759c3b4..c6162af21 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt @@ -1,7 +1,7 @@ package org.scip_code.scip_java.kotlinc import java.lang.System.err -import org.jetbrains.kotlin.fir.analysis.checkers.declaration.isLocalMember +import org.jetbrains.kotlin.fir.analysis.checkers.declaration.isLocalDeclaredInBlock import org.jetbrains.kotlin.fir.declarations.DirectDeclarationsAccess import org.jetbrains.kotlin.fir.declarations.FirClass import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin @@ -81,7 +81,7 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { private fun uncachedSymbol(symbol: FirBasedSymbol<*>?, locals: LocalSymbolsCache): Symbol { if (symbol == null || symbol is FirAnonymousFunctionSymbol) return Symbol.NONE - if (symbol.fir.isLocalMember) return locals + symbol + if (symbol.fir.isLocalDeclaredInBlock) return locals + symbol val owner = getParentSymbol(symbol, locals) From 5aebd5ec93f79bb24632a5254bd103724942c3bf Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Fri, 3 Jul 2026 17:38:05 +0200 Subject: [PATCH 18/18] Update to Kotlin 2.4.0 Changes to scip-kotlinc: - -Xcontext-parameters argument isn't required anymore - FirAllModifierRenderer now takes an argument Ported from https://github.com/mozsearch/semanticdb-kotlinc/commit/89427a5a132de2e706c1e84256023709cc79da51 --- gradle/libs.versions.toml | 4 ++-- scip-kotlinc/build.gradle.kts | 4 ---- .../scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3f942792b..a028c2915 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,9 +2,9 @@ clikt = "5.1.0" gradle-api = "8.11.1" junit-jupiter = "5.11.4" -kctfork = "0.12.1" +kctfork = "0.13.0" kotest = "6.2.1" -kotlin = "2.3.20" +kotlin = "2.4.0" kotlinx-serialization = "1.11.0" lombok = "1.18.46" maven-plugin-annotations = "3.15.2" diff --git a/scip-kotlinc/build.gradle.kts b/scip-kotlinc/build.gradle.kts index 699d24dd9..aad935acf 100644 --- a/scip-kotlinc/build.gradle.kts +++ b/scip-kotlinc/build.gradle.kts @@ -24,10 +24,6 @@ dependencies { testImplementation(libs.kctfork.core) } -tasks.withType().configureEach { - compilerOptions.freeCompilerArgs.add("-Xcontext-parameters") -} - tasks.named("test") { maxHeapSize = "2g" } diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt index b3e315d61..6aabbb8f4 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt @@ -152,7 +152,7 @@ class ScipTextDocumentBuilder( bodyRenderer = null, propertyAccessorRenderer = null, callArgumentsRenderer = FirCallNoArgumentsRenderer(), - modifierRenderer = FirAllModifierRenderer(), + modifierRenderer = FirAllModifierRenderer(FirModifierRenderer.StaticPolicy.Default), callableSignatureRenderer = FirCallableSignatureRendererForReadability(), declarationRenderer = FirDeclarationRenderer("local "), )