From fcad30b3971c8e1b6c3f9b85b6678332c38dc9d5 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 10:25:26 +0200 Subject: [PATCH 01/15] Add scip-kotlin-analysis: Analysis API based Kotlin indexer --- gradle/libs.versions.toml | 19 ++ scip-kotlin-analysis/build.gradle.kts | 32 +++ .../kotlin_analysis/KotlinAnalysisIndexer.kt | 231 ++++++++++++++++ .../scip_java/kotlin_analysis/ScipSymbols.kt | 73 +++++ .../scip_java/kotlin_analysis/SymbolsCache.kt | 260 ++++++++++++++++++ .../KotlinAnalysisIndexerTest.kt | 108 ++++++++ settings.gradle.kts | 5 + 7 files changed, 728 insertions(+) create mode 100644 scip-kotlin-analysis/build.gradle.kts create mode 100644 scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt create mode 100644 scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/ScipSymbols.kt create mode 100644 scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt create mode 100644 scip-kotlin-analysis/src/test/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexerTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 93d930c20..f7cbec268 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,14 @@ junit-jupiter = "5.11.4" kctfork = "0.7.1" kotest = "6.2.1" kotlin = "2.2.0" +# Kotlin Analysis API engine bundled by scip-kotlin-analysis. Independent from +# the `kotlin` toolchain version: `-for-ide` artifacts are only published as +# IDE-aligned builds (see https://redirector.kotlinlang.org/maven/kotlin-ide-plugin-dependencies). +kotlin-analysis-api = "2.2.0-ij251-98" +kotlinx-collections-immutable = "0.3.8" +# Matches the coroutines dependency declared by the kotlin-compiler POM at the +# kotlin-analysis-api version above. +kotlinx-coroutines = "1.8.0" kotlinx-serialization = "1.11.0" lombok = "1.18.46" maven-plugin-annotations = "3.15.2" @@ -18,11 +26,20 @@ spotless = "8.8.0" vanniktech-maven-publish = "0.37.0" [libraries] +caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version = "2.9.3" } clikt-jvm = { module = "com.github.ajalt.clikt:clikt-jvm", version.ref = "clikt" } gradle-api = { module = "dev.gradleplugins:gradle-api", version.ref = "gradle-api" } gradle-test-kit = { module = "dev.gradleplugins:gradle-test-kit", version.ref = "gradle-api" } kctfork-core = { module = "dev.zacsweers.kctfork:core", version.ref = "kctfork" } kotest-assertions-core = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest" } +kotlin-analysis-api-api = { module = "org.jetbrains.kotlin:analysis-api-for-ide", version.ref = "kotlin-analysis-api" } +kotlin-analysis-api-impl-base = { module = "org.jetbrains.kotlin:analysis-api-impl-base-for-ide", version.ref = "kotlin-analysis-api" } +kotlin-analysis-api-k2 = { module = "org.jetbrains.kotlin:analysis-api-k2-for-ide", version.ref = "kotlin-analysis-api" } +kotlin-analysis-api-platform-interface = { module = "org.jetbrains.kotlin:analysis-api-platform-interface-for-ide", version.ref = "kotlin-analysis-api" } +kotlin-analysis-api-standalone = { module = "org.jetbrains.kotlin:analysis-api-standalone-for-ide", version.ref = "kotlin-analysis-api" } +kotlin-analysis-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler", version.ref = "kotlin-analysis-api" } +kotlin-analysis-low-level-api-fir = { module = "org.jetbrains.kotlin:low-level-api-fir-for-ide", version.ref = "kotlin-analysis-api" } +kotlin-analysis-symbol-light-classes = { module = "org.jetbrains.kotlin:symbol-light-classes-for-ide", version.ref = "kotlin-analysis-api" } kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-scripting-common = { module = "org.jetbrains.kotlin:kotlin-scripting-common", version.ref = "kotlin" } @@ -32,6 +49,8 @@ kotlin-scripting-jvm = { module = "org.jetbrains.kotlin:kotlin-scripting-jvm", v kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" } +kotlinx-collections-immutable-jvm = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm", version.ref = "kotlinx-collections-immutable" } +kotlinx-coroutines-core-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "kotlinx-coroutines" } kotlinx-serialization-json-jvm = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version.ref = "kotlinx-serialization" } lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" } maven-plugin-annotations = { module = "org.apache.maven.plugin-tools:maven-plugin-annotations", version.ref = "maven-plugin-annotations" } diff --git a/scip-kotlin-analysis/build.gradle.kts b/scip-kotlin-analysis/build.gradle.kts new file mode 100644 index 000000000..59adaf598 --- /dev/null +++ b/scip-kotlin-analysis/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + id("scip.java-library") + id("scip.kotlin-jvm") +} + +description = "Standalone Kotlin SCIP indexer built on the Kotlin Analysis API" + +// The Analysis API `*-for-ide` artifacts have unusable POM metadata (they drag in +// IDE-only dependencies), so every artifact is pinned non-transitively — the same +// approach used by Dokka's analysis-kotlin-symbols and KSP2. `kotlin-compiler` +// (the non-embeddable jar) provides the bundled IntelliJ core classes at runtime. +dependencies { + implementation(project(":scip-shared")) + implementation(libs.scip.kotlin.bindings) + + implementation(libs.kotlin.analysis.compiler) { isTransitive = false } + implementation(libs.kotlin.analysis.api.api) { isTransitive = false } + implementation(libs.kotlin.analysis.api.standalone) { isTransitive = false } + + runtimeOnly(libs.kotlin.analysis.api.impl.base) { isTransitive = false } + runtimeOnly(libs.kotlin.analysis.api.k2) { isTransitive = false } + runtimeOnly(libs.kotlin.analysis.low.level.api.fir) { isTransitive = false } + runtimeOnly(libs.kotlin.analysis.api.platform.`interface`) { isTransitive = false } + runtimeOnly(libs.kotlin.analysis.symbol.light.classes) { isTransitive = false } + runtimeOnly(libs.caffeine) + runtimeOnly(libs.kotlinx.coroutines.core.jvm) + runtimeOnly(libs.kotlinx.collections.immutable.jvm) + runtimeOnly(libs.kotlinx.serialization.json.jvm) + + testImplementation(libs.kotlin.test) + testImplementation(libs.kotlin.test.junit5) +} diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt new file mode 100644 index 000000000..86d62e6ee --- /dev/null +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt @@ -0,0 +1,231 @@ +package org.scip_code.scip_java.kotlin_analysis + +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.TextRange +import java.nio.file.Path +import java.nio.file.Paths +import org.jetbrains.kotlin.analysis.api.KaSession +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.analysis.api.standalone.buildStandaloneAnalysisAPISession +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtLibraryModule +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSdkModule +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSourceModule +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.jetbrains.kotlin.psi.KtConstructor +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtImportDirective +import org.jetbrains.kotlin.psi.KtNamedDeclaration +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.psi.KtPrimaryConstructor +import org.jetbrains.kotlin.psi.KtPropertyAccessor +import org.jetbrains.kotlin.psi.KtSecondaryConstructor +import org.jetbrains.kotlin.psi.KtSimpleNameExpression +import org.jetbrains.kotlin.psi.KtTreeVisitorVoid +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject +import org.jetbrains.kotlin.psi.psiUtil.parents +import org.scip_code.scip.Document +import org.scip_code.scip.Occurrence +import org.scip_code.scip.SymbolRole +import org.scip_code.scip.symbolInformation +import org.scip_code.scip_java.shared.ScipDocumentBuilder +import org.scip_code.scip_java.shared.ScipRange +import org.scip_code.scip_java.shared.ScipShardPaths +import org.scip_code.scip_java.shared.ScipShardWriter + +/** + * Indexes Kotlin sources into SCIP documents using the Kotlin Analysis API in standalone mode. + * + * Unlike scip-kotlinc, this indexer does not run inside the user's kotlinc process: it bundles its + * own analysis engine, so the Kotlin version of the indexed project only matters as a + * language-version setting, never as a binary compatibility constraint. + * + * One SCIP shard is written per source file under + * `/META-INF/scip/.scip`, matching the layout produced by the + * scip-javac and scip-kotlinc compiler plugins. + */ +class KotlinAnalysisIndexer( + private val sourceroot: Path, + private val targetroot: Path, + private val sourceRoots: List, + private val classpath: List = emptyList(), + private val jdkHome: Path? = Paths.get(System.getProperty("java.home")), + private val callback: (Document) -> Unit = {}, +) { + fun run(): List { + val disposable = Disposer.newDisposable("scip-kotlin-analysis") + try { + val session = + buildStandaloneAnalysisAPISession(projectDisposable = disposable) { + buildKtModuleProvider { + platform = JvmPlatforms.defaultJvmPlatform + val dependencies = buildList { + jdkHome?.let { home -> + add( + buildKtSdkModule { + platform = JvmPlatforms.defaultJvmPlatform + addBinaryRootsFromJdkHome(home, isJre = false) + libraryName = "JDK" + } + ) + } + if (classpath.isNotEmpty()) { + add( + buildKtLibraryModule { + platform = JvmPlatforms.defaultJvmPlatform + addBinaryRoots(classpath) + libraryName = "classpath" + } + ) + } + } + addModule( + buildKtSourceModule { + platform = JvmPlatforms.defaultJvmPlatform + moduleName = "scip-kotlin-analysis" + addSourceRoots(sourceRoots) + dependencies.forEach { addRegularDependency(it) } + } + ) + } + } + + val documents = mutableListOf() + val ktFiles = session.modulesWithFiles.values.flatten().filterIsInstance() + for (ktFile in ktFiles) { + val document = indexFile(ktFile) + writeShard(ktFile, document) + callback(document) + documents.add(document) + } + return documents + } finally { + Disposer.dispose(disposable) + } + } + + private fun indexFile(ktFile: KtFile): Document { + val cache = SymbolsCache() + val text = ktFile.text + val lines = LineIndex(text) + val builder = ScipDocumentBuilder() + analyze(ktFile) { ktFile.accept(IndexingVisitor(this, cache, lines, builder)) } + val relativePath = + ScipShardPaths.relativePath(sourceroot, Paths.get(ktFile.virtualFilePath)) + return builder.build("kotlin", relativePath, text) + } + + private fun writeShard(ktFile: KtFile, document: Document) { + val absolutePath = Paths.get(ktFile.virtualFilePath).normalize() + val shardPath = ScipShardPaths.shardPath(targetroot, sourceroot, absolutePath) + if (shardPath.isPresent) { + ScipShardWriter.writeShard(shardPath.get(), document) + } else { + System.err.println( + "given file is not under the sourceroot.\n\tSourceroot: $sourceroot\n\tFile path: $absolutePath" + ) + } + } + + private class IndexingVisitor( + private val session: KaSession, + private val cache: SymbolsCache, + private val lines: LineIndex, + private val out: ScipDocumentBuilder, + ) : KtTreeVisitorVoid() { + + override fun visitDeclaration(dcl: KtDeclaration) { + super.visitDeclaration(dcl) + // Parameters that don't belong to a declaration (function types, catch + // clauses, for loops) are not emitted as definitions; references to the + // latter two still produce local symbols on first use. + if (dcl is KtParameter && !cache.isDeclarationParameter(dcl)) return + val range = definitionRange(dcl) ?: return + val symbol = cache.symbolForDeclaration(dcl) + if (symbol == Symbol.NONE) return + addOccurrence(symbol, range, definition = true) + out.addSymbol( + symbolInformation { + this.symbol = symbol.toString() + this.displayName = displayName(dcl) + } + ) + } + + override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) { + super.visitSimpleNameExpression(expression) + if (expression.parents.any { it is KtImportDirective }) return + val target = with(session) { expression.mainReference.resolveToSymbol() } ?: return + val symbol = cache.symbolForReference(target) + if (symbol == Symbol.NONE) return + addOccurrence( + symbol, + expression.getReferencedNameElement().textRange, + definition = false, + ) + } + + private fun definitionRange(declaration: KtDeclaration): TextRange? = + when (declaration) { + // A primary constructor without the `constructor` keyword is linked + // to the class identifier, matching the FIR indexer. + is KtPrimaryConstructor -> + declaration.getConstructorKeyword()?.textRange + ?: declaration.containingClassOrObject?.nameIdentifier?.textRange + is KtSecondaryConstructor -> declaration.getConstructorKeyword().textRange + is KtPropertyAccessor -> declaration.namePlaceholder.textRange + is KtNamedDeclaration -> declaration.nameIdentifier?.textRange + else -> null + } + + private fun displayName(declaration: KtDeclaration): String = + when (declaration) { + is KtPropertyAccessor -> declaration.property.name.orEmpty() + is KtConstructor<*> -> "" + is KtNamedDeclaration -> declaration.name.orEmpty() + else -> "" + } + + private fun addOccurrence(symbol: Symbol, range: TextRange, definition: Boolean) { + val scipRange = lines.scipRange(range) ?: return + val builder = Occurrence.newBuilder().setSymbol(symbol.toString()) + if (definition) { + builder.setSymbolRoles(SymbolRole.Definition.number) + } + if (scipRange.isSingleLine) { + builder.setSingleLineRange(scipRange.toSingleLineRange()) + } else { + builder.setMultiLineRange(scipRange.toMultiLineRange()) + } + out.addOccurrence(builder.build()) + } + } +} + +/** Maps text offsets to 0-based line/character positions. */ +internal class LineIndex(private val text: String) { + private val lineStartOffsets: IntArray + + init { + val offsets = ArrayList() + offsets.add(0) + for (index in text.indices) { + if (text[index] == '\n') offsets.add(index + 1) + } + lineStartOffsets = offsets.toIntArray() + } + + fun scipRange(range: TextRange): ScipRange? { + if (range.startOffset > text.length || range.endOffset > text.length) return null + val (startLine, startCharacter) = position(range.startOffset) + val (endLine, endCharacter) = position(range.endOffset) + return ScipRange.range(startLine, startCharacter, endLine, endCharacter) + } + + private fun position(offset: Int): Pair { + var line = java.util.Arrays.binarySearch(lineStartOffsets, offset) + if (line < 0) line = -line - 2 + return line to (offset - lineStartOffsets[line]) + } +} diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/ScipSymbols.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/ScipSymbols.kt new file mode 100644 index 000000000..afe9c19a9 --- /dev/null +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/ScipSymbols.kt @@ -0,0 +1,73 @@ +package org.scip_code.scip_java.kotlin_analysis + +import org.scip_code.scip_java.shared.ScipSymbols as SharedSymbols + +// Ported from scip-kotlinc's ScipSymbols.kt so that the Analysis-API-based indexer +// produces byte-identical SCIP symbol strings. scip-kotlinc is deleted once this +// indexer reaches parity, at which point this file is the only copy. +@JvmInline +value class Symbol(private val symbol: String) { + companion object { + val NONE = Symbol(SharedSymbols.NONE) + val ROOT_PACKAGE = Symbol(SharedSymbols.ROOT_PACKAGE) + + // Note: this intentionally diverges from `SharedSymbols.global` when + // `desc == NONE` — Java returns `NONE`, Kotlin returns the owner. + // SymbolsCache relies on this behavior; do not delegate without first + // updating those call sites. + fun createGlobal(owner: Symbol, desc: ScipSymbolDescriptor): Symbol = + when { + desc == ScipSymbolDescriptor.NONE -> owner + owner != ROOT_PACKAGE -> Symbol(owner.symbol + desc.encode().symbol) + else -> desc.encode() + } + + fun createLocal(i: Int) = Symbol(SharedSymbols.local(i)) + } + + fun isGlobal() = SharedSymbols.isGlobal(symbol) + + fun isLocal() = SharedSymbols.isLocal(symbol) + + override fun toString(): String = symbol +} + +fun String.symbol(): Symbol = Symbol(this) + +data class ScipSymbolDescriptor( + val kind: Kind, + val name: String, + // Default differs from `SharedSymbols.Descriptor` (which is "") because + // Kotlin call sites — getters/setters in particular — rely on the no-arg + // overload producing `name().` rather than `name.` for METHOD kinds. + val disambiguator: String = "()", +) { + companion object { + val NONE = ScipSymbolDescriptor(Kind.NONE, "") + } + + enum class Kind { + NONE, + TERM, + METHOD, + TYPE, + PACKAGE, + PARAMETER, + TYPE_PARAMETER; + + internal fun toSharedKind(): SharedSymbols.Descriptor.Kind = + when (this) { + NONE -> SharedSymbols.Descriptor.Kind.None + TERM -> SharedSymbols.Descriptor.Kind.Term + METHOD -> SharedSymbols.Descriptor.Kind.Method + TYPE -> SharedSymbols.Descriptor.Kind.Type + PACKAGE -> SharedSymbols.Descriptor.Kind.Package + PARAMETER -> SharedSymbols.Descriptor.Kind.Parameter + TYPE_PARAMETER -> SharedSymbols.Descriptor.Kind.TypeParameter + } + } + + fun encode(): Symbol = + if (kind == Kind.NONE) Symbol(SharedSymbols.NONE) + else Symbol(SharedSymbols.Descriptor(kind.toSharedKind(), name, disambiguator).encode()) +} diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt new file mode 100644 index 000000000..cc5a42de8 --- /dev/null +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt @@ -0,0 +1,260 @@ +package org.scip_code.scip_java.kotlin_analysis + +import com.intellij.psi.PsiElement +import java.lang.System.err +import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaClassLikeSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaConstructorSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaFunctionSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaPackageSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtClassBody +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtConstructor +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtEnumEntry +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtFunctionLiteral +import org.jetbrains.kotlin.psi.KtNamedDeclaration +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtObjectDeclaration +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.psi.KtParameterList +import org.jetbrains.kotlin.psi.KtPropertyAccessor +import org.jetbrains.kotlin.psi.KtSecondaryConstructor +import org.jetbrains.kotlin.psi.KtTypeParameter +import org.jetbrains.kotlin.psi.KtTypeParameterListOwner +import org.jetbrains.kotlin.psi.KtVariableDeclaration +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType +import org.jetbrains.kotlin.psi.psiUtil.parents +import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly +import org.scip_code.scip_java.kotlin_analysis.ScipSymbolDescriptor.Kind +import org.scip_code.scip_java.shared.LocalSymbolsCache as SharedLocalSymbolsCache + +/** + * Computes SCIP symbol strings for Kotlin declarations and resolved references. + * + * This is a PSI-driven port of the FIR-based `SymbolsCache` in scip-kotlinc. Declarations with + * Kotlin source are classified from their PSI structure, which keeps the version-sensitive Analysis + * API surface minimal. References that resolve to declarations without Kotlin source (classpath or + * JDK symbols) fall back to `ClassId`/`CallableId`-derived symbols. + * + * Instantiate one cache per indexed file: local symbols are numbered per SCIP document. + */ +class SymbolsCache { + private val globals = HashMap() + private val packages = HashMap() + private val locals = + SharedLocalSymbolsCache(HashMap()) { Symbol.createLocal(it) } + + fun packageSymbol(fqName: FqName): Symbol { + if (fqName.isRoot) return Symbol.ROOT_PACKAGE + return packages.getOrPut(fqName) { + Symbol.createGlobal( + packageSymbol(fqName.parent()), + ScipSymbolDescriptor(Kind.PACKAGE, fqName.shortName().asString()), + ) + } + } + + /** The SCIP symbol for a declaration in Kotlin source. */ + fun symbolForDeclaration(declaration: KtDeclaration): Symbol { + globals[declaration]?.let { + return it + } + locals.get(declaration)?.let { + return it + } + val symbol = uncachedSymbol(declaration) + if (symbol.isGlobal()) globals[declaration] = symbol + return symbol + } + + /** The SCIP symbol for the target of a resolved reference. */ + fun symbolForReference(target: KaSymbol): Symbol { + val psi = target.psi + if (psi is KtDeclaration) { + // A constructor symbol whose PSI is the class itself is an implicit + // primary constructor. + if (target is KaConstructorSymbol && psi is KtClassOrObject) { + return Symbol.createGlobal( + symbolForDeclaration(psi), + ScipSymbolDescriptor(Kind.METHOD, ""), + ) + } + return symbolForDeclaration(psi) + } + return when (target) { + is KaPackageSymbol -> packageSymbol(target.fqName) + is KaConstructorSymbol -> + target.containingClassId?.let { classId -> + Symbol.createGlobal( + classIdSymbol(classId), + ScipSymbolDescriptor(Kind.METHOD, ""), + ) + } ?: Symbol.NONE + is KaClassLikeSymbol -> target.classId?.let(::classIdSymbol) ?: Symbol.NONE + is KaCallableSymbol -> externalCallableSymbol(target) + else -> Symbol.NONE + } + } + + private fun uncachedSymbol(declaration: KtDeclaration): Symbol { + // Anonymous functions and lambdas have no symbol of their own, mirroring + // the FIR indexer which returned NONE for FirAnonymousFunctionSymbol. + if (declaration is KtFunctionLiteral) return Symbol.NONE + if (declaration is KtNamedFunction && declaration.name == null) return Symbol.NONE + + if (isLocalMember(declaration)) return locals.put(declaration) + + val owner = ownerSymbol(declaration) + if (owner.isLocal() || owner == Symbol.NONE) return locals.put(declaration) + + return Symbol.createGlobal(owner, scipDescriptor(declaration)) + } + + // Port of FIR's `isLocalMember`: only functions, variables and classes can be + // local members; everything else is classified through its owner. + private fun isLocalMember(declaration: KtDeclaration): Boolean = + when (declaration) { + is KtNamedFunction, + is KtVariableDeclaration, + is KtClassOrObject -> + declaration.parents + .takeWhile { it !is KtFile } + .any { it !is KtClassBody && it !is KtClassOrObject } + else -> false + } + + private fun ownerSymbol(declaration: KtDeclaration): Symbol = + when (declaration) { + is KtTypeParameter -> + declaration.getStrictParentOfType()?.let { + symbolForDeclaration(it) + } ?: Symbol.NONE + is KtParameter -> + ownerDeclarationOfParameter(declaration)?.let { symbolForDeclaration(it) } + ?: Symbol.NONE + is KtPropertyAccessor -> + declaration.property.containingClassOrObject?.let { symbolForDeclaration(it) } + ?: packageSymbol(declaration.containingKtFile.packageFqName) + else -> + declaration.containingClassOrObject?.let { symbolForDeclaration(it) } + ?: packageSymbol(declaration.containingKtFile.packageFqName) + } + + private fun scipDescriptor(declaration: KtDeclaration): ScipSymbolDescriptor = + when { + declaration is KtObjectDeclaration && declaration.isObjectLiteral() -> + ScipSymbolDescriptor( + Kind.TYPE, + "", + ) + declaration is KtEnumEntry -> + ScipSymbolDescriptor(Kind.TERM, declaration.name.orEmpty()) + declaration is KtObjectDeclaration && declaration.isCompanion() -> + ScipSymbolDescriptor(Kind.TYPE, declaration.name ?: "Companion") + declaration is KtClassOrObject -> + ScipSymbolDescriptor(Kind.TYPE, declaration.name.orEmpty()) + declaration is KtPropertyAccessor && declaration.isSetter -> + ScipSymbolDescriptor( + Kind.METHOD, + "set" + declaration.property.name.orEmpty().capitalizeAsciiOnly(), + ) + declaration is KtPropertyAccessor -> + ScipSymbolDescriptor( + Kind.METHOD, + "get" + declaration.property.name.orEmpty().capitalizeAsciiOnly(), + ) + declaration is KtConstructor<*> -> + ScipSymbolDescriptor(Kind.METHOD, "", constructorDisambiguator(declaration)) + declaration is KtNamedFunction -> + ScipSymbolDescriptor( + Kind.METHOD, + declaration.name.orEmpty(), + methodDisambiguator(declaration), + ) + declaration is KtTypeParameter -> + ScipSymbolDescriptor(Kind.TYPE_PARAMETER, declaration.name.orEmpty()) + declaration is KtParameter -> + ScipSymbolDescriptor(Kind.PARAMETER, declaration.name.orEmpty()) + declaration is KtVariableDeclaration -> + ScipSymbolDescriptor(Kind.TERM, declaration.name.orEmpty()) + else -> { + err.println("unknown declaration kind ${declaration.javaClass.simpleName}") + ScipSymbolDescriptor.NONE + } + } + + /** + * Port of FIR's `methodDisambiguator`: the N-th preceding declaration with the same name in the + * containing scope yields `(+N)`, the first one yields `()`. + */ + private fun methodDisambiguator(function: KtNamedFunction): String { + val name = function.name ?: return "()" + var count = 0 + var found = false + for (sibling in siblingDeclarations(function)) { + if (sibling == function) { + found = true + break + } + if (declaredName(sibling) == name) count++ + } + if (count == 0 || !found) return "()" + return "(+$count)" + } + + private fun constructorDisambiguator(constructor: KtConstructor<*>): String { + val containingClass = constructor.containingClassOrObject ?: return "()" + val constructors = + listOfNotNull(containingClass.primaryConstructor) + + containingClass.declarations.filterIsInstance() + val index = constructors.indexOf(constructor) + return if (index <= 0) "()" else "(+$index)" + } + + private fun siblingDeclarations(declaration: KtDeclaration): List = + when (val containingClass = declaration.containingClassOrObject) { + null -> declaration.containingKtFile.declarations + else -> listOfNotNull(containingClass.primaryConstructor) + containingClass.declarations + } + + private fun declaredName(declaration: KtDeclaration): String? = + when (declaration) { + is KtConstructor<*> -> "" + is KtNamedDeclaration -> declaration.name + else -> null + } + + private fun ownerDeclarationOfParameter(parameter: KtParameter): KtDeclaration? = + (parameter.parent as? KtParameterList)?.parent as? KtDeclaration + + internal fun isDeclarationParameter(parameter: KtParameter): Boolean = + ownerDeclarationOfParameter(parameter) != null + + private fun externalCallableSymbol(target: KaCallableSymbol): Symbol { + val callableId = target.callableId ?: return Symbol.NONE + val owner = + callableId.classId?.let(::classIdSymbol) ?: packageSymbol(callableId.packageName) + val name = callableId.callableName.asString() + val descriptor = + when (target) { + is KaFunctionSymbol -> ScipSymbolDescriptor(Kind.METHOD, name) + else -> ScipSymbolDescriptor(Kind.TERM, name) + } + return Symbol.createGlobal(owner, descriptor) + } + + private fun classIdSymbol(classId: ClassId): Symbol { + var symbol = packageSymbol(classId.packageFqName) + for (segment in classId.relativeClassName.pathSegments()) { + symbol = + Symbol.createGlobal(symbol, ScipSymbolDescriptor(Kind.TYPE, segment.asString())) + } + return symbol + } +} diff --git a/scip-kotlin-analysis/src/test/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexerTest.kt b/scip-kotlin-analysis/src/test/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexerTest.kt new file mode 100644 index 000000000..6391232ae --- /dev/null +++ b/scip-kotlin-analysis/src/test/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexerTest.kt @@ -0,0 +1,108 @@ +package org.scip_code.scip_java.kotlin_analysis + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.scip_code.scip.Document +import org.scip_code.scip.SymbolRole + +class KotlinAnalysisIndexerTest { + + @Test + fun `indexes definitions, references and locals across files`() { + val sourceroot = Files.createTempDirectory("scip-kotlin-analysis").toRealPath() + val src = Files.createDirectories(sourceroot.resolve("src")) + Files.writeString( + src.resolve("Greeter.kt"), + """ + |package snapshots + | + |class Greeter(val name: String) { + | fun greet(): String { + | val message = "Hello, " + name + | return message + | } + | + | fun greet(punctuation: String): String = greet() + punctuation + |} + """ + .trimMargin(), + ) + Files.writeString( + src.resolve("Main.kt"), + """ + |package snapshots + | + |fun useGreeter(): String { + | val greeter = Greeter("world") + | return greeter.greet("!") + |} + """ + .trimMargin(), + ) + val targetroot = sourceroot.resolve("scip-targetroot") + + val documents = + KotlinAnalysisIndexer( + sourceroot = sourceroot, + targetroot = targetroot, + sourceRoots = listOf(src), + classpath = listOf(kotlinStdlibJar()), + ) + .run() + .sortedBy { it.relativePath } + + assertEquals(listOf("src/Greeter.kt", "src/Main.kt"), documents.map { it.relativePath }) + val greeter = documents[0] + val main = documents[1] + + val greeterDefinitions = definitions(greeter) + assertContains(greeterDefinitions, "snapshots/Greeter#") + assertContains(greeterDefinitions, "snapshots/Greeter#``().(name)") + assertContains(greeterDefinitions, "snapshots/Greeter#greet().") + assertContains(greeterDefinitions, "snapshots/Greeter#greet(+1).") + assertContains(greeterDefinitions, "snapshots/Greeter#greet(+1).(punctuation)") + // `val message` inside the function body is the first local of the file. + assertContains(greeterDefinitions, "local 0") + // The overload delegating to `greet()` references it. + assertContains(references(greeter), "snapshots/Greeter#greet().") + // The stdlib type reference resolves to a classpath symbol. + assertContains(references(greeter), "kotlin/String#") + + val mainDefinitions = definitions(main) + assertContains(mainDefinitions, "snapshots/useGreeter().") + assertContains(mainDefinitions, "local 0") + val mainReferences = references(main) + // Cross-file references: constructor call, method call on the overload, + // and the local variable usage. + assertContains(mainReferences, "snapshots/Greeter#``().") + assertContains(mainReferences, "snapshots/Greeter#greet(+1).") + assertContains(mainReferences, "local 0") + + assertTrue( + Files.exists(targetroot.resolve("META-INF/scip/src/Greeter.kt.scip")), + "expected SCIP shard for Greeter.kt", + ) + assertTrue( + Files.exists(targetroot.resolve("META-INF/scip/src/Main.kt.scip")), + "expected SCIP shard for Main.kt", + ) + } + + private fun definitions(document: Document): List = + document.occurrencesList + .filter { it.symbolRoles and SymbolRole.Definition.number != 0 } + .map { it.symbol } + + private fun references(document: Document): List = + document.occurrencesList + .filter { it.symbolRoles and SymbolRole.Definition.number == 0 } + .map { it.symbol } + + private fun kotlinStdlibJar(): Path = + Paths.get(Unit::class.java.protectionDomain.codeSource.location.toURI()) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 7a3395b07..6b12f3a5b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,10 @@ dependencyResolutionManagement { repositories { mavenCentral() gradlePluginPortal() + // Kotlin Analysis API (`*-for-ide`) artifacts used by scip-kotlin-analysis. + maven("https://redirector.kotlinlang.org/maven/kotlin-ide-plugin-dependencies") { + content { includeGroupAndSubgroups("org.jetbrains.kotlin") } + } } } @@ -20,6 +24,7 @@ include( "scip-shared", "scip-javac", "scip-kotlinc", + "scip-kotlin-analysis", "scip-aggregator", "scip-maven-plugin", "scip-gradle-plugin", From 06a2f625f439c01d4fb88b6a0119f9adf3121823 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 10:33:48 +0200 Subject: [PATCH 02/15] Index Kotlin via Analysis API in ScipBuildTool --- scip-java/build.gradle.kts | 6 +- .../scip_java/buildtools/ScipBuildTool.kt | 114 ++++-------------- 2 files changed, 22 insertions(+), 98 deletions(-) diff --git a/scip-java/build.gradle.kts b/scip-java/build.gradle.kts index 835e24adc..ef86756b0 100644 --- a/scip-java/build.gradle.kts +++ b/scip-java/build.gradle.kts @@ -17,13 +17,9 @@ val kotlincShadowJar = shadowJarArtifact(":scip-kotlinc", "kotlincShadowJar") dependencies { implementation(project(":scip-aggregator")) + implementation(project(":scip-kotlin-analysis")) implementation(libs.clikt.jvm) implementation(libs.kotlin.stdlib) - implementation(libs.kotlin.compiler.embeddable) - implementation(libs.kotlin.scripting.common) - implementation(libs.kotlin.scripting.jvm) - implementation(libs.kotlin.scripting.dependencies) - implementation(libs.kotlin.scripting.dependencies.maven) implementation(libs.kotlinx.serialization.json.jvm) testImplementation(libs.kotlin.test) diff --git a/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/ScipBuildTool.kt b/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/ScipBuildTool.kt index 2c06f9d41..5b8844cde 100644 --- a/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/ScipBuildTool.kt +++ b/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/ScipBuildTool.kt @@ -17,16 +17,9 @@ import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.boolean import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonArray -import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments -import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments -import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity -import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation -import org.jetbrains.kotlin.cli.common.messages.MessageCollector -import org.jetbrains.kotlin.cli.common.messages.MessageRenderer -import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler -import org.jetbrains.kotlin.config.Services import org.scip_code.scip_java.Embedded import org.scip_code.scip_java.commands.IndexCommand +import org.scip_code.scip_java.kotlin_analysis.KotlinAnalysisIndexer /** * A custom build tool that is specifically made for scip-java. @@ -130,7 +123,7 @@ class ScipBuildTool(index: IndexCommand) : BuildTool("SCIP", index) { val errors = mutableListOf() compileJavaFiles(tmp, config, javaFiles)?.let { errors += it } - compileKotlinFiles(config, kotlinFiles, tmp)?.let { errors += it } + indexKotlinFiles(config, kotlinFiles)?.let { errors += it } if (index.cleanup) { tmp.toFile().deleteRecursively() @@ -154,93 +147,28 @@ class ScipBuildTool(index: IndexCommand) : BuildTool("SCIP", index) { } } - private fun compileKotlinFiles( - config: Config, - allKotlinFiles: List, - tmp: Path, - ): Throwable? { + private fun indexKotlinFiles(config: Config, allKotlinFiles: List): Throwable? { if (allKotlinFiles.isEmpty()) return null val sourceroot = index.workingDirectory - val filesPaths = allKotlinFiles.map { it.toString() } - - // The scip-kotlinc compiler plugin is built and shipped together - // with the scip-java CLI as an embedded resource (see Embedded.kt and - // the :scip-java Gradle resources wiring). - val plugin = Embedded.scipKotlincJar(tmp) - - val classpath = - config.classpath.joinToString(File.pathSeparator) { - index.workingDirectory.resolve(it).toString() - } - - val kargs = K2JVMCompilerArguments() - val args = - mutableListOf( - "-nowarn", - "-no-reflect", - "-no-stdlib", - "-Xmulti-platform", - "-Xno-check-actual", - "-verbose:class", - "-opt-in=kotlin.RequiresOptIn", - "-opt-in=kotlin.ExperimentalUnsignedTypes", - "-opt-in=kotlin.ExperimentalStdlibApi", - "-opt-in=kotlin.ExperimentalMultiplatform", - "-opt-in=kotlin.contracts.ExperimentalContracts", - "-Xallow-kotlin-package", - "-Xplugin=$plugin", - "-P", - "plugin:scip-kotlinc:sourceroot=$sourceroot", - "-P", - "plugin:scip-kotlinc:targetroot=$targetroot", - "-classpath", - classpath, - ) - args += filesPaths - - parseCommandLineArguments(args, kargs) - - val exit = - K2JVMCompiler() - .exec( - object : MessageCollector { - private var sawError = false - - override fun clear() { - sawError = false - } - - override fun hasErrors(): Boolean = sawError - - override fun report( - severity: CompilerMessageSeverity, - message: String, - location: CompilerMessageSourceLocation?, - ) { - if ( - message.endsWith("without a body must be abstract") || - message.endsWith("must have a body") - ) { - // We get these when indexing the stdlib; - // no other solution found yet. - return - } - val rendered = - MessageRenderer.PLAIN_FULL_PATHS.render(severity, message, location) - index.app.reporter.debug(rendered) - // Only treat ERROR / EXCEPTION as failures. - // Kotlin 2.2.0's K2JVMCompiler emits LOGGING/INFO/WARNING - // messages during normal compilation; pushing those onto - // `errors` would cause hasErrors to return true. - if (severity.isError) { - sawError = true - } - } - }, - Services.EMPTY, - kargs, + val classpath = config.classpath.map { sourceroot.resolve(it).normalize() } + val jdkHome = + config.javaHome?.let { Paths.get(it) } ?: Paths.get(System.getProperty("java.home")) + // Unlike the old scip-kotlinc compiler plugin, the Analysis API indexer does + // not compile the sources: unresolved code degrades individual occurrences + // instead of failing the build, so only indexer crashes surface as errors. + return try { + KotlinAnalysisIndexer( + sourceroot = sourceroot, + targetroot = targetroot, + sourceRoots = allKotlinFiles, + classpath = classpath, + jdkHome = jdkHome, ) - return if (exit.code == 0) null else Exception(exit.toString()) + .run() + null + } catch (e: Exception) { + e + } } private fun compileJavaFiles(tmp: Path, config: Config, allJavaFiles: List): Throwable? { From 2476638837f52eab16c5b900ad72d9cfa73feaf3 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 11:16:11 +0200 Subject: [PATCH 03/15] Bring scip-kotlin-analysis to output parity with scip-kotlinc --- .../org/scip_code/scip_java/ScipJava.kt | 3 + .../kotlin_analysis/KotlinAnalysisIndexer.kt | 540 ++++++++++++++++-- .../scip_java/kotlin_analysis/Main.kt | 51 ++ .../kotlin_analysis/SignatureRenderer.kt | 220 +++++++ .../scip_java/kotlin_analysis/SymbolsCache.kt | 191 ++++++- .../cases/kotlin/common/build.gradle.kts | 43 ++ 6 files changed, 975 insertions(+), 73 deletions(-) create mode 100644 scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/Main.kt create mode 100644 scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt diff --git a/scip-java/src/main/kotlin/org/scip_code/scip_java/ScipJava.kt b/scip-java/src/main/kotlin/org/scip_code/scip_java/ScipJava.kt index a15ba62a4..47b535700 100644 --- a/scip-java/src/main/kotlin/org/scip_code/scip_java/ScipJava.kt +++ b/scip-java/src/main/kotlin/org/scip_code/scip_java/ScipJava.kt @@ -17,6 +17,9 @@ object ScipJava { @JvmStatic fun main(args: Array) { app.runAndExitIfNonZero(args.toList()) + // The Kotlin Analysis API indexer leaves non-daemon threads behind; exit + // explicitly instead of waiting on them. + kotlin.system.exitProcess(0) } fun printHelp(out: PrintStream) { diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt index 86d62e6ee..3455bc0a7 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt @@ -7,27 +7,47 @@ import java.nio.file.Paths import org.jetbrains.kotlin.analysis.api.KaSession import org.jetbrains.kotlin.analysis.api.analyze import org.jetbrains.kotlin.analysis.api.standalone.buildStandaloneAnalysisAPISession +import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaConstructorSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaFunctionSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaPropertySymbol +import org.jetbrains.kotlin.analysis.api.types.KaClassType import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtLibraryModule import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSdkModule import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSourceModule import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtClassOrObject import org.jetbrains.kotlin.psi.KtConstructor +import org.jetbrains.kotlin.psi.KtConstructorCalleeExpression import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtDestructuringDeclarationEntry +import org.jetbrains.kotlin.psi.KtEnumEntry import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.KtImportDirective -import org.jetbrains.kotlin.psi.KtNamedDeclaration +import org.jetbrains.kotlin.psi.KtFunctionLiteral +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtObjectDeclaration import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.KtPrimaryConstructor +import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtPropertyAccessor import org.jetbrains.kotlin.psi.KtSecondaryConstructor import org.jetbrains.kotlin.psi.KtSimpleNameExpression import org.jetbrains.kotlin.psi.KtTreeVisitorVoid +import org.jetbrains.kotlin.psi.KtTypeParameter +import org.jetbrains.kotlin.psi.KtVariableDeclaration import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject import org.jetbrains.kotlin.psi.psiUtil.parents import org.scip_code.scip.Document import org.scip_code.scip.Occurrence import org.scip_code.scip.SymbolRole +import org.scip_code.scip.relationship +import org.scip_code.scip.signature import org.scip_code.scip.symbolInformation import org.scip_code.scip_java.shared.ScipDocumentBuilder import org.scip_code.scip_java.shared.ScipRange @@ -134,73 +154,475 @@ class KotlinAnalysisIndexer( private val lines: LineIndex, private val out: ScipDocumentBuilder, ) : KtTreeVisitorVoid() { + private val signatures = SignatureRenderer(session) override fun visitDeclaration(dcl: KtDeclaration) { super.visitDeclaration(dcl) - // Parameters that don't belong to a declaration (function types, catch - // clauses, for loops) are not emitted as definitions; references to the - // latter two still produce local symbols on first use. - if (dcl is KtParameter && !cache.isDeclarationParameter(dcl)) return - val range = definitionRange(dcl) ?: return - val symbol = cache.symbolForDeclaration(dcl) - if (symbol == Symbol.NONE) return - addOccurrence(symbol, range, definition = true) - out.addSymbol( - symbolInformation { - this.symbol = symbol.toString() - this.displayName = displayName(dcl) - } - ) + when (dcl) { + is KtEnumEntry -> emitEnumEntry(dcl) + is KtClassOrObject -> emitClassLike(dcl) + is KtConstructor<*> -> emitConstructor(dcl) + is KtNamedFunction -> emitFunction(dcl) + is KtProperty -> emitProperty(dcl) + is KtParameter -> emitParameter(dcl) + is KtPropertyAccessor -> emitExplicitAccessor(dcl) + is KtTypeParameter -> emitTypeParameter(dcl) + is KtDestructuringDeclarationEntry -> emitLocalVariable(dcl) + else -> {} + } } override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) { super.visitSimpleNameExpression(expression) - if (expression.parents.any { it is KtImportDirective }) return + val referencedName = expression.getReferencedName() + if (referencedName == "this" || referencedName == "super") return val target = with(session) { expression.mainReference.resolveToSymbol() } ?: return - val symbol = cache.symbolForReference(target) - if (symbol == Symbol.NONE) return - addOccurrence( - symbol, - expression.getReferencedNameElement().textRange, - definition = false, + val range = expression.getReferencedNameElement().textRange + + // The implicit `it` lambda parameter has no declaration PSI; its + // definition is anchored at the whole lambda literal, emitted at the + // first reference. + val targetPsi = target.psi + if ( + targetPsi is KtFunctionLiteral && + target is KaCallableSymbol && + target !is KaFunctionSymbol + ) { + emitImplicitItDefinition(targetPsi, target) + addReference(cache.implicitItSymbol(targetPsi), range) + return + } + + // Super-type entries and annotations reference the class, not the + // constructor (`class Seagull : Bird()` references `Bird#`). + if ( + target is KaConstructorSymbol && + expression.parents + .takeWhile { it !is KtFile } + .any { it is KtConstructorCalleeExpression } + ) { + addReference(cache.constructorOwnerSymbol(session, target), range) + return + } + + // Property references fan out to the property and its accessors, + // e.g. `banana` yields `Class#banana.`, `Class#getBanana().` and + // `Class#setBanana().`. + if (target is KaPropertySymbol) { + propertyReferenceSymbols(target).forEach { addReference(it, range) } + return + } + + addReference(cache.symbolForReference(session, target), range) + } + + // --------------------------------------------------------------- + // Declarations + // --------------------------------------------------------------- + + private fun emitClassLike(declaration: KtClassOrObject) { + val isAnonymous = declaration is KtObjectDeclaration && declaration.isObjectLiteral() + val range = + when { + declaration.nameIdentifier != null -> declaration.nameIdentifier!!.textRange + isAnonymous -> + (declaration as KtObjectDeclaration).getObjectKeyword()?.textRange + ?: declaration.textRange + else -> declaration.textRange // unnamed companion object + } + val displayName = if (isAnonymous) "" else declaration.name ?: "Companion" + val symbol = cache.symbolForDeclaration(declaration) + emitDefinition( + symbol = symbol, + range = range, + enclosing = declaration.textRange, + displayName = displayName, + signatureText = signatures.classSignature(declaration), + documentation = docComment(declaration), + relationships = superTypeRelationships(declaration), + ) + // Classes and objects (but not interfaces) declare an implicit primary + // constructor when no explicit one is present in source. + val isInterface = declaration is KtClass && declaration.isInterface() + if (!isInterface && declaration.primaryConstructor == null) { + emitDefinition( + symbol = cache.implicitConstructorSymbol(declaration), + range = range, + enclosing = declaration.textRange, + displayName = displayName, + signatureText = signatures.implicitConstructorSignature(declaration), + documentation = docComment(declaration), + ) + } + } + + private fun emitEnumEntry(declaration: KtEnumEntry) { + val identifier = declaration.nameIdentifier ?: return + emitDefinition( + symbol = cache.symbolForDeclaration(declaration), + range = identifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + signatureText = signatures.propertySignature(declaration), + documentation = docComment(declaration), + ) + } + + private fun emitConstructor(declaration: KtConstructor<*>) { + val range = + when (declaration) { + is KtPrimaryConstructor -> + declaration.getConstructorKeyword()?.textRange + ?: declaration.containingClassOrObject?.nameIdentifier?.textRange + ?: return + is KtSecondaryConstructor -> declaration.textRange + else -> return + } + emitDefinition( + symbol = cache.symbolForDeclaration(declaration), + range = range, + enclosing = declaration.textRange, + displayName = declaration.containingClassOrObject?.name ?: "", + signatureText = signatures.constructorSignature(declaration), + documentation = docComment(declaration), ) } - private fun definitionRange(declaration: KtDeclaration): TextRange? = - when (declaration) { - // A primary constructor without the `constructor` keyword is linked - // to the class identifier, matching the FIR indexer. - is KtPrimaryConstructor -> - declaration.getConstructorKeyword()?.textRange - ?: declaration.containingClassOrObject?.nameIdentifier?.textRange - is KtSecondaryConstructor -> declaration.getConstructorKeyword().textRange - is KtPropertyAccessor -> declaration.namePlaceholder.textRange - is KtNamedDeclaration -> declaration.nameIdentifier?.textRange - else -> null + private fun emitFunction(declaration: KtNamedFunction) { + val identifier = declaration.nameIdentifier ?: return + val relationships = + if (declaration.hasModifier(org.jetbrains.kotlin.lexer.KtTokens.OVERRIDE_KEYWORD)) { + overriddenSymbols(declaration) + } else { + emptyList() + } + emitDefinition( + symbol = cache.symbolForDeclaration(declaration), + range = identifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + signatureText = signatures.functionSignature(declaration), + documentation = docComment(declaration), + relationships = relationships, + ) + } + + private fun emitProperty(declaration: KtProperty) { + val identifier = declaration.nameIdentifier ?: return + val symbol = cache.symbolForDeclaration(declaration) + if (symbol.isLocal()) { + emitDefinition( + symbol = symbol, + range = identifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + signatureText = signatures.localVariableSignature(declaration), + documentation = docComment(declaration), + ) + return } + emitDefinition( + symbol = symbol, + range = identifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + signatureText = signatures.propertySignature(declaration), + documentation = docComment(declaration), + ) + emitSyntheticAccessors( + property = declaration, + nameRange = identifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + hasExplicitGetter = declaration.getter != null, + hasExplicitSetter = declaration.setter != null, + isVar = declaration.isVar, + ) + } - private fun displayName(declaration: KtDeclaration): String = - when (declaration) { - is KtPropertyAccessor -> declaration.property.name.orEmpty() - is KtConstructor<*> -> "" - is KtNamedDeclaration -> declaration.name.orEmpty() - else -> "" + private fun emitParameter(declaration: KtParameter) { + // Parameters of function types, catch clauses and for loops are not + // declarations of their own. + if (!cache.isDeclarationParameter(declaration)) return + val identifier = declaration.nameIdentifier ?: return + val symbol = cache.symbolForDeclaration(declaration) + emitDefinition( + symbol = symbol, + range = identifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + signatureText = signatures.parameterSignature(declaration), + documentation = null, + ) + // A `val`/`var` constructor parameter also declares a property: the + // property initializer references the parameter at the same range, and + // the property plus its synthetic accessors are all anchored there. + if (declaration.hasValOrVar()) { + emitDefinition( + symbol = cache.parameterPropertySymbol(declaration), + range = identifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + signatureText = signatures.propertySignature(declaration), + documentation = null, + ) + addReference(symbol, identifier.textRange) + emitSyntheticAccessors( + property = declaration, + nameRange = identifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + hasExplicitGetter = false, + hasExplicitSetter = false, + isVar = declaration.isMutable, + ) } + } - private fun addOccurrence(symbol: Symbol, range: TextRange, definition: Boolean) { - val scipRange = lines.scipRange(range) ?: return - val builder = Occurrence.newBuilder().setSymbol(symbol.toString()) - if (definition) { - builder.setSymbolRoles(SymbolRole.Definition.number) + private fun emitSyntheticAccessors( + property: KtDeclaration, + nameRange: TextRange, + enclosing: TextRange, + displayName: String, + hasExplicitGetter: Boolean, + hasExplicitSetter: Boolean, + isVar: Boolean, + ) { + if (!hasExplicitGetter) { + emitDefinition( + symbol = cache.syntheticAccessorSymbol(property, setter = false), + range = nameRange, + enclosing = enclosing, + displayName = displayName, + signatureText = signatures.syntheticAccessorSignature(property, setter = false), + documentation = null, + ) } - if (scipRange.isSingleLine) { - builder.setSingleLineRange(scipRange.toSingleLineRange()) - } else { - builder.setMultiLineRange(scipRange.toMultiLineRange()) + if (isVar && !hasExplicitSetter) { + emitDefinition( + symbol = cache.syntheticAccessorSymbol(property, setter = true), + range = nameRange, + enclosing = enclosing, + displayName = displayName, + signatureText = signatures.syntheticAccessorSignature(property, setter = true), + documentation = null, + ) + emitDefinition( + symbol = cache.syntheticSetterValueSymbol(property), + range = nameRange, + enclosing = enclosing, + displayName = "value", + signatureText = signatures.setterValueSignature(property), + documentation = null, + ) } + } + + private fun emitExplicitAccessor(declaration: KtPropertyAccessor) { + emitDefinition( + symbol = cache.symbolForDeclaration(declaration), + range = declaration.namePlaceholder.textRange, + enclosing = declaration.textRange, + displayName = declaration.property.name.orEmpty(), + signatureText = signatures.accessorSignature(declaration), + documentation = docComment(declaration), + ) + } + + private fun emitTypeParameter(declaration: KtTypeParameter) { + val identifier = declaration.nameIdentifier ?: return + emitDefinition( + symbol = cache.symbolForDeclaration(declaration), + range = identifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + signatureText = declaration.text, + documentation = null, + ) + } + + private fun emitLocalVariable(declaration: KtVariableDeclaration) { + val identifier = declaration.nameIdentifier ?: return + emitDefinition( + symbol = cache.symbolForDeclaration(declaration), + range = identifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + signatureText = signatures.localVariableSignature(declaration), + documentation = null, + ) + } + + private val emittedItParameters = HashSet() + + /** + * A lambda with a single implicit `it` parameter declares it without any PSI: the FIR + * indexer anchored its definition at the whole lambda literal. + */ + private fun emitImplicitItDefinition(literal: KtFunctionLiteral, target: KaCallableSymbol) { + if (!emittedItParameters.add(literal)) return + emitDefinition( + symbol = cache.implicitItSymbol(literal), + range = literal.textRange, + enclosing = literal.textRange, + displayName = "it", + signatureText = signatures.implicitItSignature(target), + documentation = null, + ) + } + + // --------------------------------------------------------------- + // Symbol helpers + // --------------------------------------------------------------- + + private fun propertyReferenceSymbols(target: KaPropertySymbol): List { + val psi = target.psi + return when { + psi is KtProperty -> { + val property = cache.symbolForDeclaration(psi) + if (property.isLocal()) return listOf(property) + buildList { + add(property) + psi.getter?.let { add(cache.symbolForDeclaration(it)) } + ?: add(cache.syntheticAccessorSymbol(psi, setter = false)) + if (psi.isVar) { + psi.setter?.let { add(cache.symbolForDeclaration(it)) } + ?: add(cache.syntheticAccessorSymbol(psi, setter = true)) + } + } + } + psi is KtParameter -> + buildList { + add(cache.parameterPropertySymbol(psi)) + add(cache.syntheticAccessorSymbol(psi, setter = false)) + if (psi.isMutable) { + add(cache.syntheticAccessorSymbol(psi, setter = true)) + } + } + else -> + buildList { + add(cache.symbolForReference(session, target)) + add(cache.externalAccessorSymbol(session, target, setter = false)) + if (!target.isVal) { + add(cache.externalAccessorSymbol(session, target, setter = true)) + } + } + }.filter { it != Symbol.NONE } + } + + private fun superTypeRelationships(declaration: KtClassOrObject): List = + with(session) { + val classSymbol = declaration.symbol as? KaClassSymbol ?: return@with emptyList() + classSymbol.superTypes + .mapNotNull { (it as? KaClassType)?.classId } + .filterNot { it == ANY_CLASS_ID } + .map { cache.classSymbol(it) } + .filter { it != Symbol.NONE } + } + + private fun overriddenSymbols(declaration: KtNamedFunction): List = + with(session) { + val symbol = declaration.symbol as? KaCallableSymbol ?: return@with emptyList() + symbol.directlyOverriddenSymbols + .map { cache.symbolForReference(session, it) } + .filter { it != Symbol.NONE } + .toList() + } + + // --------------------------------------------------------------- + // Emission + // --------------------------------------------------------------- + + private fun emitDefinition( + symbol: Symbol, + range: TextRange, + enclosing: TextRange?, + displayName: String, + signatureText: String?, + documentation: String?, + relationships: List = emptyList(), + ) { + if (symbol == Symbol.NONE) return + val scipRange = lines.anchorRange(range) ?: return + val builder = + Occurrence.newBuilder() + .setSymbol(symbol.toString()) + .setSymbolRoles(SymbolRole.Definition.number) + builder.setSingleLineRange(scipRange.toSingleLineRange()) + enclosing + ?.let { lines.scipRange(it) } + ?.let { enclosingRange -> + if (enclosingRange.isSingleLine) { + builder.setSingleLineEnclosingRange(enclosingRange.toSingleLineRange()) + } else { + builder.setMultiLineEnclosingRange(enclosingRange.toMultiLineRange()) + } + } out.addOccurrence(builder.build()) + out.addSymbol( + symbolInformation { + this.symbol = symbol.toString() + this.displayName = displayName + if (signatureText != null) { + this.signatureDocumentation = signature { + language = "kotlin" + text = signatureText + } + } + if (documentation != null) { + this.documentation += documentation + } + for (parent in relationships) { + this.relationships += relationship { + this.symbol = parent.toString() + this.isImplementation = true + } + } + } + ) + } + + private fun addReference(symbol: Symbol, range: TextRange) { + if (symbol == Symbol.NONE) return + val scipRange = lines.anchorRange(range) ?: return + val builder = Occurrence.newBuilder().setSymbol(symbol.toString()) + builder.setSingleLineRange(scipRange.toSingleLineRange()) + out.addOccurrence(builder.build()) + } + + private fun docComment(declaration: KtDeclaration): String? { + val kdoc = declaration.docComment?.text ?: return null + return stripKdoc(kdoc).ifEmpty { null } } + + companion object { + private val ANY_CLASS_ID = ClassId(FqName("kotlin"), Name.identifier("Any")) + } + } +} + +/** Strips the `/**`, leading `*`s, and `*/` from a kdoc block, returning just the body text. */ +internal fun stripKdoc(kdoc: String): String { + if (kdoc.isEmpty()) return kdoc + val out = StringBuilder() + var first = true + kdoc.lineSequence().forEach { line -> + if (line.isEmpty()) return@forEach + var start = 0 + while (start < line.length && line[start].isWhitespace()) start++ + if (start < line.length && line[start] == '/') start++ + while (start < line.length && line[start] == '*') start++ + var end = line.length - 1 + if (end > start && line[end] == '/') end-- + while (end > start && line[end] == '*') end-- + while (end > start && line[end].isWhitespace()) end-- + start = minOf(start, line.length - 1) + if (end > start) end++ + if (!first) out.append('\n') + out.append(line, start, end) + first = false } + return out.toString().trim() } /** Maps text offsets to 0-based line/character positions. */ @@ -223,6 +645,26 @@ internal class LineIndex(private val text: String) { return ScipRange.range(startLine, startCharacter, endLine, endCharacter) } + /** + * Occurrence anchor ranges are always single-line in the scip-kotlinc output: multiline + * declarations (unnamed companion objects, multiline secondary constructors) were collapsed + * onto their start line with the end character measured from that line's start. The true extent + * lives in the enclosing range. Reproduced here so anchors stay byte-identical and renderable + * by `scip snapshot`. + */ + fun anchorRange(range: TextRange): ScipRange? { + if (range.startOffset > text.length || range.endOffset > text.length) return null + val (startLine, startCharacter) = position(range.startOffset) + val (endLine, endCharacter) = position(range.endOffset) + if (startLine == endLine) { + return ScipRange.singleLine(startLine, startCharacter, endCharacter) + } + val flattenedEnd = range.endOffset - lineStart(startLine) + return ScipRange.singleLine(startLine, startCharacter, flattenedEnd) + } + + private fun lineStart(line: Int): Int = lineStartOffsets[line] + private fun position(offset: Int): Pair { var line = java.util.Arrays.binarySearch(lineStartOffsets, offset) if (line < 0) line = -line - 2 diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/Main.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/Main.kt new file mode 100644 index 000000000..5e6f41170 --- /dev/null +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/Main.kt @@ -0,0 +1,51 @@ +package org.scip_code.scip_java.kotlin_analysis + +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.system.exitProcess + +/** + * Command-line entry point, primarily used by the scip-snapshots harness. The scip-java CLI calls + * [KotlinAnalysisIndexer] in-process instead. + * + * Usage: `--sourceroot --targetroot [--classpath ] [--jdk-home + * ] ...` + */ +fun main(args: Array) { + var sourceroot: Path? = null + var targetroot: Path? = null + var classpath: List = emptyList() + var jdkHome: Path? = Paths.get(System.getProperty("java.home")) + val sourceRoots = mutableListOf() + var i = 0 + while (i < args.size) { + when (val arg = args[i]) { + "--sourceroot" -> sourceroot = Paths.get(args[++i]) + "--targetroot" -> targetroot = Paths.get(args[++i]) + "--classpath" -> + classpath = + args[++i] + .split(File.pathSeparator) + .filter { it.isNotEmpty() } + .map { Paths.get(it) } + "--jdk-home" -> jdkHome = Paths.get(args[++i]) + else -> sourceRoots.add(Paths.get(arg)) + } + i++ + } + require(sourceroot != null) { "missing required flag: --sourceroot" } + require(targetroot != null) { "missing required flag: --targetroot" } + require(sourceRoots.isNotEmpty()) { "missing source files or directories" } + KotlinAnalysisIndexer( + sourceroot = sourceroot, + targetroot = targetroot, + sourceRoots = sourceRoots, + classpath = classpath, + jdkHome = jdkHome, + ) + .run() + // The Analysis API environment leaves non-daemon threads behind; exit + // explicitly so callers are not left waiting on them. + exitProcess(0) +} diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt new file mode 100644 index 000000000..769130274 --- /dev/null +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt @@ -0,0 +1,220 @@ +package org.scip_code.scip_java.kotlin_analysis + +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import org.jetbrains.kotlin.analysis.api.KaSession +import org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource +import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtConstructor +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtObjectDeclaration +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtPropertyAccessor +import org.jetbrains.kotlin.psi.KtVariableDeclaration +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject +import org.jetbrains.kotlin.types.Variance + +/** + * Renders hover signatures in the exact textual format that scip-kotlinc's FirRenderer setup + * produced (see the goldens under scip-snapshots/expected/kotlin), so that migrating the indexer + * does not churn signature documentation. Explicitly written type annotations are taken verbatim + * from source; inferred types are rendered with short names via the Analysis API. + */ +@OptIn(KaExperimentalApi::class) +internal class SignatureRenderer(private val session: KaSession) { + + fun classSignature(declaration: KtClassOrObject): String { + if (declaration is KtObjectDeclaration && declaration.isObjectLiteral()) { + return "object : ${superTypesText(declaration)}" + } + val keyword = + when { + declaration is KtObjectDeclaration && declaration.isCompanion() -> + "companion object" + declaration is KtObjectDeclaration -> "object" + declaration is KtClass && declaration.isInterface() -> "interface" + declaration is KtClass && declaration.isEnum() -> "enum class" + else -> "class" + } + val name = declaration.name ?: "Companion" + return "${visibility(declaration)} ${classModality(declaration)} $keyword $name : " + + superTypesText(declaration) + } + + fun constructorSignature(constructor: KtConstructor<*>): String { + val owner = constructor.containingClassOrObject + val visibility = explicitVisibility(constructor) ?: "public" + return "$visibility constructor(${parametersText(constructor.valueParameters)}): " + + ownerName(owner) + } + + fun implicitConstructorSignature(declaration: KtClassOrObject): String { + val visibility = if (declaration is KtObjectDeclaration) "private" else "public" + return "$visibility constructor(): ${ownerName(declaration)}" + } + + fun functionSignature(declaration: KtNamedFunction): String { + val returnType = + declaration.typeReference?.text ?: renderedReturnType(declaration) ?: "Unit" + val signature = + "${visibility(declaration)} ${memberModality(declaration)}fun " + + "${declaration.name.orEmpty()}(${parametersText(declaration.valueParameters)}): " + + returnType + // FirRenderer emitted a trailing newline for functions without a body + // (see the Animal#sound() golden). + return if (declaration.hasBody()) signature else signature + "\n" + } + + fun propertySignature(declaration: KtDeclaration): String { + val name = (declaration as? org.jetbrains.kotlin.psi.KtNamedDeclaration)?.name.orEmpty() + val valOrVar = if (isVar(declaration)) "var" else "val" + val type = explicitPropertyType(declaration) ?: renderedReturnType(declaration) ?: "Any" + return "${visibility(declaration)} ${memberModality(declaration)}$valOrVar $name: $type" + } + + fun localVariableSignature(declaration: KtVariableDeclaration): String { + val valOrVar = if (declaration.isVar) "var" else "val" + val type = declaration.typeReference?.text ?: renderedReturnType(declaration) ?: "Any" + return "local $valOrVar ${declaration.name.orEmpty()}: $type" + } + + fun accessorSignature(accessor: KtPropertyAccessor): String { + val property = accessor.property + val visibility = explicitVisibility(accessor) ?: visibility(property) + val type = explicitPropertyType(property) ?: renderedReturnType(property) ?: "Any" + return if (accessor.isSetter) { + val parameterName = accessor.parameter?.name ?: "value" + "$visibility set($parameterName: $type): Unit" + } else { + "$visibility get(): $type" + } + } + + fun syntheticAccessorSignature(property: KtDeclaration, setter: Boolean): String { + val visibility = visibility(property) + val type = explicitPropertyType(property) ?: renderedReturnType(property) ?: "Any" + return if (setter) "$visibility set(value: $type): Unit" else "$visibility get(): $type" + } + + fun setterValueSignature(property: KtDeclaration): String { + val type = explicitPropertyType(property) ?: renderedReturnType(property) ?: "Any" + return "value: $type" + } + + fun parameterSignature(parameter: KtParameter): String { + val vararg = if (parameter.hasModifier(KtTokens.VARARG_KEYWORD)) "vararg " else "" + val type = parameter.typeReference?.text ?: renderedReturnType(parameter) ?: "Any" + val default = if (parameter.hasDefaultValue()) " = ..." else "" + return "$vararg${parameter.name.orEmpty()}: $type$default" + } + + fun implicitItSignature(target: KaCallableSymbol): String { + val type = + with(session) { + target.returnType.render( + KaTypeRendererForSource.WITH_SHORT_NAMES, + position = Variance.INVARIANT, + ) + } + return "it: $type" + } + + private fun ownerName(declaration: KtClassOrObject?): String { + if (declaration == null) return "" + if (declaration is KtObjectDeclaration && declaration.isObjectLiteral()) { + return "" + } + val names = mutableListOf() + var current: KtClassOrObject? = declaration + while (current != null) { + names.add(current.name ?: "Companion") + current = current.containingClassOrObject + } + return names.reversed().joinToString(".") + } + + private fun superTypesText(declaration: KtClassOrObject): String { + val entries = declaration.superTypeListEntries.mapNotNull { it.typeReference?.text } + return if (entries.isEmpty()) "Any" else entries.joinToString(", ") + } + + private fun parametersText(parameters: List): String = + parameters.joinToString(", ") { parameter -> + val type = parameter.typeReference?.text ?: "Any" + val default = if (parameter.hasDefaultValue()) " = ..." else "" + "${parameter.name.orEmpty()}: $type$default" + } + + private fun explicitPropertyType(declaration: KtDeclaration): String? = + when (declaration) { + is KtProperty -> declaration.typeReference?.text + is KtParameter -> declaration.typeReference?.text + else -> null + } + + private fun renderedReturnType(declaration: KtDeclaration): String? = + with(session) { + (declaration.symbol as? KaCallableSymbol) + ?.returnType + ?.render(KaTypeRendererForSource.WITH_SHORT_NAMES, position = Variance.INVARIANT) + } + + private fun isVar(declaration: KtDeclaration): Boolean = + when (declaration) { + is KtProperty -> declaration.isVar + is KtParameter -> declaration.isMutable + else -> false + } + + private fun visibility(declaration: KtDeclaration): String = + explicitVisibility(declaration) ?: "public" + + private fun explicitVisibility(declaration: KtDeclaration): String? = + when { + declaration.hasModifier(KtTokens.PRIVATE_KEYWORD) -> "private" + declaration.hasModifier(KtTokens.PROTECTED_KEYWORD) -> "protected" + declaration.hasModifier(KtTokens.INTERNAL_KEYWORD) -> "internal" + else -> null + } + + private fun classModality(declaration: KtClassOrObject): String = + when { + declaration is KtClass && declaration.isInterface() -> "abstract" + declaration.hasModifier(KtTokens.SEALED_KEYWORD) -> "sealed" + declaration.hasModifier(KtTokens.ABSTRACT_KEYWORD) -> "abstract" + declaration.hasModifier(KtTokens.OPEN_KEYWORD) -> "open" + else -> "final" + } + + /** Modality + `override`, with a trailing space; empty for abstract-less contexts. */ + private fun memberModality(declaration: KtDeclaration): String { + val containingInterface = + (declaration.containingClassOrObject as? KtClass)?.isInterface() == true + val isAbstract = + declaration.hasModifier(KtTokens.ABSTRACT_KEYWORD) || + (containingInterface && !hasImplementation(declaration)) + val isOverride = declaration.hasModifier(KtTokens.OVERRIDE_KEYWORD) + val modality = + when { + isAbstract -> "abstract" + declaration.hasModifier(KtTokens.OPEN_KEYWORD) || isOverride -> "open" + containingInterface -> "open" + else -> "final" + } + return if (isOverride) "$modality override " else "$modality " + } + + private fun hasImplementation(declaration: KtDeclaration): Boolean = + when (declaration) { + is KtNamedFunction -> declaration.hasBody() + is KtProperty -> + declaration.hasInitializer() || + declaration.hasDelegate() || + declaration.accessors.any { it.hasBody() } + else -> true + } +} diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt index cc5a42de8..518849de9 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt @@ -2,26 +2,33 @@ package org.scip_code.scip_java.kotlin_analysis import com.intellij.psi.PsiElement import java.lang.System.err +import org.jetbrains.kotlin.analysis.api.KaSession import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaClassLikeSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaConstructorSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaFunctionSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaPackageSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaTypeAliasSymbol +import org.jetbrains.kotlin.analysis.api.types.KaClassType import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtAnonymousInitializer +import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtClassBody import org.jetbrains.kotlin.psi.KtClassOrObject import org.jetbrains.kotlin.psi.KtConstructor import org.jetbrains.kotlin.psi.KtDeclaration import org.jetbrains.kotlin.psi.KtEnumEntry import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.KtFunctionLiteral import org.jetbrains.kotlin.psi.KtNamedDeclaration import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.KtObjectDeclaration import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.KtParameterList +import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtPropertyAccessor import org.jetbrains.kotlin.psi.KtSecondaryConstructor import org.jetbrains.kotlin.psi.KtTypeParameter @@ -73,17 +80,84 @@ class SymbolsCache { return symbol } + /** + * The SCIP symbol of the implicit primary constructor of [classOrObject], which has no PSI of + * its own. Explicit constructors go through [symbolForDeclaration]. + */ + fun implicitConstructorSymbol(classOrObject: KtClassOrObject): Symbol = + Symbol.createGlobal( + symbolForDeclaration(classOrObject), + ScipSymbolDescriptor(Kind.METHOD, ""), + ) + + /** + * The SCIP symbol of the synthetic getter/setter of a property (or `val`/`var` constructor + * parameter) that has no explicit accessor PSI. Accessors are owned by the property's container + * (`Class#getBanana().`), not by the constructor that declares the parameter. + */ + fun syntheticAccessorSymbol(property: KtDeclaration, setter: Boolean): Symbol { + val name = (property as? KtNamedDeclaration)?.name.orEmpty() + return Symbol.createGlobal(propertyOwnerSymbol(property), accessorDescriptor(name, setter)) + } + + /** The property symbol declared by a `val`/`var` constructor parameter (`Class#banana.`). */ + fun parameterPropertySymbol(parameter: KtParameter): Symbol = + Symbol.createGlobal( + propertyOwnerSymbol(parameter), + ScipSymbolDescriptor(Kind.TERM, parameter.name.orEmpty()), + ) + + /** The class symbol owning a constructor reference (`class Seagull : Bird()` → `Bird#`). */ + fun constructorOwnerSymbol(session: KaSession, target: KaConstructorSymbol): Symbol { + when (val psi = target.psi) { + is KtClassOrObject -> return symbolForDeclaration(psi) + is KtConstructor<*> -> + psi.containingClassOrObject?.let { + return symbolForDeclaration(it) + } + } + return target.containingClassId?.let(::classSymbol) ?: Symbol.NONE + } + + /** The SCIP symbol of a class referenced by its (expanded) [ClassId]. */ + fun classSymbol(classId: ClassId): Symbol = classIdSymbol(classId) + + private fun propertyOwnerSymbol(property: KtDeclaration): Symbol = + when (property) { + is KtParameter -> + (ownerDeclarationOfParameter(property) as? KtConstructor<*>) + ?.containingClassOrObject + ?.let { symbolForDeclaration(it) } ?: Symbol.NONE + else -> ownerSymbol(property) + } + + /** The `(value)` parameter symbol of a synthetic setter. */ + fun syntheticSetterValueSymbol(property: KtDeclaration): Symbol = + Symbol.createGlobal( + syntheticAccessorSymbol(property, setter = true), + ScipSymbolDescriptor(Kind.PARAMETER, "value"), + ) + + /** + * The SCIP symbol of the implicit `it` parameter of a lambda. The function literal PSI is used + * as the cache key; this cannot collide with the literal itself because anonymous functions + * never cache a symbol. + */ + fun implicitItSymbol(literal: KtFunctionLiteral): Symbol = + locals.get(literal) ?: locals.put(literal) + /** The SCIP symbol for the target of a resolved reference. */ - fun symbolForReference(target: KaSymbol): Symbol { + fun symbolForReference(session: KaSession, target: KaSymbol): Symbol { val psi = target.psi if (psi is KtDeclaration) { // A constructor symbol whose PSI is the class itself is an implicit // primary constructor. if (target is KaConstructorSymbol && psi is KtClassOrObject) { - return Symbol.createGlobal( - symbolForDeclaration(psi), - ScipSymbolDescriptor(Kind.METHOD, ""), - ) + return implicitConstructorSymbol(psi) + } + // The implicit `it` parameter's PSI is the enclosing function literal. + if (psi is KtFunctionLiteral && target !is KaFunctionSymbol) { + return implicitItSymbol(psi) } return symbolForDeclaration(psi) } @@ -96,18 +170,54 @@ class SymbolsCache { ScipSymbolDescriptor(Kind.METHOD, ""), ) } ?: Symbol.NONE + // Library type aliases are expanded to the aliased class, matching the + // FIR indexer (`AutoCloseable` → `java/lang/AutoCloseable#`) and + // keeping Kotlin and Java references to the same class unified. + is KaTypeAliasSymbol -> + with(session) { (target.expandedType as? KaClassType)?.classId } + ?.let(::classIdSymbol) ?: target.classId?.let(::classIdSymbol) ?: Symbol.NONE is KaClassLikeSymbol -> target.classId?.let(::classIdSymbol) ?: Symbol.NONE - is KaCallableSymbol -> externalCallableSymbol(target) + is KaCallableSymbol -> externalCallableSymbol(session, target) else -> Symbol.NONE } } + /** The getter/setter symbol of an external (classpath) property reference. */ + fun externalAccessorSymbol( + session: KaSession, + target: KaCallableSymbol, + setter: Boolean, + ): Symbol { + val callableId = target.callableId ?: return Symbol.NONE + val owner = + callableId.classId?.let(::classIdSymbol) ?: packageSymbol(callableId.packageName) + return Symbol.createGlobal( + owner, + accessorDescriptor(callableId.callableName.asString(), setter), + ) + } + + private fun accessorDescriptor(propertyName: String, setter: Boolean): ScipSymbolDescriptor = + ScipSymbolDescriptor( + Kind.METHOD, + (if (setter) "set" else "get") + propertyName.capitalizeAsciiOnly(), + ) + private fun uncachedSymbol(declaration: KtDeclaration): Symbol { // Anonymous functions and lambdas have no symbol of their own, mirroring // the FIR indexer which returned NONE for FirAnonymousFunctionSymbol. if (declaration is KtFunctionLiteral) return Symbol.NONE if (declaration is KtNamedFunction && declaration.name == null) return Symbol.NONE + // Anonymous objects are global symbols owned by the file's package, + // mirroring FIR where getContainingDeclaration returns null for them. + if (declaration is KtObjectDeclaration && declaration.isObjectLiteral()) { + return Symbol.createGlobal( + packageSymbol(declaration.containingKtFile.packageFqName), + scipDescriptor(declaration), + ) + } + if (isLocalMember(declaration)) return locals.put(declaration) val owner = ownerSymbol(declaration) @@ -116,18 +226,31 @@ class SymbolsCache { return Symbol.createGlobal(owner, scipDescriptor(declaration)) } - // Port of FIR's `isLocalMember`: only functions, variables and classes can be - // local members; everything else is classified through its owner. - private fun isLocalMember(declaration: KtDeclaration): Boolean = + /** + * Port of FIR's `isLocalMember`: functions, variables and classes declared in code bodies are + * local. Members of any class-like container — including anonymous objects — are not: locality + * is decided by the nearest scope owner, not by syntactic nesting. + */ + private fun isLocalMember(declaration: KtDeclaration): Boolean { when (declaration) { is KtNamedFunction, is KtVariableDeclaration, - is KtClassOrObject -> - declaration.parents - .takeWhile { it !is KtFile } - .any { it !is KtClassBody && it !is KtClassOrObject } - else -> false + is KtClassOrObject -> {} + else -> return false + } + for (parent in declaration.parents) { + when (parent) { + is KtClassBody, + is KtFile -> return false + is KtBlockExpression, + is KtFunction, + is KtPropertyAccessor, + is KtProperty, + is KtAnonymousInitializer -> return true + } } + return false + } private fun ownerSymbol(declaration: KtDeclaration): Symbol = when (declaration) { @@ -160,15 +283,9 @@ class SymbolsCache { declaration is KtClassOrObject -> ScipSymbolDescriptor(Kind.TYPE, declaration.name.orEmpty()) declaration is KtPropertyAccessor && declaration.isSetter -> - ScipSymbolDescriptor( - Kind.METHOD, - "set" + declaration.property.name.orEmpty().capitalizeAsciiOnly(), - ) + accessorDescriptor(declaration.property.name.orEmpty(), setter = true) declaration is KtPropertyAccessor -> - ScipSymbolDescriptor( - Kind.METHOD, - "get" + declaration.property.name.orEmpty().capitalizeAsciiOnly(), - ) + accessorDescriptor(declaration.property.name.orEmpty(), setter = false) declaration is KtConstructor<*> -> ScipSymbolDescriptor(Kind.METHOD, "", constructorDisambiguator(declaration)) declaration is KtNamedFunction -> @@ -236,19 +353,45 @@ class SymbolsCache { internal fun isDeclarationParameter(parameter: KtParameter): Boolean = ownerDeclarationOfParameter(parameter) != null - private fun externalCallableSymbol(target: KaCallableSymbol): Symbol { + /** + * External (classpath/JDK) callables, disambiguated by their position among the same-named + * callables of their container — mirroring FIR, which consulted the class's declared members or + * the top-level symbol provider (e.g. `kotlin/collections/forEachIndexed(+9).`). + */ + private fun externalCallableSymbol(session: KaSession, target: KaCallableSymbol): Symbol { val callableId = target.callableId ?: return Symbol.NONE val owner = callableId.classId?.let(::classIdSymbol) ?: packageSymbol(callableId.packageName) val name = callableId.callableName.asString() val descriptor = when (target) { - is KaFunctionSymbol -> ScipSymbolDescriptor(Kind.METHOD, name) + is KaFunctionSymbol -> + ScipSymbolDescriptor( + Kind.METHOD, + name, + externalMethodDisambiguator(session, target), + ) else -> ScipSymbolDescriptor(Kind.TERM, name) } return Symbol.createGlobal(owner, descriptor) } + private fun externalMethodDisambiguator(session: KaSession, target: KaCallableSymbol): String { + val callableId = target.callableId ?: return "()" + val overloads: List = + with(session) { + val classId = callableId.classId + if (classId == null) { + findTopLevelCallables(callableId.packageName, callableId.callableName).toList() + } else { + val classSymbol = findClass(classId) ?: return "()" + classSymbol.declaredMemberScope.callables(callableId.callableName).toList() + } + } + val index = overloads.indexOfFirst { it == target } + return if (index <= 0) "()" else "(+$index)" + } + private fun classIdSymbol(classId: ClassId): Symbol { var symbol = packageSymbol(classId.packageFqName) for (segment in classId.relativeClassName.pathSegments()) { diff --git a/scip-snapshots/cases/kotlin/common/build.gradle.kts b/scip-snapshots/cases/kotlin/common/build.gradle.kts index 0dc573c2d..be5226192 100644 --- a/scip-snapshots/cases/kotlin/common/build.gradle.kts +++ b/scip-snapshots/cases/kotlin/common/build.gradle.kts @@ -17,6 +17,16 @@ dependencies { implementation(libs.kotlin.stdlib) } +// Runtime classpath of the Analysis-API-based indexer, used by the +// scipIndexKotlinAnalysis comparison task below. +val scipKotlinAnalysis: Configuration by configurations.creating { + isCanBeConsumed = false +} + +dependencies { + scipKotlinAnalysis(project(":scip-kotlin-analysis")) +} + val scipTargetroot = layout.buildDirectory.dir("scip-targetroot") val sourceroot = rootProject.rootDir.absolutePath val targetroot = scipTargetroot.get().asFile.absolutePath @@ -35,3 +45,36 @@ tasks.named("compileJava") { } publishDirectoryArtifact("scipTargetrootElements", scipTargetroot, tasks.named("classes")) + +// Indexes the same Kotlin sources with the standalone Analysis API indexer into a +// separate targetroot, so its SCIP output can be compared against scip-kotlinc's. +val scipAnalysisTargetroot = layout.buildDirectory.dir("scip-targetroot-analysis") + +tasks.register("scipIndexKotlinAnalysis") { + classpath = scipKotlinAnalysis + mainClass.set("org.scip_code.scip_java.kotlin_analysis.MainKt") + val kotlinSources = layout.projectDirectory.dir("src/main/kotlin") + val compileClasspath = sourceSets.main.map { it.compileClasspath } + inputs.dir(kotlinSources) + inputs.files(compileClasspath) + outputs.dir(scipAnalysisTargetroot) + cleanDirectoryBeforeRunning(scipAnalysisTargetroot) + // Locals only: the argument provider must not capture the build script + // (configuration cache). + val sourcerootArg = sourceroot + val targetrootArg = scipAnalysisTargetroot + val kotlinSourcesArg = kotlinSources.asFile.absolutePath + argumentProviders.add( + CommandLineArgumentProvider { + listOf( + "--sourceroot", + sourcerootArg, + "--targetroot", + targetrootArg.get().asFile.absolutePath, + "--classpath", + compileClasspath.get().asPath, + kotlinSourcesArg, + ) + } + ) +} From abd33f86966759d8a9d5eda3f96c78779345570d Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 12:03:17 +0200 Subject: [PATCH 04/15] Extend kotlin snapshots and close indexer parity gaps --- .../kotlin_analysis/KotlinAnalysisIndexer.kt | 297 +++++++++++++++++- .../kotlin_analysis/SignatureRenderer.kt | 86 ++++- .../scip_java/kotlin_analysis/SymbolsCache.kt | 44 ++- .../src/main/kotlin/snapshots/Annotations.kt | 10 + .../main/kotlin/snapshots/Destructuring.kt | 10 + .../common/src/main/kotlin/snapshots/Enums.kt | 18 ++ .../src/main/kotlin/snapshots/Generics.kt | 16 + .../src/main/kotlin/snapshots/Annotations.kt | 66 ++++ .../main/kotlin/snapshots/Destructuring.kt | 182 +++++++++++ .../common/src/main/kotlin/snapshots/Enums.kt | 182 +++++++++++ .../src/main/kotlin/snapshots/Generics.kt | 148 +++++++++ 11 files changed, 1040 insertions(+), 19 deletions(-) create mode 100644 scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt create mode 100644 scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt create mode 100644 scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt create mode 100644 scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt create mode 100644 scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt create mode 100644 scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt create mode 100644 scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt create mode 100644 scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt index 3455bc0a7..b76e45f86 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt @@ -33,12 +33,14 @@ import org.jetbrains.kotlin.psi.KtFunctionLiteral import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.KtObjectDeclaration import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.psi.KtParameterList import org.jetbrains.kotlin.psi.KtPrimaryConstructor import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtPropertyAccessor import org.jetbrains.kotlin.psi.KtSecondaryConstructor import org.jetbrains.kotlin.psi.KtSimpleNameExpression import org.jetbrains.kotlin.psi.KtTreeVisitorVoid +import org.jetbrains.kotlin.psi.KtTypeAlias import org.jetbrains.kotlin.psi.KtTypeParameter import org.jetbrains.kotlin.psi.KtVariableDeclaration import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject @@ -148,6 +150,7 @@ class KotlinAnalysisIndexer( } } + @OptIn(org.jetbrains.kotlin.analysis.api.KaExperimentalApi::class) private class IndexingVisitor( private val session: KaSession, private val cache: SymbolsCache, @@ -157,7 +160,9 @@ class KotlinAnalysisIndexer( private val signatures = SignatureRenderer(session) override fun visitDeclaration(dcl: KtDeclaration) { - super.visitDeclaration(dcl) + // Definitions are emitted before visiting children so that same-range + // occurrence order and local-symbol numbering match scip-kotlinc, + // which processed declarations before their bodies. when (dcl) { is KtEnumEntry -> emitEnumEntry(dcl) is KtClassOrObject -> emitClassLike(dcl) @@ -167,9 +172,11 @@ class KotlinAnalysisIndexer( is KtParameter -> emitParameter(dcl) is KtPropertyAccessor -> emitExplicitAccessor(dcl) is KtTypeParameter -> emitTypeParameter(dcl) - is KtDestructuringDeclarationEntry -> emitLocalVariable(dcl) + is KtTypeAlias -> emitTypeAlias(dcl) + is KtDestructuringDeclarationEntry -> emitDestructuringEntry(dcl) else -> {} } + super.visitDeclaration(dcl) } override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) { @@ -242,9 +249,13 @@ class KotlinAnalysisIndexer( relationships = superTypeRelationships(declaration), ) // Classes and objects (but not interfaces) declare an implicit primary - // constructor when no explicit one is present in source. + // constructor when no explicit one is present in source. A primary + // constructor without the `constructor` keyword also anchors at the class + // identifier and is emitted here so the class definition stays first at + // that range (matching scip-kotlinc's order); emitConstructor skips it. val isInterface = declaration is KtClass && declaration.isInterface() - if (!isInterface && declaration.primaryConstructor == null) { + val primaryConstructor = declaration.primaryConstructor + if (!isInterface && primaryConstructor == null) { emitDefinition( symbol = cache.implicitConstructorSymbol(declaration), range = range, @@ -253,9 +264,87 @@ class KotlinAnalysisIndexer( signatureText = signatures.implicitConstructorSignature(declaration), documentation = docComment(declaration), ) + } else if ( + primaryConstructor != null && primaryConstructor.getConstructorKeyword() == null + ) { + emitDefinition( + symbol = cache.symbolForDeclaration(primaryConstructor), + range = range, + enclosing = primaryConstructor.textRange, + displayName = displayName, + signatureText = signatures.constructorSignature(primaryConstructor), + documentation = docComment(primaryConstructor), + ) + } + if (declaration is KtClass && declaration.isEnum()) { + emitEnumSynthetics(declaration, range) + } + if (declaration is KtClass && declaration.isData()) { + declaration.primaryConstructor?.let { constructor -> + emitDefinition( + symbol = cache.memberMethodSymbol(declaration, "copy"), + range = constructor.textRange, + enclosing = constructor.textRange, + displayName = "copy", + signatureText = signatures.dataCopySignature(declaration), + documentation = null, + ) + } } } + /** + * Compiler-generated enum members: `values()`, `valueOf(value)` and `entries` (plus its + * getter), all anchored at the enum class identifier like in scip-kotlinc. Unlike + * scip-kotlinc, the entries getter is owned by the enum class instead of the package — the + * old symbol (`snapshots/getEntries().`) was a bug. + */ + private fun emitEnumSynthetics(declaration: KtClass, range: TextRange) { + val name = declaration.name.orEmpty() + val enclosing = declaration.textRange + emitDefinition( + symbol = cache.memberMethodSymbol(declaration, "values"), + range = range, + enclosing = enclosing, + displayName = "values", + signatureText = signatures.enumValuesSignature(name), + documentation = null, + ) + val valueOf = cache.memberMethodSymbol(declaration, "valueOf") + emitDefinition( + symbol = valueOf, + range = range, + enclosing = enclosing, + displayName = "valueOf", + signatureText = signatures.enumValueOfSignature(name), + documentation = null, + ) + emitDefinition( + symbol = cache.methodParameterSymbol(valueOf, "value"), + range = range, + enclosing = enclosing, + displayName = "value", + signatureText = "value: String", + documentation = null, + ) + emitDefinition( + symbol = cache.memberTermSymbol(declaration, "entries"), + range = range, + enclosing = enclosing, + displayName = "entries", + signatureText = signatures.enumEntriesSignature(name), + documentation = null, + ) + emitDefinition( + symbol = cache.memberMethodSymbol(declaration, "getEntries"), + range = range, + enclosing = enclosing, + displayName = "entries", + signatureText = signatures.enumGetEntriesSignature(name), + documentation = null, + ) + } + private fun emitEnumEntry(declaration: KtEnumEntry) { val identifier = declaration.nameIdentifier ?: return emitDefinition( @@ -271,10 +360,9 @@ class KotlinAnalysisIndexer( private fun emitConstructor(declaration: KtConstructor<*>) { val range = when (declaration) { + // Keyword-less primary constructors are emitted by emitClassLike. is KtPrimaryConstructor -> - declaration.getConstructorKeyword()?.textRange - ?: declaration.containingClassOrObject?.nameIdentifier?.textRange - ?: return + declaration.getConstructorKeyword()?.textRange ?: return is KtSecondaryConstructor -> declaration.textRange else -> return } @@ -341,8 +429,42 @@ class KotlinAnalysisIndexer( } private fun emitParameter(declaration: KtParameter) { - // Parameters of function types, catch clauses and for loops are not - // declarations of their own. + val loopOrCatchIdentifier = declaration.nameIdentifier + // `for` loop variables and `catch` parameters are local declarations, + // mirroring the FIR indexer (which also assigned their local numbers at + // the declaration site). + if (loopOrCatchIdentifier != null) { + if (declaration.parent is org.jetbrains.kotlin.psi.KtForExpression) { + val type = + declaration.typeReference?.text + ?: renderedParameterType(declaration) + ?: "Any" + emitDefinition( + symbol = cache.symbolForDeclaration(declaration), + range = loopOrCatchIdentifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + signatureText = "local val ${declaration.name.orEmpty()}: $type", + documentation = null, + ) + return + } + if ( + (declaration.parent as? KtParameterList)?.parent + is org.jetbrains.kotlin.psi.KtCatchClause + ) { + emitDefinition( + symbol = cache.symbolForDeclaration(declaration), + range = loopOrCatchIdentifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + signatureText = signatures.parameterSignature(declaration), + documentation = null, + ) + return + } + } + // Parameters of function types are not declarations of their own. if (!cache.isDeclarationParameter(declaration)) return val identifier = declaration.nameIdentifier ?: return val symbol = cache.symbolForDeclaration(declaration) @@ -376,9 +498,166 @@ class KotlinAnalysisIndexer( hasExplicitSetter = false, isVar = declaration.isMutable, ) + emitDataClassParameterSynthetics(declaration, identifier.textRange) + } + } + + /** + * A data-class constructor parameter additionally declares `componentN()` and a `copy()` + * parameter, and the generated `copy()` default value reads the property — all anchored at + * the parameter identifier, mirroring scip-kotlinc. + */ + private fun emitDataClassParameterSynthetics(declaration: KtParameter, range: TextRange) { + val constructor = declaration.parent?.parent as? KtPrimaryConstructor ?: return + val containingClass = constructor.containingClassOrObject as? KtClass ?: return + if (!containingClass.isData()) return + val index = constructor.valueParameters.indexOf(declaration) + if (index < 0) return + emitDefinition( + symbol = cache.memberMethodSymbol(containingClass, "component${index + 1}"), + range = range, + enclosing = declaration.textRange, + displayName = "component${index + 1}", + signatureText = signatures.dataComponentSignature(index + 1, declaration), + documentation = null, + ) + emitDefinition( + symbol = + cache.methodParameterSymbol( + cache.memberMethodSymbol(containingClass, "copy"), + declaration.name.orEmpty(), + ), + range = range, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + signatureText = signatures.dataCopyParameterSignature(declaration), + documentation = null, + ) + addReference(cache.parameterPropertySymbol(declaration), range) + addReference(cache.syntheticAccessorSymbol(declaration, setter = false), range) + if (declaration.isMutable) { + addReference(cache.syntheticAccessorSymbol(declaration, setter = true), range) + } + } + + private fun emitTypeAlias(declaration: KtTypeAlias) { + val identifier = declaration.nameIdentifier ?: return + emitDefinition( + symbol = cache.symbolForDeclaration(declaration), + range = identifier.textRange, + enclosing = declaration.textRange, + displayName = declaration.name.orEmpty(), + signatureText = signatures.typeAliasSignature(declaration), + documentation = docComment(declaration), + ) + } + + /** + * `when (subject)` introduces a synthetic `` local in the FIR desugaring; + * scip-kotlinc emitted a definition for it at the subject expression. Emitted before + * visiting children so local numbering matches. + */ + override fun visitWhenExpression(expression: org.jetbrains.kotlin.psi.KtWhenExpression) { + val subject = expression.subjectExpression + if (subject != null && expression.subjectVariable == null) { + emitDefinition( + symbol = cache.syntheticLocalSymbol(expression), + range = subject.textRange, + enclosing = subject.textRange, + displayName = "", + signatureText = + signatures.whenSubjectSignature(renderedExpressionType(subject)), + documentation = null, + ) } + super.visitWhenExpression(expression) } + /** + * Destructuring introduces a synthetic `` local holding the destructured value; + * each entry references its `componentN()` function and the `` local. Emitted + * before visiting children so local numbering matches. + */ + override fun visitDestructuringDeclaration( + destructuringDeclaration: org.jetbrains.kotlin.psi.KtDestructuringDeclaration + ) { + val isParameter = destructuringDeclaration.parent is KtParameter + val type = + destructuringDeclaration.initializer?.let { renderedExpressionType(it) } + ?: (destructuringDeclaration.parent as? KtParameter)?.let { + renderedParameterType(it) + } + ?: "Any" + emitDefinition( + symbol = cache.syntheticLocalSymbol(destructuringDeclaration), + range = destructuringDeclaration.textRange, + enclosing = destructuringDeclaration.textRange, + displayName = "", + signatureText = signatures.destructSignature(type, isParameter), + documentation = null, + ) + super.visitDestructuringDeclaration(destructuringDeclaration) + } + + private fun emitDestructuringEntry(declaration: KtDestructuringDeclarationEntry) { + emitLocalVariable(declaration) + val range = declaration.nameIdentifier?.textRange ?: return + val destructuring = + declaration.parent as? org.jetbrains.kotlin.psi.KtDestructuringDeclaration ?: return + // Each entry desugars to a `componentN()` call on the destructured value. + // Standalone analysis does not resolve destructuring-entry references, so + // the symbol is derived from the destructured type and the entry position. + val index = destructuring.entries.indexOf(declaration) + destructuredClassId(destructuring)?.let { classId -> + addReference( + Symbol.createGlobal( + cache.classSymbol(classId), + ScipSymbolDescriptor( + ScipSymbolDescriptor.Kind.METHOD, + "component${index + 1}", + ), + ), + range, + ) + } + addReference(cache.syntheticLocalSymbol(destructuring), range) + } + + private fun destructuredClassId( + destructuring: org.jetbrains.kotlin.psi.KtDestructuringDeclaration + ): ClassId? = + with(session) { + val type = + destructuring.initializer?.expressionType + ?: (destructuring.parent as? KtParameter)?.let { + (it.symbol as? KaCallableSymbol)?.returnType + } + (type as? KaClassType)?.classId + } + + private fun renderedExpressionType( + expression: org.jetbrains.kotlin.psi.KtExpression + ): String = + with(session) { + expression.expressionType?.render( + org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource + .WITH_SHORT_NAMES, + position = org.jetbrains.kotlin.types.Variance.INVARIANT, + ) + } ?: "Any" + + private fun renderedParameterType(parameter: KtParameter): String? = + with(session) { + (parameter.symbol as? KaCallableSymbol) + ?.returnType + ?.render( + org.jetbrains.kotlin.analysis.api.renderer.types.impl + .KaTypeRendererForSource + .WITH_SHORT_NAMES, + position = org.jetbrains.kotlin.types.Variance.INVARIANT, + ) + } + private fun emitSyntheticAccessors( property: KtDeclaration, nameRange: TextRange, diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt index 769130274..7469d45c3 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt @@ -14,6 +14,7 @@ import org.jetbrains.kotlin.psi.KtObjectDeclaration import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtPropertyAccessor +import org.jetbrains.kotlin.psi.KtTypeAlias import org.jetbrains.kotlin.psi.KtVariableDeclaration import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject import org.jetbrains.kotlin.types.Variance @@ -38,30 +39,36 @@ internal class SignatureRenderer(private val session: KaSession) { declaration is KtObjectDeclaration -> "object" declaration is KtClass && declaration.isInterface() -> "interface" declaration is KtClass && declaration.isEnum() -> "enum class" + declaration is KtClass && declaration.isAnnotation() -> "annotation class" + declaration is KtClass && declaration.isData() -> "data class" else -> "class" } val name = declaration.name ?: "Companion" - return "${visibility(declaration)} ${classModality(declaration)} $keyword $name : " + + val typeParameters = (declaration as? KtClass)?.typeParameterList?.text.orEmpty() + return "${annotationsPrefix(declaration)}${visibility(declaration)} " + + "${classModality(declaration)} $keyword $name$typeParameters : " + superTypesText(declaration) } fun constructorSignature(constructor: KtConstructor<*>): String { val owner = constructor.containingClassOrObject - val visibility = explicitVisibility(constructor) ?: "public" - return "$visibility constructor(${parametersText(constructor.valueParameters)}): " + - ownerName(owner) + val visibility = explicitVisibility(constructor) ?: constructorVisibility(owner) + return "$visibility constructor${classTypeParameters(owner)}" + + "(${parametersText(constructor.valueParameters)}): " + + constructorReturnType(owner) } fun implicitConstructorSignature(declaration: KtClassOrObject): String { - val visibility = if (declaration is KtObjectDeclaration) "private" else "public" - return "$visibility constructor(): ${ownerName(declaration)}" + return "${constructorVisibility(declaration)} constructor(): ${ownerName(declaration)}" } fun functionSignature(declaration: KtNamedFunction): String { val returnType = declaration.typeReference?.text ?: renderedReturnType(declaration) ?: "Unit" + val typeParameters = declaration.typeParameterList?.text?.let { "$it " }.orEmpty() val signature = - "${visibility(declaration)} ${memberModality(declaration)}fun " + + "${annotationsPrefix(declaration)}${visibility(declaration)} " + + "${memberModality(declaration)}fun $typeParameters" + "${declaration.name.orEmpty()}(${parametersText(declaration.valueParameters)}): " + returnType // FirRenderer emitted a trailing newline for functions without a body @@ -69,6 +76,64 @@ internal class SignatureRenderer(private val session: KaSession) { return if (declaration.hasBody()) signature else signature + "\n" } + fun typeAliasSignature(declaration: KtTypeAlias): String = + "public final typealias ${declaration.name.orEmpty()} = " + + declaration.getTypeReference()?.text.orEmpty() + + "\n" + + /** `values()` / `valueOf(value: String)` / `entries` signatures of an enum class. */ + fun enumValuesSignature(name: String): String = "public final static fun values(): Array<$name>" + + fun enumValueOfSignature(name: String): String = + "public final static fun valueOf(value: String): $name" + + fun enumEntriesSignature(name: String): String = + "public final static val entries: EnumEntries<$name>" + + fun enumGetEntriesSignature(name: String): String = "public get(): EnumEntries<$name>" + + fun dataCopySignature(declaration: KtClass): String { + val parameters = + declaration.primaryConstructor?.valueParameters.orEmpty().joinToString(", ") { + "${it.name.orEmpty()}: ${it.typeReference?.text ?: "Any"} = ..." + } + return "public final fun copy($parameters): ${declaration.name.orEmpty()}\n" + } + + fun dataComponentSignature(index: Int, parameter: KtParameter): String = + "public final operator fun component$index(): ${parameter.typeReference?.text ?: "Any"}\n" + + fun dataCopyParameterSignature(parameter: KtParameter): String = + "${parameter.name.orEmpty()}: ${parameter.typeReference?.text ?: "Any"} = ..." + + fun whenSubjectSignature(type: String): String = "local val : $type" + + fun destructSignature(type: String, isParameter: Boolean): String = + if (isParameter) ": $type" else "local val : $type" + + private fun constructorVisibility(declaration: KtClassOrObject?): String = + when { + declaration is KtObjectDeclaration -> "private" + declaration is KtClass && declaration.isEnum() -> "private" + else -> "public" + } + + private fun classTypeParameters(declaration: KtClassOrObject?): String = + (declaration as? KtClass)?.typeParameterList?.text.orEmpty() + + private fun constructorReturnType(declaration: KtClassOrObject?): String { + val name = ownerName(declaration) + val typeArguments = + (declaration as? KtClass)?.typeParameters?.mapNotNull { it.name }.orEmpty() + return if (typeArguments.isEmpty()) name else "$name<${typeArguments.joinToString(", ")}>" + } + + private fun annotationsPrefix(declaration: KtDeclaration): String = + declaration.annotationEntries.joinToString("") { entry -> + val name = entry.shortName?.asString().orEmpty() + if (entry.valueArgumentList != null) "@$name(...) " else "@$name " + } + fun propertySignature(declaration: KtDeclaration): String { val name = (declaration as? org.jetbrains.kotlin.psi.KtNamedDeclaration)?.name.orEmpty() val valOrVar = if (isVar(declaration)) "var" else "val" @@ -139,7 +204,12 @@ internal class SignatureRenderer(private val session: KaSession) { private fun superTypesText(declaration: KtClassOrObject): String { val entries = declaration.superTypeListEntries.mapNotNull { it.typeReference?.text } - return if (entries.isEmpty()) "Any" else entries.joinToString(", ") + if (entries.isNotEmpty()) return entries.joinToString(", ") + return when { + declaration is KtClass && declaration.isEnum() -> "Enum<${declaration.name.orEmpty()}>" + declaration is KtClass && declaration.isAnnotation() -> "Annotation" + else -> "Any" + } } private fun parametersText(parameters: List): String = diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt index 518849de9..3f260a000 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt @@ -31,6 +31,7 @@ import org.jetbrains.kotlin.psi.KtParameterList import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtPropertyAccessor import org.jetbrains.kotlin.psi.KtSecondaryConstructor +import org.jetbrains.kotlin.psi.KtTypeAlias import org.jetbrains.kotlin.psi.KtTypeParameter import org.jetbrains.kotlin.psi.KtTypeParameterListOwner import org.jetbrains.kotlin.psi.KtVariableDeclaration @@ -122,6 +123,24 @@ class SymbolsCache { /** The SCIP symbol of a class referenced by its (expanded) [ClassId]. */ fun classSymbol(classId: ClassId): Symbol = classIdSymbol(classId) + /** Symbol of a compiler-generated member method (enum `values()`, data-class `copy()`, …). */ + fun memberMethodSymbol(owner: KtClassOrObject, name: String): Symbol = + Symbol.createGlobal(symbolForDeclaration(owner), ScipSymbolDescriptor(Kind.METHOD, name)) + + /** Symbol of a compiler-generated member property (enum `entries`). */ + fun memberTermSymbol(owner: KtClassOrObject, name: String): Symbol = + Symbol.createGlobal(symbolForDeclaration(owner), ScipSymbolDescriptor(Kind.TERM, name)) + + /** Symbol of a parameter of a compiler-generated method. */ + fun methodParameterSymbol(method: Symbol, name: String): Symbol = + Symbol.createGlobal(method, ScipSymbolDescriptor(Kind.PARAMETER, name)) + + /** + * A per-document local symbol for a compiler-generated declaration without dedicated PSI + * (implicit `it`, ``, ``), keyed by the surrounding element. + */ + fun syntheticLocalSymbol(key: PsiElement): Symbol = locals.get(key) ?: locals.put(key) + private fun propertyOwnerSymbol(property: KtDeclaration): Symbol = when (property) { is KtParameter -> @@ -143,8 +162,7 @@ class SymbolsCache { * as the cache key; this cannot collide with the literal itself because anonymous functions * never cache a symbol. */ - fun implicitItSymbol(literal: KtFunctionLiteral): Symbol = - locals.get(literal) ?: locals.put(literal) + fun implicitItSymbol(literal: KtFunctionLiteral): Symbol = syntheticLocalSymbol(literal) /** The SCIP symbol for the target of a resolved reference. */ fun symbolForReference(session: KaSession, target: KaSymbol): Symbol { @@ -159,6 +177,26 @@ class SymbolsCache { if (psi is KtFunctionLiteral && target !is KaFunctionSymbol) { return implicitItSymbol(psi) } + // Source type aliases are expanded like library ones, matching scip-kotlinc. + if (psi is KtTypeAlias && target is KaTypeAliasSymbol) { + return with(session) { (target.expandedType as? KaClassType)?.classId } + ?.let(::classIdSymbol) ?: symbolForDeclaration(psi) + } + // Compiler-generated callables (data-class componentN/copy, enum + // `entries`, …) resolve to PSI of the class or parameter they derive + // from; their symbol comes from the callable id instead. + if ( + target is KaFunctionSymbol && + target !is KaConstructorSymbol && + psi !is KtNamedFunction && + psi !is KtPropertyAccessor && + psi !is KtFunctionLiteral + ) { + return externalCallableSymbol(session, target) + } + if (target is KaCallableSymbol && psi is KtClassOrObject) { + return externalCallableSymbol(session, target) + } return symbolForDeclaration(psi) } return when (target) { @@ -294,6 +332,8 @@ class SymbolsCache { declaration.name.orEmpty(), methodDisambiguator(declaration), ) + declaration is KtTypeAlias -> + ScipSymbolDescriptor(Kind.TYPE, declaration.name.orEmpty()) declaration is KtTypeParameter -> ScipSymbolDescriptor(Kind.TYPE_PARAMETER, declaration.name.orEmpty()) declaration is KtParameter -> diff --git a/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt b/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt new file mode 100644 index 000000000..72c2d7dce --- /dev/null +++ b/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt @@ -0,0 +1,10 @@ +package snapshots + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +annotation class Tagged(val tag: String) + +@Tagged("service") +class AnnotatedService { + @Tagged("run") + fun run(): String = "running" +} diff --git a/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt b/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt new file mode 100644 index 000000000..ca2646890 --- /dev/null +++ b/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt @@ -0,0 +1,10 @@ +package snapshots + +data class Point(val x: Int, val y: Int) + +fun destructure(): Int { + val (x, y) = Point(1, 2) + val labeled = listOf(Point(3, 4) to "label") + val total = labeled.sumOf { (point, label) -> point.x + label.length } + return x + y + total +} diff --git a/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt b/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt new file mode 100644 index 000000000..32366c6d5 --- /dev/null +++ b/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt @@ -0,0 +1,18 @@ +package snapshots + +enum class Suit(val symbol: Char) { + HEARTS('h'), + SPADES('s'); + + fun isRed(): Boolean = symbol == 'h' + + companion object { + fun fromSymbol(symbol: Char): Suit? = entries.find { it.symbol == symbol } + } +} + +fun describe(suit: Suit): String = + when (suit) { + Suit.HEARTS -> "red" + Suit.SPADES -> "black" + } diff --git a/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt b/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt new file mode 100644 index 000000000..d917e3c04 --- /dev/null +++ b/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt @@ -0,0 +1,16 @@ +package snapshots + +class Container>(private val items: MutableList) { + fun add(item: T): Container { + items.add(item) + return this + } + + fun mapItems(transform: (T) -> R?): List = items.mapNotNull(transform) +} + +fun firstOrSelf(items: List, fallback: T): T = items.firstOrNull() ?: fallback + +typealias StringContainer = Container + +fun useContainer(container: StringContainer): StringContainer = container.add("hello") diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt new file mode 100644 index 000000000..a458ba724 --- /dev/null +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt @@ -0,0 +1,66 @@ + package snapshots +// ^^^^^^^^^ reference scip-java maven . . snapshots/ + +//⌄ enclosing_range_start scip-java maven . . snapshots/Tagged# + @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +// ^^^^^ reference scip-java maven . . kotlin/annotation/AnnotationTarget#CLASS. +// ^^^^^^^^ reference scip-java maven . . kotlin/annotation/AnnotationTarget#FUNCTION. +// ⌄ enclosing_range_start scip-java maven . . snapshots/Tagged#``(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Tagged#``().(tag) +// ⌄ enclosing_range_start scip-java maven . . snapshots/Tagged#tag. +// ⌄ enclosing_range_start scip-java maven . . snapshots/Tagged#getTag(). + annotation class Tagged(val tag: String) +// ^^^^^^ definition scip-java maven . . snapshots/Tagged# +// display_name Tagged +// signature_documentation +// > @Target(...) public final annotation class Tagged : Annotation +// relationship scip-java maven . . kotlin/Annotation# implementation +// ^^^^^^ definition scip-java maven . . snapshots/Tagged#``(). +// display_name Tagged +// signature_documentation +// > public constructor(tag: String): Tagged +// ^^^ definition scip-java maven . . snapshots/Tagged#``().(tag) +// display_name tag +// signature_documentation +// > tag: String +// ^^^ definition scip-java maven . . snapshots/Tagged#tag. +// display_name tag +// signature_documentation +// > public final val tag: String +// ^^^ reference scip-java maven . . snapshots/Tagged#``().(tag) +// ^^^ definition scip-java maven . . snapshots/Tagged#getTag(). +// display_name tag +// signature_documentation +// > public get(): String +// ^^^^^^ reference scip-java maven . . kotlin/String# +// ⌃ enclosing_range_end scip-java maven . . snapshots/Tagged#``().(tag) +// ⌃ enclosing_range_end scip-java maven . . snapshots/Tagged#tag. +// ⌃ enclosing_range_end scip-java maven . . snapshots/Tagged#getTag(). +// ⌃ enclosing_range_end scip-java maven . . snapshots/Tagged# +// ⌃ enclosing_range_end scip-java maven . . snapshots/Tagged#``(). + +//⌄ enclosing_range_start scip-java maven . . snapshots/AnnotatedService# +//⌄ enclosing_range_start scip-java maven . . snapshots/AnnotatedService#``(). + @Tagged("service") + class AnnotatedService { +// ^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/AnnotatedService# +// display_name AnnotatedService +// signature_documentation +// > @Tagged(...) public final class AnnotatedService : Any +// ^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/AnnotatedService#``(). +// display_name AnnotatedService +// signature_documentation +// > public constructor(): AnnotatedService +// ⌄ enclosing_range_start scip-java maven . . snapshots/AnnotatedService#run(). + @Tagged("run") + fun run(): String = "running" +// ^^^ definition scip-java maven . . snapshots/AnnotatedService#run(). +// display_name run +// signature_documentation +// > @Tagged(...) public final fun run(): String +// ^^^^^^ reference scip-java maven . . kotlin/String# +// ⌃ enclosing_range_end scip-java maven . . snapshots/AnnotatedService#run(). + } +//⌃ enclosing_range_end scip-java maven . . snapshots/AnnotatedService# +//⌃ enclosing_range_end scip-java maven . . snapshots/AnnotatedService#``(). + diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt new file mode 100644 index 000000000..56bc9c232 --- /dev/null +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt @@ -0,0 +1,182 @@ + package snapshots +// ^^^^^^^^^ reference scip-java maven . . snapshots/ + +//⌄ enclosing_range_start scip-java maven . . snapshots/Point# +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#``(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#copy(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#``().(x) +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#x. +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#getX(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#component1(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#copy().(x) +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#``().(y) +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#y. +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#getY(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#component2(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#copy().(y) + data class Point(val x: Int, val y: Int) +// ^^^^^ definition scip-java maven . . snapshots/Point# +// display_name Point +// signature_documentation +// > public final data class Point : Any +// ^^^^^ definition scip-java maven . . snapshots/Point#``(). +// display_name Point +// signature_documentation +// > public constructor(x: Int, y: Int): Point +// ^^^^^^^^^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/Point#copy(). +// display_name copy +// signature_documentation +// > public final fun copy(x: Int = ..., y: Int = ...): Point +// > +// ^ definition scip-java maven . . snapshots/Point#``().(x) +// display_name x +// signature_documentation +// > x: Int +// ^ definition scip-java maven . . snapshots/Point#x. +// display_name x +// signature_documentation +// > public final val x: Int +// ^ reference scip-java maven . . snapshots/Point#``().(x) +// ^ definition scip-java maven . . snapshots/Point#getX(). +// display_name x +// signature_documentation +// > public get(): Int +// ^ definition scip-java maven . . snapshots/Point#component1(). +// display_name component1 +// signature_documentation +// > public final operator fun component1(): Int +// > +// ^ definition scip-java maven . . snapshots/Point#copy().(x) +// display_name x +// signature_documentation +// > x: Int = ... +// ^ reference scip-java maven . . snapshots/Point#x. +// ^ reference scip-java maven . . snapshots/Point#getX(). +// ^^^ reference scip-java maven . . kotlin/Int# +// ^ definition scip-java maven . . snapshots/Point#``().(y) +// display_name y +// signature_documentation +// > y: Int +// ^ definition scip-java maven . . snapshots/Point#y. +// display_name y +// signature_documentation +// > public final val y: Int +// ^ reference scip-java maven . . snapshots/Point#``().(y) +// ^ definition scip-java maven . . snapshots/Point#getY(). +// display_name y +// signature_documentation +// > public get(): Int +// ^ definition scip-java maven . . snapshots/Point#component2(). +// display_name component2 +// signature_documentation +// > public final operator fun component2(): Int +// > +// ^ definition scip-java maven . . snapshots/Point#copy().(y) +// display_name y +// signature_documentation +// > y: Int = ... +// ^ reference scip-java maven . . snapshots/Point#y. +// ^ reference scip-java maven . . snapshots/Point#getY(). +// ^^^ reference scip-java maven . . kotlin/Int# +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point#``().(x) +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point#x. +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point#getX(). +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point#component1(). +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point#copy().(x) +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point#``().(y) +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point#y. +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point#getY(). +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point#component2(). +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point#copy().(y) +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point# +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point#``(). +// ⌃ enclosing_range_end scip-java maven . . snapshots/Point#copy(). + +//⌄ enclosing_range_start scip-java maven . . snapshots/destructure(). + fun destructure(): Int { +// ^^^^^^^^^^^ definition scip-java maven . . snapshots/destructure(). +// display_name destructure +// signature_documentation +// > public final fun destructure(): Int +// ^^^ reference scip-java maven . . kotlin/Int# +// ⌄ enclosing_range_start local 0 +// ⌄ enclosing_range_start local 1 +// ⌄ enclosing_range_start local 2 + val (x, y) = Point(1, 2) +// ^^^^^^^^^^^^^^^^^^^^^^^^ definition local 0 +// display_name +// signature_documentation +// > local val : Point +// ^ definition local 1 +// display_name x +// signature_documentation +// > local val x: Int +// ^ reference scip-java maven . . snapshots/Point#component1(). +// ^ reference local 0 +// ^ definition local 2 +// display_name y +// signature_documentation +// > local val y: Int +// ^ reference scip-java maven . . snapshots/Point#component2(). +// ^ reference local 0 +// ^^^^^ reference scip-java maven . . snapshots/Point#``(). +// ⌃ enclosing_range_end local 1 +// ⌃ enclosing_range_end local 2 +// ⌃ enclosing_range_end local 0 +// ⌄ enclosing_range_start local 3 + val labeled = listOf(Point(3, 4) to "label") +// ^^^^^^^ definition local 3 +// display_name labeled +// signature_documentation +// > local val labeled: List> +// ^^^^^^ reference scip-java maven . . kotlin/collections/listOf(). +// ^^^^^ reference scip-java maven . . snapshots/Point#``(). +// ^^ reference scip-java maven . . kotlin/to(). +// ⌃ enclosing_range_end local 3 +// ⌄ enclosing_range_start local 4 +// ⌄ enclosing_range_start local 5 +// ⌄ enclosing_range_start local 6 +// ⌄ enclosing_range_start local 7 + val total = labeled.sumOf { (point, label) -> point.x + label.length } +// ^^^^^ definition local 4 +// display_name total +// signature_documentation +// > local val total: Int +// ^^^^^^^ reference local 3 +// ^^^^^ reference scip-java maven . . kotlin/collections/sumOf(+66). +// ^^^^^^^^^^^^^^ definition local 5 +// display_name +// signature_documentation +// > : Pair +// ^^^^^ definition local 6 +// display_name point +// signature_documentation +// > local val point: Point +// ^^^^^ reference scip-java maven . . kotlin/Pair#component1(). +// ^^^^^ reference local 5 +// ^^^^^ definition local 7 +// display_name label +// signature_documentation +// > local val label: String +// ^^^^^ reference scip-java maven . . kotlin/Pair#component2(). +// ^^^^^ reference local 5 +// ^^^^^ reference local 6 +// ^ reference scip-java maven . . snapshots/Point#x. +// ^ reference scip-java maven . . snapshots/Point#getX(). +// ^ reference scip-java maven . . kotlin/Int#plus(+2). +// ^^^^^ reference local 7 +// ^^^^^^ reference scip-java maven . . kotlin/String#length. +// ^^^^^^ reference scip-java maven . . kotlin/String#getLength(). +// ⌃ enclosing_range_end local 6 +// ⌃ enclosing_range_end local 7 +// ⌃ enclosing_range_end local 5 +// ⌃ enclosing_range_end local 4 + return x + y + total +// ^ reference local 1 +// ^ reference scip-java maven . . kotlin/Int#plus(+2). +// ^ reference local 2 +// ^ reference scip-java maven . . kotlin/Int#plus(+2). +// ^^^^^ reference local 4 + } +//⌃ enclosing_range_end scip-java maven . . snapshots/destructure(). + diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt new file mode 100644 index 000000000..3361608b0 --- /dev/null +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt @@ -0,0 +1,182 @@ + package snapshots +// ^^^^^^^^^ reference scip-java maven . . snapshots/ + +//⌄ enclosing_range_start scip-java maven . . snapshots/Suit# +//⌄ enclosing_range_start scip-java maven . . snapshots/Suit#values(). +//⌄ enclosing_range_start scip-java maven . . snapshots/Suit#valueOf(). +//⌄ enclosing_range_start scip-java maven . . snapshots/Suit#valueOf().(value) +//⌄ enclosing_range_start scip-java maven . . snapshots/Suit#entries. +//⌄ enclosing_range_start scip-java maven . . snapshots/getEntries(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#``(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#``().(symbol) +// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#symbol. +// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#getSymbol(). + enum class Suit(val symbol: Char) { +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ reference scip-java maven . . kotlin/Enum# +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ reference scip-java maven . . snapshots/Suit# +// ^^^^ definition scip-java maven . . snapshots/Suit# +// display_name Suit +// signature_documentation +// > public final enum class Suit : Enum +// relationship scip-java maven . . kotlin/Enum# implementation +// ^^^^ definition scip-java maven . . snapshots/Suit#``(). +// display_name Suit +// signature_documentation +// > private constructor(symbol: Char): Suit +// ^^^^ definition scip-java maven . . snapshots/Suit#values(). +// display_name values +// signature_documentation +// > public final static fun values(): Array +// ^^^^ definition scip-java maven . . snapshots/Suit#valueOf(). +// display_name valueOf +// signature_documentation +// > public final static fun valueOf(value: String): Suit +// ^^^^ definition scip-java maven . . snapshots/Suit#valueOf().(value) +// display_name value +// signature_documentation +// > value: String +// ^^^^ definition scip-java maven . . snapshots/Suit#entries. +// display_name entries +// signature_documentation +// > public final static val entries: EnumEntries +// ^^^^ definition scip-java maven . . snapshots/getEntries(). +// display_name entries +// signature_documentation +// > public get(): EnumEntries +// ^^^^^^ definition scip-java maven . . snapshots/Suit#``().(symbol) +// display_name symbol +// signature_documentation +// > symbol: Char +// ^^^^^^ definition scip-java maven . . snapshots/Suit#symbol. +// display_name symbol +// signature_documentation +// > public final val symbol: Char +// ^^^^^^ reference scip-java maven . . snapshots/Suit#``().(symbol) +// ^^^^^^ definition scip-java maven . . snapshots/Suit#getSymbol(). +// display_name symbol +// signature_documentation +// > public get(): Char +// ^^^^ reference scip-java maven . . kotlin/Char# +// ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#``().(symbol) +// ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#symbol. +// ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#getSymbol(). +// ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#``(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/``# +// ⌄ enclosing_range_start scip-java maven . . snapshots/``#``(). + HEARTS('h'), +// ^^^^^^ definition scip-java maven . . snapshots/``# +// display_name +// signature_documentation +// > object : Suit +// relationship scip-java maven . . snapshots/Suit# implementation +// ^^^^^^ definition scip-java maven . . snapshots/``#``(). +// display_name HEARTS +// signature_documentation +// > private constructor(): +// ⌃ enclosing_range_end scip-java maven . . snapshots/``# +// ⌃ enclosing_range_end scip-java maven . . snapshots/``#``(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/``# +// ⌄ enclosing_range_start scip-java maven . . snapshots/``#``(). + SPADES('s'); +// ^^^^^^ definition scip-java maven . . snapshots/``# +// display_name +// signature_documentation +// > object : Suit +// relationship scip-java maven . . snapshots/Suit# implementation +// ^^^^^^ definition scip-java maven . . snapshots/``#``(). +// display_name SPADES +// signature_documentation +// > private constructor(): +// ⌃ enclosing_range_end scip-java maven . . snapshots/``# +// ⌃ enclosing_range_end scip-java maven . . snapshots/``#``(). + +// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#isRed(). + fun isRed(): Boolean = symbol == 'h' +// ^^^^^ definition scip-java maven . . snapshots/Suit#isRed(). +// display_name isRed +// signature_documentation +// > public final fun isRed(): Boolean +// ^^^^^^^ reference scip-java maven . . kotlin/Boolean# +// ^^^^^^ reference scip-java maven . . snapshots/Suit#symbol. +// ^^^^^^ reference scip-java maven . . snapshots/Suit#getSymbol(). +// ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#isRed(). + +// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#Companion# +// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#Companion#``(). + companion object { +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/Suit#Companion# +// display_name Companion +// signature_documentation +// > public final companion object Companion : Any +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/Suit#Companion#``(). +// display_name Companion +// signature_documentation +// > private constructor(): Suit.Companion +// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#Companion#fromSymbol(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#Companion#fromSymbol().(symbol) +// ⌄ enclosing_range_start local 0 + fun fromSymbol(symbol: Char): Suit? = entries.find { it.symbol == symbol } +// ^^^^^^^^^^ definition scip-java maven . . snapshots/Suit#Companion#fromSymbol(). +// display_name fromSymbol +// signature_documentation +// > public final fun fromSymbol(symbol: Char): Suit? +// ^^^^^^ definition scip-java maven . . snapshots/Suit#Companion#fromSymbol().(symbol) +// display_name symbol +// signature_documentation +// > symbol: Char +// ^^^^ reference scip-java maven . . kotlin/Char# +// ^^^^^ reference scip-java maven . . snapshots/Suit# +// ^^^^^^^ reference scip-java maven . . snapshots/Suit#entries. +// ^^^^^^^ reference scip-java maven . . snapshots/getEntries(). +// ^^^^ reference scip-java maven . . kotlin/collections/find(+9). +// ^^^^^^^^^^^^^^^^^^^^^^^ definition local 0 +// display_name it +// signature_documentation +// > it: Suit +// ^^ reference local 0 +// ^^^^^^ reference scip-java maven . . snapshots/Suit#symbol. +// ^^^^^^ reference scip-java maven . . snapshots/Suit#getSymbol(). +// ^^^^^^ reference scip-java maven . . snapshots/Suit#Companion#fromSymbol().(symbol) +// ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#Companion#fromSymbol().(symbol) +// ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#Companion#fromSymbol(). +// ⌃ enclosing_range_end local 0 + } +// ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#Companion# +// ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#Companion#``(). + } +//⌃ enclosing_range_end scip-java maven . . snapshots/Suit# +//⌃ enclosing_range_end scip-java maven . . snapshots/Suit#values(). +//⌃ enclosing_range_end scip-java maven . . snapshots/Suit#valueOf(). +//⌃ enclosing_range_end scip-java maven . . snapshots/Suit#valueOf().(value) +//⌃ enclosing_range_end scip-java maven . . snapshots/Suit#entries. +//⌃ enclosing_range_end scip-java maven . . snapshots/getEntries(). + +//⌄ enclosing_range_start scip-java maven . . snapshots/describe(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/describe().(suit) + fun describe(suit: Suit): String = +// ^^^^^^^^ definition scip-java maven . . snapshots/describe(). +// display_name describe +// signature_documentation +// > public final fun describe(suit: Suit): String +// ^^^^ definition scip-java maven . . snapshots/describe().(suit) +// display_name suit +// signature_documentation +// > suit: Suit +// ^^^^ reference scip-java maven . . snapshots/Suit# +// ^^^^^^ reference scip-java maven . . kotlin/String# +// ⌃ enclosing_range_end scip-java maven . . snapshots/describe().(suit) +// ⌄ enclosing_range_start local 1 + when (suit) { +// ^^^^ definition local 1 +// display_name +// signature_documentation +// > local val : Suit +// ^^^^ reference scip-java maven . . snapshots/describe().(suit) +// ⌃ enclosing_range_end local 1 + Suit.HEARTS -> "red" +// ^^^^^^ reference scip-java maven . . snapshots/Suit#HEARTS. + Suit.SPADES -> "black" +// ^^^^^^ reference scip-java maven . . snapshots/Suit#SPADES. + } +// ⌃ enclosing_range_end scip-java maven . . snapshots/describe(). + diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt new file mode 100644 index 000000000..f2731f886 --- /dev/null +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt @@ -0,0 +1,148 @@ + package snapshots +// ^^^^^^^^^ reference scip-java maven . . snapshots/ + +//⌄ enclosing_range_start scip-java maven . . snapshots/Container# +// ⌄ enclosing_range_start scip-java maven . . snapshots/Container#[T] +// ⌄ enclosing_range_start scip-java maven . . snapshots/Container#``(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Container#``().(items) +// ⌄ enclosing_range_start scip-java maven . . snapshots/Container#items. +// ⌄ enclosing_range_start scip-java maven . . snapshots/Container#getItems(). + class Container>(private val items: MutableList) { +// ^^^^^^^^^ definition scip-java maven . . snapshots/Container# +// display_name Container +// signature_documentation +// > public final class Container> : Any +// ^^^^^^^^^ definition scip-java maven . . snapshots/Container#``(). +// display_name Container +// signature_documentation +// > public constructor>(items: MutableList): Container +// ^ definition scip-java maven . . snapshots/Container#[T] +// display_name FirTypeParameterSymbol T +// signature_documentation +// > T : Comparable +// ^^^^^ definition scip-java maven . . snapshots/Container#``().(items) +// display_name items +// signature_documentation +// > items: MutableList +// ^^^^^ definition scip-java maven . . snapshots/Container#items. +// display_name items +// signature_documentation +// > private final val items: MutableList +// ^^^^^ reference scip-java maven . . snapshots/Container#``().(items) +// ^^^^^ definition scip-java maven . . snapshots/Container#getItems(). +// display_name items +// signature_documentation +// > private get(): MutableList +// ^^^^^^^^^^^^^^ reference scip-java maven . . kotlin/collections/MutableList# +// ⌃ enclosing_range_end scip-java maven . . snapshots/Container#[T] +// ⌃ enclosing_range_end scip-java maven . . snapshots/Container#``().(items) +// ⌃ enclosing_range_end scip-java maven . . snapshots/Container#items. +// ⌃ enclosing_range_end scip-java maven . . snapshots/Container#getItems(). +// ⌃ enclosing_range_end scip-java maven . . snapshots/Container#``(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Container#add(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Container#add().(item) + fun add(item: T): Container { +// ^^^ definition scip-java maven . . snapshots/Container#add(). +// display_name add +// signature_documentation +// > public final fun add(item: T): Container +// ^^^^ definition scip-java maven . . snapshots/Container#add().(item) +// display_name item +// signature_documentation +// > item: T +// ^^^^^^^^^^^^ reference scip-java maven . . snapshots/Container# +// ⌃ enclosing_range_end scip-java maven . . snapshots/Container#add().(item) + items.add(item) +// ^^^^^ reference scip-java maven . . snapshots/Container#items. +// ^^^^^ reference scip-java maven . . snapshots/Container#getItems(). +// ^^^ reference scip-java maven . . kotlin/collections/MutableList#add(). +// ^^^^ reference scip-java maven . . snapshots/Container#add().(item) + return this + } +// ⌃ enclosing_range_end scip-java maven . . snapshots/Container#add(). + +// ⌄ enclosing_range_start scip-java maven . . snapshots/Container#mapItems(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Container#mapItems().[R] +// ⌄ enclosing_range_start scip-java maven . . snapshots/Container#mapItems().(transform) + fun mapItems(transform: (T) -> R?): List = items.mapNotNull(transform) +// ^ definition scip-java maven . . snapshots/Container#mapItems().[R] +// display_name FirTypeParameterSymbol R +// signature_documentation +// > R : Any +// ^^^^^^^^ definition scip-java maven . . snapshots/Container#mapItems(). +// display_name mapItems +// signature_documentation +// > public final fun mapItems(transform: (T) -> R?): List +// ^^^^^^^^^ definition scip-java maven . . snapshots/Container#mapItems().(transform) +// display_name transform +// signature_documentation +// > transform: (T) -> R? +// ^^^^^^^^^ reference scip-java maven . . kotlin/Function1# +// ^^^^^^^ reference scip-java maven . . kotlin/collections/List# +// ^^^^^ reference scip-java maven . . snapshots/Container#items. +// ^^^^^ reference scip-java maven . . snapshots/Container#getItems(). +// ^^^^^^^^^^ reference scip-java maven . . kotlin/collections/mapNotNull(+1). +// ^^^^^^^^^ reference scip-java maven . . snapshots/Container#mapItems().(transform) +// ⌃ enclosing_range_end scip-java maven . . snapshots/Container#mapItems().[R] +// ⌃ enclosing_range_end scip-java maven . . snapshots/Container#mapItems().(transform) +// ⌃ enclosing_range_end scip-java maven . . snapshots/Container#mapItems(). + } +//⌃ enclosing_range_end scip-java maven . . snapshots/Container# + +//⌄ enclosing_range_start scip-java maven . . snapshots/firstOrSelf(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/firstOrSelf().[T] +// ⌄ enclosing_range_start scip-java maven . . snapshots/firstOrSelf().(items) +// ⌄ enclosing_range_start scip-java maven . . snapshots/firstOrSelf().(fallback) + fun firstOrSelf(items: List, fallback: T): T = items.firstOrNull() ?: fallback +// ^ definition scip-java maven . . snapshots/firstOrSelf().[T] +// display_name FirTypeParameterSymbol T +// signature_documentation +// > T +// ^^^^^^^^^^^ definition scip-java maven . . snapshots/firstOrSelf(). +// display_name firstOrSelf +// signature_documentation +// > public final fun firstOrSelf(items: List, fallback: T): T +// ^^^^^ definition scip-java maven . . snapshots/firstOrSelf().(items) +// display_name items +// signature_documentation +// > items: List +// ^^^^^^^ reference scip-java maven . . kotlin/collections/List# +// ^^^^^^^^ definition scip-java maven . . snapshots/firstOrSelf().(fallback) +// display_name fallback +// signature_documentation +// > fallback: T +// ^^^^^ reference scip-java maven . . snapshots/firstOrSelf().(items) +// ^^^^^^^^^^^ reference scip-java maven . . kotlin/collections/firstOrNull(+19). +// ^^^^^^^^ reference scip-java maven . . snapshots/firstOrSelf().(fallback) +// ⌃ enclosing_range_end scip-java maven . . snapshots/firstOrSelf().[T] +// ⌃ enclosing_range_end scip-java maven . . snapshots/firstOrSelf().(items) +// ⌃ enclosing_range_end scip-java maven . . snapshots/firstOrSelf().(fallback) +// ⌃ enclosing_range_end scip-java maven . . snapshots/firstOrSelf(). + +//⌄ enclosing_range_start scip-java maven . . snapshots/StringContainer# + typealias StringContainer = Container +// ^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/StringContainer# +// display_name FirTypeAliasSymbol snapshots/StringContainer +// signature_documentation +// > public final typealias StringContainer = Container +// > +// ⌃ enclosing_range_end scip-java maven . . snapshots/StringContainer# + +//⌄ enclosing_range_start scip-java maven . . snapshots/useContainer(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/useContainer().(container) + fun useContainer(container: StringContainer): StringContainer = container.add("hello") +// ^^^^^^^^^^^^ definition scip-java maven . . snapshots/useContainer(). +// display_name useContainer +// signature_documentation +// > public final fun useContainer(container: {snapshots/StringContainer=} Container): {snapshots/StringContainer=} Container +// ^^^^^^^^^ definition scip-java maven . . snapshots/useContainer().(container) +// display_name container +// signature_documentation +// > container: {snapshots/StringContainer=} Container +// ^^^^^^^^^^^^^^^ reference scip-java maven . . snapshots/Container# +// ^^^^^^^^^^^^^^^ reference scip-java maven . . snapshots/Container# +// ^^^^^^^^^ reference scip-java maven . . snapshots/useContainer().(container) +// ^^^ reference scip-java maven . . snapshots/Container#add(). +// ⌃ enclosing_range_end scip-java maven . . snapshots/useContainer().(container) +// ⌃ enclosing_range_end scip-java maven . . snapshots/useContainer(). + From 08045204d53d5fe55a4dd724e10ff0d49ba91295 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 12:17:24 +0200 Subject: [PATCH 05/15] Fix indexer divergences found in large-corpus A/B comparison --- .../kotlin_analysis/KotlinAnalysisIndexer.kt | 37 ++++++++++++++----- .../kotlin_analysis/SignatureRenderer.kt | 13 +++++-- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt index b76e45f86..66f2f335b 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt @@ -609,16 +609,33 @@ class KotlinAnalysisIndexer( // the symbol is derived from the destructured type and the entry position. val index = destructuring.entries.indexOf(declaration) destructuredClassId(destructuring)?.let { classId -> - addReference( - Symbol.createGlobal( - cache.classSymbol(classId), - ScipSymbolDescriptor( - ScipSymbolDescriptor.Kind.METHOD, - "component${index + 1}", - ), - ), - range, - ) + val name = "component${index + 1}" + // `componentN` may be a member (data classes, Pair) or an extension + // (e.g. Map.Entry's componentN lives in kotlin.collections). + val isMember = + with(session) { + findClass(classId) + ?.declaredMemberScope + ?.callables(Name.identifier(name)) + ?.any() == true + } + val symbol = + if (isMember) { + Symbol.createGlobal( + cache.classSymbol(classId), + ScipSymbolDescriptor(ScipSymbolDescriptor.Kind.METHOD, name), + ) + } else { + with(session) { + findTopLevelCallables(classId.packageFqName, Name.identifier(name)) + .firstOrNull { extension -> + (extension.receiverParameter?.returnType as? KaClassType) + ?.classId == classId + } + ?.let { cache.symbolForReference(session, it) } + } ?: Symbol.NONE + } + addReference(symbol, range) } addReference(cache.syntheticLocalSymbol(destructuring), range) } diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt index 7469d45c3..947e10cfa 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt @@ -41,6 +41,7 @@ internal class SignatureRenderer(private val session: KaSession) { declaration is KtClass && declaration.isEnum() -> "enum class" declaration is KtClass && declaration.isAnnotation() -> "annotation class" declaration is KtClass && declaration.isData() -> "data class" + declaration is KtClass && declaration.isInner() -> "inner class" else -> "class" } val name = declaration.name ?: "Companion" @@ -97,7 +98,7 @@ internal class SignatureRenderer(private val session: KaSession) { declaration.primaryConstructor?.valueParameters.orEmpty().joinToString(", ") { "${it.name.orEmpty()}: ${it.typeReference?.text ?: "Any"} = ..." } - return "public final fun copy($parameters): ${declaration.name.orEmpty()}\n" + return "public final fun copy($parameters): ${ownerName(declaration)}\n" } fun dataComponentSignature(index: Int, parameter: KtParameter): String = @@ -131,20 +132,24 @@ internal class SignatureRenderer(private val session: KaSession) { private fun annotationsPrefix(declaration: KtDeclaration): String = declaration.annotationEntries.joinToString("") { entry -> val name = entry.shortName?.asString().orEmpty() - if (entry.valueArgumentList != null) "@$name(...) " else "@$name " + val useSite = entry.useSiteTarget?.let { "${it.text}:" }.orEmpty() + val arguments = if (entry.valueArguments.isEmpty()) "()" else "(...)" + "$useSite@$name$arguments " } fun propertySignature(declaration: KtDeclaration): String { val name = (declaration as? org.jetbrains.kotlin.psi.KtNamedDeclaration)?.name.orEmpty() + val const = if (declaration.hasModifier(KtTokens.CONST_KEYWORD)) "const " else "" + val lateinit = if (declaration.hasModifier(KtTokens.LATEINIT_KEYWORD)) "lateinit " else "" val valOrVar = if (isVar(declaration)) "var" else "val" val type = explicitPropertyType(declaration) ?: renderedReturnType(declaration) ?: "Any" - return "${visibility(declaration)} ${memberModality(declaration)}$valOrVar $name: $type" + return "${visibility(declaration)} ${memberModality(declaration)}$const$lateinit$valOrVar $name: $type" } fun localVariableSignature(declaration: KtVariableDeclaration): String { val valOrVar = if (declaration.isVar) "var" else "val" val type = declaration.typeReference?.text ?: renderedReturnType(declaration) ?: "Any" - return "local $valOrVar ${declaration.name.orEmpty()}: $type" + return "${annotationsPrefix(declaration)}local $valOrVar ${declaration.name.orEmpty()}: $type" } fun accessorSignature(accessor: KtPropertyAccessor): String { From e47ac00a146c3f4debf31702634a35e31caa6eae Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 12:44:49 +0200 Subject: [PATCH 06/15] Replace scip-kotlinc with scip-kotlin-analysis everywhere --- .github/workflows/ci.yaml | 8 +- CONTRIBUTING.md | 2 +- .../scip_java/buildlogic/ScipCompile.kt | 24 - gradle/libs.versions.toml | 10 - .../scip_java/gradle/ScipGradlePlugin.java | 89 +- scip-java/build.gradle.kts | 4 - .../org/scip_code/scip_java/Embedded.kt | 2 - .../scip_java/buildtools/GradleBuildTool.kt | 51 +- scip-kotlinc/build.gradle.kts | 37 - .../scip_java/kotlinc/AnalyzerCheckers.kt | 467 ------ .../kotlinc/AnalyzerCommandLineProcessor.kt | 47 - .../kotlinc/AnalyzerFirExtensionRegistrar.kt | 11 - .../kotlinc/AnalyzerParamsProvider.kt | 22 - .../scip_java/kotlinc/AnalyzerRegistrar.kt | 31 - .../scip_code/scip_java/kotlinc/LineMap.kt | 39 - .../kotlinc/PostAnalysisExtension.kt | 93 -- .../scip_java/kotlinc/ScipSymbols.kt | 70 - .../kotlinc/ScipTextDocumentBuilder.kt | 198 --- .../scip_java/kotlinc/ScipVisitor.kt | 186 --- .../scip_java/kotlinc/SymbolsCache.kt | 223 --- ...otlin.compiler.plugin.CommandLineProcessor | 1 - ...in.compiler.plugin.CompilerPluginRegistrar | 1 - .../scip_java/kotlinc/test/AnalyzerTest.kt | 1432 ----------------- .../scip_java/kotlinc/test/ScipBuilders.kt | 121 -- .../scip_java/kotlinc/test/ScipSymbolsTest.kt | 793 --------- .../scip_code/scip_java/kotlinc/test/Utils.kt | 206 --- scip-snapshots/README.md | 2 +- .../cases/kotlin/common/build.gradle.kts | 50 +- .../src/main/kotlin/snapshots/Annotations.kt | 5 + .../common/src/main/kotlin/snapshots/Class.kt | 2 + .../main/kotlin/snapshots/CompanionOwner.kt | 1 + .../common/src/main/kotlin/snapshots/Enums.kt | 45 +- .../src/main/kotlin/snapshots/Generics.kt | 35 +- .../main/kotlin/snapshots/Implementations.kt | 2 +- .../src/main/kotlin/snapshots/Lambdas.kt | 1 + .../src/main/kotlin/snapshots/ObjectKt.kt | 2 +- settings.gradle.kts | 1 - 37 files changed, 184 insertions(+), 4130 deletions(-) delete mode 100644 scip-kotlinc/build.gradle.kts delete mode 100644 scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt delete mode 100644 scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCommandLineProcessor.kt delete mode 100644 scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerFirExtensionRegistrar.kt delete mode 100644 scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerParamsProvider.kt delete mode 100644 scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt delete mode 100644 scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/LineMap.kt delete mode 100644 scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt delete mode 100644 scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipSymbols.kt delete mode 100644 scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt delete mode 100644 scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt delete mode 100644 scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt delete mode 100644 scip-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor delete mode 100644 scip-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar delete mode 100644 scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt delete mode 100644 scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipBuilders.kt delete mode 100644 scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt delete mode 100644 scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dbebb16d9..38990c085 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,9 +22,9 @@ jobs: - run: gradle spotlessCheck --no-daemon - kotlin_plugin: + kotlin_indexer: runs-on: ubuntu-latest - name: scip-kotlinc + name: scip-kotlin-analysis steps: - uses: actions/checkout@v7 @@ -37,8 +37,8 @@ jobs: with: gradle-version: 9.4.1 - - name: scip-kotlinc tests - run: gradle :scip-kotlinc:test --no-daemon + - name: scip-kotlin-analysis tests + run: gradle :scip-kotlin-analysis:test --no-daemon docker_test: runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85c7f09c1..ba814e94d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,7 +54,7 @@ These are the main components of the project. | ------------------------------------------------------- | -------- | ----------------------------------------------------------------------- | | `gradle test --no-daemon` | terminal | Run all Gradle tests. | | `gradle :scip-java:test --no-daemon` | terminal | Run CLI build-tool integration tests (Gradle, Maven, SCIP config). | -| `gradle :scip-kotlinc:test --no-daemon` | terminal | Run Kotlin compiler-plugin tests. | +| `gradle :scip-kotlin-analysis:test --no-daemon` | terminal | Run Kotlin indexer tests. | | `gradle :scip-snapshots:test --no-daemon` | terminal | Compare Java and Kotlin snapshot goldens. | | `gradle :scip-snapshots:saveSnapshots --no-daemon` | terminal | Regenerate Java and Kotlin snapshot goldens. | | `gradle :scip-java:installDist --no-daemon` | terminal | Build a local `scip-java` distribution under `scip-java/build/install/`. | diff --git a/build-logic/src/main/kotlin/org/scip_code/scip_java/buildlogic/ScipCompile.kt b/build-logic/src/main/kotlin/org/scip_code/scip_java/buildlogic/ScipCompile.kt index 079e9b877..7d766e266 100644 --- a/build-logic/src/main/kotlin/org/scip_code/scip_java/buildlogic/ScipCompile.kt +++ b/build-logic/src/main/kotlin/org/scip_code/scip_java/buildlogic/ScipCompile.kt @@ -4,7 +4,6 @@ import java.io.File import org.gradle.api.Task import org.gradle.api.artifacts.Configuration import org.gradle.api.file.Directory -import org.gradle.api.file.FileSystemLocation import org.gradle.api.provider.Provider import org.gradle.api.tasks.compile.JavaCompile @@ -27,29 +26,6 @@ fun JavaCompile.useScipJavac( (options.forkOptions.jvmArgs ?: emptyList()) + JavacInternals.jvmOptions(rootDir) } -/** - * Builds the `kotlinc` arguments that load the scip-kotlinc compiler plugin from [pluginClasspath] - * (the resolved shaded jar) and point it at [sourceroot]/[targetroot]. - * - * The mapping lives here, in compiled build logic, rather than in a build script: a `.map {}` - * lambda declared in a `.gradle.kts` file captures a hidden reference to the script object, which - * the configuration cache cannot serialize. - */ -fun scipKotlincPluginArgs( - pluginClasspath: Provider>, - sourceroot: String, - targetroot: String, -): Provider> = - pluginClasspath.map { locations -> - listOf( - "-Xplugin=${locations.single().asFile.absolutePath}", - "-P", - "plugin:scip-kotlinc:sourceroot=$sourceroot", - "-P", - "plugin:scip-kotlinc:targetroot=$targetroot", - ) - } - /** * Registers a `doFirst` action that empties [dir] (deletes then recreates it) before the task runs. * diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f7cbec268..6a26038e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,6 @@ clikt = "5.1.0" gradle-api = "8.11.1" junit-jupiter = "5.11.4" -kctfork = "0.7.1" -kotest = "6.2.1" kotlin = "2.2.0" # Kotlin Analysis API engine bundled by scip-kotlin-analysis. Independent from # the `kotlin` toolchain version: `-for-ide` artifacts are only published as @@ -30,8 +28,6 @@ caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version = "2.9.3 clikt-jvm = { module = "com.github.ajalt.clikt:clikt-jvm", version.ref = "clikt" } gradle-api = { module = "dev.gradleplugins:gradle-api", version.ref = "gradle-api" } gradle-test-kit = { module = "dev.gradleplugins:gradle-test-kit", version.ref = "gradle-api" } -kctfork-core = { module = "dev.zacsweers.kctfork:core", version.ref = "kctfork" } -kotest-assertions-core = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest" } kotlin-analysis-api-api = { module = "org.jetbrains.kotlin:analysis-api-for-ide", version.ref = "kotlin-analysis-api" } kotlin-analysis-api-impl-base = { module = "org.jetbrains.kotlin:analysis-api-impl-base-for-ide", version.ref = "kotlin-analysis-api" } kotlin-analysis-api-k2 = { module = "org.jetbrains.kotlin:analysis-api-k2-for-ide", version.ref = "kotlin-analysis-api" } @@ -40,12 +36,6 @@ kotlin-analysis-api-standalone = { module = "org.jetbrains.kotlin:analysis-api-s kotlin-analysis-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler", version.ref = "kotlin-analysis-api" } kotlin-analysis-low-level-api-fir = { module = "org.jetbrains.kotlin:low-level-api-fir-for-ide", version.ref = "kotlin-analysis-api" } kotlin-analysis-symbol-light-classes = { module = "org.jetbrains.kotlin:symbol-light-classes-for-ide", version.ref = "kotlin-analysis-api" } -kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } -kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } -kotlin-scripting-common = { module = "org.jetbrains.kotlin:kotlin-scripting-common", version.ref = "kotlin" } -kotlin-scripting-dependencies = { module = "org.jetbrains.kotlin:kotlin-scripting-dependencies", version.ref = "kotlin" } -kotlin-scripting-dependencies-maven = { module = "org.jetbrains.kotlin:kotlin-scripting-dependencies-maven", version.ref = "kotlin" } -kotlin-scripting-jvm = { module = "org.jetbrains.kotlin:kotlin-scripting-jvm", version.ref = "kotlin" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" } diff --git a/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java b/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java index 310dc2179..bf792bc7c 100644 --- a/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java +++ b/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java @@ -92,12 +92,9 @@ private void configureProject(Project project) { triggers.add("compileTestKotlin"); } - // The CLI's init script provides the path of the embedded scip-kotlinc jar. - Object scipKotlinc = requiredExtra(extraProperties, "scipKotlincJar"); project .getTasks() - .configureEach( - task -> configureKotlinCompileTask(task, scipKotlinc, sourceRoot, targetRoot)); + .configureEach(task -> configureKotlinCompileTask(task, sourceRoot, targetRoot)); } project.getTasks().create("scipCompileAll").dependsOn(triggers); @@ -153,39 +150,65 @@ private static boolean tryAddJavacPlugin(Project project, Object javacPluginDep) } } - private static void configureKotlinCompileTask( - Task task, Object scipKotlinc, String sourceRoot, String targetRoot) { + /** + * Kotlin sources are no longer indexed inside kotlinc: after each Kotlin compile task runs, its + * sources and classpath are dumped to {@code /kotlin-configs/.txt}, and the + * scip-java CLI indexes them with the standalone Analysis API indexer after the build. This + * removes any binary-compatibility coupling with the Kotlin compiler version of the build being + * indexed. + */ + private static void configureKotlinCompileTask(Task task, String sourceRoot, String targetRoot) { if (!task.getClass().getSimpleName().contains("KotlinCompile")) { return; } - // Referring to KotlinCompile directly here triggers NoClassDefFoundError - - // the plugin classpath is murky and we deliberately don't bundle the Kotlin - // Gradle plugin. So we commit the sins of reflection for our limited needs. - try { - Object kotlinOptions = task.getClass().getMethod("getKotlinOptions").invoke(task); - - @SuppressWarnings("unchecked") - List freeCompilerArgs = - (List) - kotlinOptions.getClass().getMethod("getFreeCompilerArgs").invoke(kotlinOptions); - - List newArgs = new ArrayList<>(freeCompilerArgs.size() + 5); - newArgs.addAll(freeCompilerArgs); - newArgs.add("-Xplugin=" + scipKotlinc); - newArgs.add("-P"); - newArgs.add("plugin:scip-kotlinc:sourceroot=" + sourceRoot); - newArgs.add("-P"); - newArgs.add("plugin:scip-kotlinc:targetroot=" + targetRoot); - - kotlinOptions - .getClass() - .getMethod("setFreeCompilerArgs", List.class) - .invoke(kotlinOptions, newArgs); - } catch (ReflectiveOperationException exc) { - throw new RuntimeException( - "scip-java: failed to configure Kotlin compile task '" + task.getName() + "'", exc); - } + task.doLast( + ignored -> { + try { + List lines = new ArrayList<>(); + lines.add("sourceroot " + sourceRoot); + + // Referring to KotlinCompile directly triggers NoClassDefFoundError - + // the plugin classpath is murky and we deliberately don't bundle the + // Kotlin Gradle plugin. So we commit the sins of reflection. + org.gradle.api.file.FileCollection libraries; + try { + libraries = + (org.gradle.api.file.FileCollection) + task.getClass().getMethod("getLibraries").invoke(task); + } catch (ReflectiveOperationException exc) { + libraries = + (org.gradle.api.file.FileCollection) + task.getClass().getMethod("getClasspath").invoke(task); + } + for (java.io.File entry : libraries.getFiles()) { + lines.add("classpath " + entry.getAbsolutePath()); + } + + int sources = 0; + for (java.io.File file : task.getInputs().getSourceFiles().getFiles()) { + if (file.getName().endsWith(".kt") || file.getName().endsWith(".kts")) { + lines.add("source " + file.getAbsolutePath()); + sources++; + } + } + if (sources == 0) { + return; + } + + java.nio.file.Path configDir = java.nio.file.Paths.get(targetRoot, "kotlin-configs"); + java.nio.file.Files.createDirectories(configDir); + String fileName = task.getPath().replaceAll("[^A-Za-z0-9]", "-") + ".txt"; + java.nio.file.Files.write(configDir.resolve(fileName), lines); + } catch (Exception exc) { + throw new RuntimeException( + "scip-java: failed to extract Kotlin compile configuration for task '" + + task.getName() + + "': " + + exc, + exc); + } + }); } private static Object requiredExtra(Map extraProperties, String name) { diff --git a/scip-java/build.gradle.kts b/scip-java/build.gradle.kts index ef86756b0..26111ee58 100644 --- a/scip-java/build.gradle.kts +++ b/scip-java/build.gradle.kts @@ -13,7 +13,6 @@ description = "Java and Kotlin indexer for SCIP" val javacShadowJar = shadowJarArtifact(":scip-javac", "javacShadowJar") val gradlePluginShadowJar = shadowJarArtifact(":scip-gradle-plugin", "gradlePluginShadowJar") -val kotlincShadowJar = shadowJarArtifact(":scip-kotlinc", "kotlincShadowJar") dependencies { implementation(project(":scip-aggregator")) @@ -42,9 +41,6 @@ val generateEmbeddedResources = tasks.register("generateEmbeddedResources" from(gradlePluginShadowJar) { rename { "gradle-plugin.jar" } } - from(kotlincShadowJar) { - rename { "scip-kotlinc.jar" } - } into(layout.buildDirectory.dir("generated/resources/embedded")) } diff --git a/scip-java/src/main/kotlin/org/scip_code/scip_java/Embedded.kt b/scip-java/src/main/kotlin/org/scip_code/scip_java/Embedded.kt index caf9b4f50..2d86b8074 100644 --- a/scip-java/src/main/kotlin/org/scip_code/scip_java/Embedded.kt +++ b/scip-java/src/main/kotlin/org/scip_code/scip_java/Embedded.kt @@ -25,8 +25,6 @@ object Embedded { fun gradlePluginJar(tmpDir: Path): Path = copyFile(tmpDir, "gradle-plugin.jar") - fun scipKotlincJar(tmpDir: Path): Path = copyFile(tmpDir, "scip-kotlinc.jar") - private fun javacErrorpath(tmp: Path): Path = tmp.resolve("errorpath.txt") data class CustomJavac(val executable: Path, val environment: Map) diff --git a/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/GradleBuildTool.kt b/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/GradleBuildTool.kt index 900287442..fee71085b 100644 --- a/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/GradleBuildTool.kt +++ b/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/GradleBuildTool.kt @@ -6,6 +6,7 @@ import java.nio.file.Path import java.nio.file.Paths import org.scip_code.scip_java.Embedded import org.scip_code.scip_java.commands.IndexCommand +import org.scip_code.scip_java.kotlin_analysis.KotlinAnalysisIndexer class GradleBuildTool(index: IndexCommand) : BuildTool("Gradle", index) { @@ -89,19 +90,64 @@ This means our SCIP compiler plugin was not attached to one or more JavaCompile cmd += "--no-daemon" cmd += "--init-script" cmd += script - cmd += "-Pkotlin.compiler.execution.strategy=in-process" cmd += "-Dscip.targetroot=${targetroot()}" cmd += index.finalBuildCommand(listOf("clean", "scipPrintDependencies", "scipCompileAll")) targetroot().toFile().deleteRecursively() val result = index.app.runProcess(cmd, env = mapOf("TERM" to "dumb")) + indexKotlinConfigs() return Embedded.reportUnexpectedJavacErrors(index.app.reporter, tmp) ?: result } + /** + * The Gradle plugin dumps each Kotlin compile task's sources and classpath under + * `/kotlin-configs/`; index them with the standalone Analysis API indexer. The + * Kotlin version of the indexed build is irrelevant here — the indexer bundles its own analysis + * engine. + */ + private fun indexKotlinConfigs() { + val configsDir = targetroot().resolve("kotlin-configs") + if (!Files.isDirectory(configsDir)) return + Files.list(configsDir).use { stream -> + for (config in stream.filter { it.fileName.toString().endsWith(".txt") }) { + try { + indexKotlinConfig(config) + } catch (e: Exception) { + index.app.reporter.error( + RuntimeException("failed to index Kotlin sources from $config", e) + ) + } + } + } + } + + private fun indexKotlinConfig(config: Path) { + var sourceroot: Path = index.workingDirectory + val classpath = mutableListOf() + val sources = mutableListOf() + for (line in Files.readAllLines(config)) { + val separator = line.indexOf(' ') + if (separator < 0) continue + val value = line.substring(separator + 1) + when (line.substring(0, separator)) { + "sourceroot" -> sourceroot = Paths.get(value) + "classpath" -> classpath.add(Paths.get(value)) + "source" -> sources.add(Paths.get(value)) + } + } + if (sources.isEmpty()) return + KotlinAnalysisIndexer( + sourceroot = sourceroot, + targetroot = targetroot(), + sourceRoots = sources, + classpath = classpath, + ) + .run() + } + private fun initScript(tmp: Path): Path { val pluginpath = Embedded.scipJar(tmp) val gradlePluginPath = Embedded.gradlePluginJar(tmp) - val scipKotlincPath = Embedded.scipKotlincJar(tmp) val dependenciesPath = targetroot().resolve("dependencies.txt") Files.deleteIfExists(dependenciesPath) @@ -119,7 +165,6 @@ This means our SCIP compiler plugin was not attached to one or more JavaCompile project.ext["scipTarget"] = "${targetroot()}" project.ext["javacPluginJar"] = "$pluginpath" project.ext["dependenciesOut"] = "$dependenciesPath" - project.ext["scipKotlincJar"] = "$scipKotlincPath" apply plugin: ScipGradlePlugin } """ diff --git a/scip-kotlinc/build.gradle.kts b/scip-kotlinc/build.gradle.kts deleted file mode 100644 index 699d24dd9..000000000 --- a/scip-kotlinc/build.gradle.kts +++ /dev/null @@ -1,37 +0,0 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - id("scip.java-library") - id("scip.kotlin-jvm") - id("scip.shadow-producer") - id("scip.maven-publish") -} - -description = "A kotlinc plugin to emit SCIP information" - -dependencies { - implementation(project(":scip-shared")) - implementation(libs.scip.kotlin.bindings) - compileOnly(libs.kotlin.stdlib) - compileOnly(libs.kotlin.compiler.embeddable) - - testImplementation(libs.kotlin.compiler.embeddable) - testImplementation(libs.kotlin.test) - testImplementation(libs.kotlin.test.junit5) - testImplementation(libs.kotlin.reflect) - testImplementation(libs.kotest.assertions.core) - testImplementation(libs.kctfork.core) -} - -tasks.withType().configureEach { - compilerOptions.freeCompilerArgs.add("-Xcontext-parameters") -} - -tasks.named("test") { - maxHeapSize = "2g" -} - -tasks.named("shadowJar") { - mergeServiceFiles() -} 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 deleted file mode 100644 index 37ece4b57..000000000 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt +++ /dev/null @@ -1,467 +0,0 @@ -package org.scip_code.scip_java.kotlinc - -import java.nio.file.Path -import org.jetbrains.kotlin.* -import org.jetbrains.kotlin.com.intellij.lang.LighterASTNode -import org.jetbrains.kotlin.com.intellij.util.diff.FlyweightCapableTreeStructure -import org.jetbrains.kotlin.diagnostics.* -import org.jetbrains.kotlin.fir.FirSession -import org.jetbrains.kotlin.fir.analysis.checkers.MppCheckerKind -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.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.* -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.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.lexer.KtTokens -import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.name.FqName - -open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtension(session) { - companion object { - val visitors: MutableMap = mutableMapOf() - - private fun getIdentifier(element: KtSourceElement): KtSourceElement = - element.treeStructure - .findChildByType(element.lighterASTNode, KtTokens.IDENTIFIER) - ?.toKtLightSourceElement(element.treeStructure) ?: element - } - - override val declarationCheckers: DeclarationCheckers - get() = AnalyzerDeclarationCheckers(session.analyzerParamsProvider.sourceroot) - - override val expressionCheckers: ExpressionCheckers - get() = - object : ExpressionCheckers() { - override val qualifiedAccessExpressionCheckers: - Set = - setOf(SemanticQualifiedAccessExpressionChecker()) - - override val typeOperatorCallCheckers: - Set = - setOf(SemanticClassReferenceExpressionChecker()) - } - - open class AnalyzerDeclarationCheckers(sourceroot: Path) : DeclarationCheckers() { - override val fileCheckers: Set = - setOf(SemanticFileChecker(sourceroot), SemanticImportsChecker()) - override val classLikeCheckers: Set = setOf(SemanticClassLikeChecker()) - override val constructorCheckers: Set = - setOf(SemanticConstructorChecker()) - override val simpleFunctionCheckers: Set = - setOf(SemanticSimpleFunctionChecker()) - override val anonymousFunctionCheckers: Set = - setOf(SemanticAnonymousFunctionChecker()) - override val propertyCheckers: Set = setOf(SemanticPropertyChecker()) - override val valueParameterCheckers: Set = - setOf(SemanticValueParameterChecker()) - override val typeParameterCheckers: Set = - setOf(SemanticTypeParameterChecker()) - override val typeAliasCheckers: Set = setOf(SemanticTypeAliasChecker()) - override val propertyAccessorCheckers: Set = - setOf(SemanticPropertyAccessorChecker()) - } - - private class SemanticFileChecker(private val sourceroot: Path) : - FirFileChecker(MppCheckerKind.Common) { - companion object { - val globals = GlobalSymbolsCache() - } - - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirFile) { - val ktFile = declaration.sourceFile ?: return - val lineMap = LineMap(declaration) - val visitor = ScipVisitor(sourceroot, ktFile, lineMap, globals) - visitors[ktFile] = visitor - } - } - - class SemanticImportsChecker : FirFileChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirFile) { - val ktFile = declaration.sourceFile ?: return - val visitor = visitors[ktFile] - - val eachFqNameElement = - { - fqName: FqName, - tree: FlyweightCapableTreeStructure, - names: LighterASTNode, - callback: (FqName, KtLightSourceElement) -> Unit -> - val nameList = - if (names.tokenType == KtNodeTypes.REFERENCE_EXPRESSION) listOf(names) - else tree.collectDescendantsOfType(names, KtNodeTypes.REFERENCE_EXPRESSION) - - var ancestor = fqName - var depth = 0 - - while (ancestor != FqName.ROOT) { - val nameNode = nameList[nameList.lastIndex - depth] - val nameSource = nameNode.toKtLightSourceElement(tree) - - callback(ancestor, nameSource) - - ancestor = ancestor.parent() - depth++ - } - } - - val packageDirective = declaration.packageDirective - val fqName = packageDirective.packageFqName - val source = packageDirective.source - if (source != null) { - val names = - source.treeStructure.findChildByType( - source.lighterASTNode, - KtNodeTypes.DOT_QUALIFIED_EXPRESSION, - ) - ?: source.treeStructure.findChildByType( - source.lighterASTNode, - KtNodeTypes.REFERENCE_EXPRESSION, - ) - - if (names != null) { - eachFqNameElement(fqName, source.treeStructure, names) { fqName, name -> - visitor?.visitPackage(fqName, name, context) - } - } - } - - declaration.imports.forEach { import -> - val source = import.source ?: return@forEach - val fqName = import.importedFqName ?: return@forEach - - val names = - source.treeStructure.findDescendantByType( - source.lighterASTNode, - KtNodeTypes.DOT_QUALIFIED_EXPRESSION, - ) - if (names != null) { - eachFqNameElement(fqName, source.treeStructure, names) { fqName, name -> - val symbolProvider = context.session.symbolProvider - - val klass = - symbolProvider.getClassLikeSymbolByClassId(ClassId.topLevel(fqName)) - val callables = - symbolProvider.getTopLevelCallableSymbols( - fqName.parent(), - fqName.shortName(), - ) - - if (klass != null) { - visitor?.visitClassReference(klass, name, context) - } else if (callables.isNotEmpty()) { - for (callable in callables) { - visitor?.visitCallableReference(callable, name, context) - } - } else { - visitor?.visitPackage(fqName, name, context) - } - } - } - } - } - } - - private class SemanticClassLikeChecker : FirClassLikeChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirClassLikeDeclaration) { - val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return - val visitor = visitors[ktFile] - val objectKeyword = - if (declaration is FirAnonymousObject) { - source.treeStructure - .findChildByType(source.lighterASTNode, KtTokens.OBJECT_KEYWORD) - ?.toKtLightSourceElement(source.treeStructure) - } else { - null - } - visitor?.visitClassOrObject( - declaration, - objectKeyword ?: getIdentifier(source), - context, - 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, context) - } - } - } - } - } - - private class SemanticConstructorChecker : FirConstructorChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirConstructor) { - val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return - val visitor = visitors[ktFile] - - if (declaration.isPrimary) { - // if the constructor is not denoted by the 'constructor' keyword, we want to link - // it to the - // class identifier - val klass = declaration.symbol.getContainingClassSymbol() - val klassSource = klass?.source ?: source - val constructorKeyboard = - source.treeStructure - .findChildByType(source.lighterASTNode, KtTokens.CONSTRUCTOR_KEYWORD) - ?.toKtLightSourceElement(source.treeStructure) - - val objectKeyword = - if (klass is FirAnonymousObjectSymbol) { - source.treeStructure - .findChildByType(source.lighterASTNode, KtTokens.OBJECT_KEYWORD) - ?.toKtLightSourceElement(source.treeStructure) - } else { - null - } - - visitor?.visitPrimaryConstructor( - declaration, - constructorKeyboard ?: objectKeyword ?: getIdentifier(klassSource), - context, - enclosingSource = source, - ) - } else { - visitor?.visitSecondaryConstructor( - declaration, - getIdentifier(source), - context, - enclosingSource = source, - ) - } - } - } - - private class SemanticSimpleFunctionChecker : FirSimpleFunctionChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirSimpleFunction) { - val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return - val visitor = visitors[ktFile] - visitor?.visitNamedFunction( - declaration, - getIdentifier(source), - context, - 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), context) - } - } - } - - private class SemanticAnonymousFunctionChecker : - FirAnonymousFunctionChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirAnonymousFunction) { - val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return - val visitor = visitors[ktFile] - visitor?.visitNamedFunction(declaration, source, context, enclosingSource = source) - } - } - - private class SemanticPropertyChecker : FirPropertyChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirProperty) { - val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return - val visitor = visitors[ktFile] - visitor?.visitProperty( - declaration, - getIdentifier(source), - context, - 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), context) - } - } - } - - private class SemanticValueParameterChecker : FirValueParameterChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirValueParameter) { - val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return - val visitor = visitors[ktFile] - visitor?.visitParameter( - declaration, - getIdentifier(source), - context, - 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), context) - } - } - } - - private class SemanticTypeParameterChecker : FirTypeParameterChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirTypeParameter) { - val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return - val visitor = visitors[ktFile] - visitor?.visitTypeParameter( - declaration, - getIdentifier(source), - context, - enclosingSource = source, - ) - } - } - - private class SemanticTypeAliasChecker : FirTypeAliasChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirTypeAlias) { - val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return - val visitor = visitors[ktFile] - visitor?.visitTypeAlias( - declaration, - getIdentifier(source), - context, - enclosingSource = source, - ) - } - } - - private class SemanticPropertyAccessorChecker : - FirPropertyAccessorChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirPropertyAccessor) { - val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return - val visitor = visitors[ktFile] - val identifierSource = - if (declaration.isGetter) { - source.treeStructure - .findChildByType(source.lighterASTNode, KtTokens.GET_KEYWORD) - ?.toKtLightSourceElement(source.treeStructure) ?: getIdentifier(source) - } else if (declaration.isSetter) { - source.treeStructure - .findChildByType(source.lighterASTNode, KtTokens.SET_KEYWORD) - ?.toKtLightSourceElement(source.treeStructure) ?: getIdentifier(source) - } else { - getIdentifier(source) - } - - visitor?.visitPropertyAccessor( - declaration, - identifierSource, - context, - enclosingSource = source, - ) - } - } - - private class SemanticQualifiedAccessExpressionChecker : - FirQualifiedAccessExpressionChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(expression: FirQualifiedAccessExpression) { - val source = expression.source ?: return - val calleeReference = expression.calleeReference - if ((calleeReference as? FirResolvedNamedReference) == null) { - return - } - - val ktFile = context.containingFile?.sourceFile ?: return - val visitor = visitors[ktFile] - visitor?.visitSimpleNameExpression( - calleeReference, - getIdentifier(calleeReference.source ?: source), - context, - ) - - val resolvedSymbol = calleeReference.resolvedSymbol - if ( - resolvedSymbol.origin == FirDeclarationOrigin.SamConstructor && - resolvedSymbol is FirSyntheticFunctionSymbol - ) { - val referencedKlass = - resolvedSymbol.resolvedReturnType.toClassLikeSymbol(context.session) - if (referencedKlass != null) { - visitor?.visitClassReference( - referencedKlass, - getIdentifier(calleeReference.source ?: source), - context, - ) - } - } - - // When encountering a reference to a property symbol, emit both getter and setter - // symbols - if (resolvedSymbol is FirPropertySymbol) { - resolvedSymbol.getterSymbol?.let { - visitor?.visitCallableReference( - it, - getIdentifier(calleeReference.source ?: source), - context, - ) - } - resolvedSymbol.setterSymbol?.let { - visitor?.visitCallableReference( - it, - getIdentifier(calleeReference.source ?: source), - context, - ) - } - } - } - } - - private class SemanticClassReferenceExpressionChecker : - FirTypeOperatorCallChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(expression: FirTypeOperatorCall) { - val typeRef = expression.conversionTypeRef - val source = typeRef.source ?: return - val classSymbol = - expression.conversionTypeRef.toClassLikeSymbol(context.session) ?: return - val ktFile = context.containingFile?.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/AnalyzerCommandLineProcessor.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCommandLineProcessor.kt deleted file mode 100644 index 45bb987b2..000000000 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCommandLineProcessor.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.scip_code.scip_java.kotlinc - -import java.nio.file.Path -import java.nio.file.Paths -import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption -import org.jetbrains.kotlin.compiler.plugin.CliOption -import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor -import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi -import org.jetbrains.kotlin.config.CompilerConfiguration -import org.jetbrains.kotlin.config.CompilerConfigurationKey - -const val VAL_SOURCES = "sourceroot" -val KEY_SOURCES = CompilerConfigurationKey(VAL_SOURCES) - -const val VAL_TARGET = "targetroot" -val KEY_TARGET = CompilerConfigurationKey(VAL_TARGET) - -@OptIn(ExperimentalCompilerApi::class) -class AnalyzerCommandLineProcessor : CommandLineProcessor { - override val pluginId: String = "scip-kotlinc" - override val pluginOptions: Collection = - listOf( - CliOption( - VAL_SOURCES, - "", - "the absolute path to the root of the Kotlin sources", - required = true, - ), - CliOption( - VAL_TARGET, - "", - "the absolute path to the directory where to generate SCIP files.", - required = true, - ), - ) - - override fun processOption( - option: AbstractCliOption, - value: String, - configuration: CompilerConfiguration, - ) { - when (option.optionName) { - VAL_SOURCES -> configuration.put(KEY_SOURCES, Paths.get(value)) - VAL_TARGET -> configuration.put(KEY_TARGET, Paths.get(value)) - } - } -} diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerFirExtensionRegistrar.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerFirExtensionRegistrar.kt deleted file mode 100644 index e0d6271de..000000000 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerFirExtensionRegistrar.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.scip_code.scip_java.kotlinc - -import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar -import org.scip_code.scip_java.shared.ScipOptions - -class AnalyzerFirExtensionRegistrar(private val options: ScipOptions) : FirExtensionRegistrar() { - override fun ExtensionRegistrarContext.configurePlugin() { - +AnalyzerParamsProvider.getFactory(options) - +::AnalyzerCheckers - } -} diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerParamsProvider.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerParamsProvider.kt deleted file mode 100644 index 38f64ce2a..000000000 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerParamsProvider.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.scip_code.scip_java.kotlinc - -import java.nio.file.Path -import org.jetbrains.kotlin.fir.FirSession -import org.jetbrains.kotlin.fir.extensions.FirExtensionSessionComponent -import org.jetbrains.kotlin.fir.extensions.FirExtensionSessionComponent.Factory -import org.scip_code.scip_java.shared.ScipOptions - -open class AnalyzerParamsProvider(session: FirSession, val options: ScipOptions) : - FirExtensionSessionComponent(session) { - val sourceroot: Path - get() = options.sourceroot - - companion object { - fun getFactory(options: ScipOptions): Factory { - return Factory { AnalyzerParamsProvider(it, options) } - } - } -} - -val FirSession.analyzerParamsProvider: AnalyzerParamsProvider by - FirSession.sessionComponentAccessor() 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 deleted file mode 100644 index 289a0cf4e..000000000 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.scip_code.scip_java.kotlinc - -import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension -import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar -import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi -import org.jetbrains.kotlin.config.CompilerConfiguration -import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrarAdapter -import org.scip_code.scip.Document -import org.scip_code.scip_java.shared.ScipOptions - -@OptIn(ExperimentalCompilerApi::class) -class AnalyzerRegistrar(private val callback: (Document) -> Unit = {}) : CompilerPluginRegistrar() { - override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { - val options = - ScipOptions().apply { - sourceroot = configuration[KEY_SOURCES]!! - targetroot = configuration[KEY_TARGET]!! - } - FirExtensionRegistrarAdapter.registerExtension(AnalyzerFirExtensionRegistrar(options)) - IrGenerationExtension.registerExtension( - PostAnalysisExtension( - sourceRoot = options.sourceroot, - targetRoot = options.targetroot, - callback = callback, - ) - ) - } - - override val supportsK2: Boolean - get() = true -} 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 deleted file mode 100644 index c2da05fdf..000000000 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/LineMap.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.scip_code.scip_java.kotlinc - -import org.jetbrains.kotlin.KtSourceElement -import org.jetbrains.kotlin.com.intellij.navigation.NavigationItem -import org.jetbrains.kotlin.fir.declarations.FirFile -import org.jetbrains.kotlin.text - -/** Maps between an element and its identifier positions */ -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 */ - fun lineNumberForOffset(offset: Int): Int = - file.sourceFileLinesMapping?.getLineByOffset(offset)?.let { it + 1 } ?: 0 - - /** Returns the non-0-based column number for a given offset */ - fun columnForOffset(offset: Int): Int = offsetToLineAndCol(offset)?.second ?: 0 - - /** Returns the non-0-based start character */ - fun startCharacter(element: KtSourceElement): Int = - offsetToLineAndCol(element.startOffset)?.second ?: 0 - - /** Returns the non-0-based end character */ - fun endCharacter(element: KtSourceElement): Int = - startCharacter(element) + nameForOffset(element).length - - /** Returns the non-0-based line number */ - fun lineNumber(element: KtSourceElement): Int = - file.sourceFileLinesMapping?.getLineByOffset(element.startOffset)?.let { it + 1 } ?: 0 - - companion object { - fun nameForOffset(element: KtSourceElement): String = - when (element) { - is NavigationItem -> element.name ?: element.text?.toString() ?: "" - else -> element.text?.toString() ?: "" - } - } -} 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 deleted file mode 100644 index 54ea38fa6..000000000 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt +++ /dev/null @@ -1,93 +0,0 @@ -package org.scip_code.scip_java.kotlinc - -import java.io.PrintWriter -import java.io.Writer -import java.nio.file.Path -import java.nio.file.Paths -import org.jetbrains.kotlin.KtSourceFile -import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity -import org.jetbrains.kotlin.cli.common.messages.MessageRenderer -import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector -import org.jetbrains.kotlin.config.CommonConfigurationKeys -import org.jetbrains.kotlin.config.CompilerConfiguration -import org.jetbrains.kotlin.ir.declarations.IrModuleFragment -import org.scip_code.scip.Document -import org.scip_code.scip_java.shared.ScipShardPaths -import org.scip_code.scip_java.shared.ScipShardWriter - -/** - * Writes per-source SCIP shards once the FIR checkers have finished and the IR phase begins. - * - *

For each source file [AnalyzerCheckers] registered a [ScipVisitor] for, this builds the file's - * [Document] and serializes it under `/META-INF/scip/.scip`. Files - * outside the source root are skipped with a stderr warning. - */ -class PostAnalysisExtension( - private val sourceRoot: Path, - private val targetRoot: Path, - private val callback: (Document) -> Unit, -) : IrGenerationExtension { - override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { - try { - for ((ktSourceFile, visitor) in AnalyzerCheckers.visitors) { - try { - val document = visitor.build() - scipShardPathForFile(ktSourceFile)?.let { outPath -> - ScipShardWriter.writeShard(outPath, document) - } - callback(document) - } catch (e: Exception) { - handleException(e) - } - } - } catch (e: Exception) { - handleException(e) - } - } - - private fun scipShardPathForFile(file: KtSourceFile): Path? { - val normalizedPath = Paths.get(file.path).normalize() - val outPath = ScipShardPaths.shardPath(targetRoot, sourceRoot, normalizedPath) - if (outPath.isPresent) { - return outPath.get() - } - System.err.println( - "given file is not under the sourceroot.\n\tSourceroot: $sourceRoot\n\tFile path: ${file.path}\n\tNormalized file path: $normalizedPath" - ) - return null - } - - private val messageCollector = - CompilerConfiguration() - .get( - CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, - PrintingMessageCollector(System.err, MessageRenderer.PLAIN_FULL_PATHS, false), - ) - - private fun handleException(e: Exception) { - val writer = - PrintWriter( - object : Writer() { - val buf = StringBuffer() - - override fun close() = - messageCollector.report(CompilerMessageSeverity.EXCEPTION, buf.toString()) - - override fun flush() = Unit - - override fun write(data: CharArray, offset: Int, len: Int) { - buf.append(data, offset, len) - } - }, - false, - ) - writer.println("Exception in scip-kotlin compiler plugin:") - e.printStackTrace(writer) - writer.println( - "Please report a bug to https://github.com/scip-code/scip-java with the stack trace above." - ) - writer.close() - } -} diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipSymbols.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipSymbols.kt deleted file mode 100644 index b3ffb4520..000000000 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipSymbols.kt +++ /dev/null @@ -1,70 +0,0 @@ -package org.scip_code.scip_java.kotlinc - -import org.scip_code.scip_java.shared.ScipSymbols as SharedSymbols - -@JvmInline -value class Symbol(private val symbol: String) { - companion object { - val NONE = Symbol(SharedSymbols.NONE) - val ROOT_PACKAGE = Symbol(SharedSymbols.ROOT_PACKAGE) - - // Note: this intentionally diverges from `SharedSymbols.global` when - // `desc == NONE` — Java returns `NONE`, Kotlin returns the owner. - // SymbolsCache relies on this behavior; do not delegate without first - // updating those call sites. - fun createGlobal(owner: Symbol, desc: ScipSymbolDescriptor): Symbol = - when { - desc == ScipSymbolDescriptor.NONE -> owner - owner != ROOT_PACKAGE -> Symbol(owner.symbol + desc.encode().symbol) - else -> desc.encode() - } - - fun createLocal(i: Int) = Symbol(SharedSymbols.local(i)) - } - - fun isGlobal() = SharedSymbols.isGlobal(symbol) - - fun isLocal() = SharedSymbols.isLocal(symbol) - - override fun toString(): String = symbol -} - -fun String.symbol(): Symbol = Symbol(this) - -data class ScipSymbolDescriptor( - val kind: Kind, - val name: String, - // Default differs from `SharedSymbols.Descriptor` (which is "") because - // Kotlin call sites — getters/setters in particular — rely on the no-arg - // overload producing `name().` rather than `name.` for METHOD kinds. - val disambiguator: String = "()", -) { - companion object { - val NONE = ScipSymbolDescriptor(Kind.NONE, "") - } - - enum class Kind { - NONE, - TERM, - METHOD, - TYPE, - PACKAGE, - PARAMETER, - TYPE_PARAMETER; - - internal fun toSharedKind(): SharedSymbols.Descriptor.Kind = - when (this) { - NONE -> SharedSymbols.Descriptor.Kind.None - TERM -> SharedSymbols.Descriptor.Kind.Term - METHOD -> SharedSymbols.Descriptor.Kind.Method - TYPE -> SharedSymbols.Descriptor.Kind.Type - PACKAGE -> SharedSymbols.Descriptor.Kind.Package - PARAMETER -> SharedSymbols.Descriptor.Kind.Parameter - TYPE_PARAMETER -> SharedSymbols.Descriptor.Kind.TypeParameter - } - } - - fun encode(): Symbol = - if (kind == Kind.NONE) Symbol(SharedSymbols.NONE) - else Symbol(SharedSymbols.Descriptor(kind.toSharedKind(), name, disambiguator).encode()) -} 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 deleted file mode 100644 index f767dda2b..000000000 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt +++ /dev/null @@ -1,198 +0,0 @@ -package org.scip_code.scip_java.kotlinc - -import java.nio.file.Path -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.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.renderer.* -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.fir.types.impl.FirImplicitAnyTypeRef -import org.jetbrains.kotlin.lexer.KtTokens -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.SymbolRole -import org.scip_code.scip.relationship -import org.scip_code.scip.signature -import org.scip_code.scip.symbolInformation -import org.scip_code.scip_java.shared.ScipDocumentBuilder -import org.scip_code.scip_java.shared.ScipRange -import org.scip_code.scip_java.shared.ScipShardPaths - -/** Builds a SCIP [Document] for a single Kotlin source file. */ -class ScipTextDocumentBuilder( - private val sourceroot: Path, - private val file: KtSourceFile, - private val lineMap: LineMap, - private val cache: SymbolsCache, -) { - private val documentBuilder = ScipDocumentBuilder() - private val fileText = file.getContentsAsStream().reader().readText() - - fun build(): Document = documentBuilder.build("kotlin", relativePath(), fileText) - - 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)) - } - } - - @OptIn(SymbolInternals::class) - private fun symbolInformation( - firBasedSymbol: FirBasedSymbol<*>?, - symbol: Symbol, - element: KtSourceElement, - context: CheckerContext, - ): SymbolInformation { - val supers = - when (firBasedSymbol) { - is FirClassSymbol -> - firBasedSymbol.resolvedSuperTypeRefs - .filter { it !is FirImplicitAnyTypeRef } - .mapNotNull { it.toClassLikeSymbol(firBasedSymbol.moduleData.session) } - .flatMap { cache[it] } - is FirFunctionSymbol<*> -> - firBasedSymbol.directOverriddenSymbolsSafe(context).flatMap { cache[it] } - else -> emptyList() - } - return symbolInformation { - this.symbol = symbol.toString() - this.displayName = - if (firBasedSymbol != null) displayName(firBasedSymbol) else element.text.toString() - if (firBasedSymbol != null) { - renderSignature(firBasedSymbol.fir)?.let { rendered -> - signatureDocumentation = signature { - language = "kotlin" - text = rendered - } - } - docComment(firBasedSymbol.fir)?.let { documentation += it } - } - for (parent in supers) { - relationships += relationship { - this.symbol = parent.toString() - isImplementation = true - } - } - } - } - - private fun occurrence( - symbol: Symbol, - element: KtSourceElement, - isDefinition: Boolean, - enclosingSource: KtSourceElement?, - ): Occurrence { - val builder = Occurrence.newBuilder().setSymbol(symbol.toString()) - if (isDefinition) builder.setSymbolRoles(SymbolRole.Definition.number) - val range = range(element) - if (range.isSingleLine) builder.singleLineRange = range.toSingleLineRange() - else builder.multiLineRange = range.toMultiLineRange() - if (enclosingSource != null) { - val enclosingRange = enclosingRange(enclosingSource) - if (enclosingRange.isSingleLine) { - builder.singleLineEnclosingRange = enclosingRange.toSingleLineRange() - } else { - builder.multiLineEnclosingRange = enclosingRange.toMultiLineRange() - } - } - return builder.build() - } - - private fun range(element: KtSourceElement): ScipRange { - val line = lineMap.lineNumber(element) - 1 - val startCol = lineMap.startCharacter(element) - val endCol = lineMap.endCharacter(element) - return ScipRange.singleLine(line, startCol, endCol) - } - - private fun enclosingRange(element: KtSourceElement): ScipRange { - val startLine = lineMap.lineNumber(element) - 1 - val startCol = lineMap.startCharacter(element) - val endLine = lineMap.lineNumberForOffset(element.endOffset) - 1 - val endCol = lineMap.columnForOffset(element.endOffset) - return ScipRange.range(startLine, startCol, endLine, endCol) - } - - private fun relativePath(): String = - ScipShardPaths.relativePath(sourceroot, Paths.get(file.path)) - - /** - * Renders [element] as a Kotlin signature using [FirRenderer]'s readability preset, with kdoc - * stripped (kdoc is exposed separately via [SymbolInformation.documentation]). - */ - private fun renderSignature(element: FirElement): String? { - val renderer = - FirRenderer( - typeRenderer = ConeTypeRenderer(), - idRenderer = ConeIdShortRenderer(), - classMemberRenderer = FirNoClassMemberRenderer(), - bodyRenderer = null, - propertyAccessorRenderer = null, - callArgumentsRenderer = FirCallNoArgumentsRenderer(), - modifierRenderer = FirAllModifierRenderer(), - callableSignatureRenderer = FirCallableSignatureRendererForReadability(), - declarationRenderer = FirDeclarationRenderer("local "), - ) - val rendered = renderer.renderElementAsString(element) - return if (rendered.isEmpty()) null else rendered - } - - private fun docComment(element: FirElement): String? { - val kdoc = element.source?.getChild(KtTokens.DOC_COMMENT)?.text?.toString() ?: return null - return stripKdoc(kdoc).ifEmpty { null } - } - - /** 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 - val out = StringBuilder() - var first = true - kdoc.lineSequence().forEach { line -> - if (line.isEmpty()) return@forEach - var start = 0 - while (start < line.length && line[start].isWhitespace()) start++ - if (start < line.length && line[start] == '/') start++ - while (start < line.length && line[start] == '*') start++ - var end = line.length - 1 - if (end > start && line[end] == '/') end-- - while (end > start && line[end] == '*') end-- - while (end > start && line[end].isWhitespace()) end-- - start = minOf(start, line.length - 1) - if (end > start) end++ - if (!first) out.append('\n') - out.append(line, start, end) - first = false - } - return out.toString().trim() - } - - companion object { - @OptIn(SymbolInternals::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 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 deleted file mode 100644 index 80b9866e5..000000000 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt +++ /dev/null @@ -1,186 +0,0 @@ -package org.scip_code.scip_java.kotlinc - -import java.nio.file.Path -import org.jetbrains.kotlin.KtSourceElement -import org.jetbrains.kotlin.KtSourceFile -import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext -import org.jetbrains.kotlin.fir.declarations.* -import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference -import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol -import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol -import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol -import org.jetbrains.kotlin.name.FqName -import org.scip_code.scip.Document - -/** - * Per-file accumulator of SCIP occurrences and symbols. The FIR checkers in [AnalyzerCheckers] call - * into this and the resulting [Document] is written as a `.scip` shard at the end of compilation. - */ -class ScipVisitor( - sourceroot: Path, - file: KtSourceFile, - lineMap: LineMap, - globals: GlobalSymbolsCache, - locals: LocalSymbolsCache = LocalSymbolsCache(), -) { - private val cache = SymbolsCache(globals, locals) - private val documentBuilder = ScipTextDocumentBuilder(sourceroot, file, lineMap, cache) - - private data class SymbolDescriptorPair( - val firBasedSymbol: FirBasedSymbol<*>?, - val symbol: Symbol, - ) - - fun build(): Document = documentBuilder.build() - - private fun Sequence?.emitAll( - element: KtSourceElement, - isDefinition: Boolean, - context: CheckerContext, - enclosingSource: KtSourceElement? = null, - ): List? = - this?.onEach { (firBasedSymbol, symbol) -> - documentBuilder.emitScipData( - firBasedSymbol, - symbol, - element, - isDefinition, - context, - enclosingSource, - ) - } - ?.map { it.symbol } - ?.toList() - - 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) - } - - fun visitClassReference( - firClassSymbol: FirClassLikeSymbol<*>, - element: KtSourceElement, - context: CheckerContext, - ) { - cache[firClassSymbol].with(firClassSymbol).emitAll(element, isDefinition = false, context) - } - - fun visitCallableReference( - firClassSymbol: FirCallableSymbol<*>, - element: KtSourceElement, - context: CheckerContext, - ) { - cache[firClassSymbol].with(firClassSymbol).emitAll(element, isDefinition = false, context) - } - - fun visitClassOrObject( - firClass: FirClassLikeDeclaration, - element: KtSourceElement, - context: CheckerContext, - enclosingSource: KtSourceElement? = null, - ) { - cache[firClass.symbol] - .with(firClass.symbol) - .emitAll(element, isDefinition = true, context, enclosingSource) - } - - fun visitPrimaryConstructor( - firConstructor: FirConstructor, - source: KtSourceElement, - context: CheckerContext, - enclosingSource: KtSourceElement? = null, - ) { - cache[firConstructor.symbol] - .with(firConstructor.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) - } - - fun visitSecondaryConstructor( - firConstructor: FirConstructor, - source: KtSourceElement, - context: CheckerContext, - enclosingSource: KtSourceElement? = null, - ) { - cache[firConstructor.symbol] - .with(firConstructor.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) - } - - fun visitNamedFunction( - firFunction: FirFunction, - source: KtSourceElement, - context: CheckerContext, - enclosingSource: KtSourceElement? = null, - ) { - cache[firFunction.symbol] - .with(firFunction.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) - } - - fun visitProperty( - firProperty: FirProperty, - source: KtSourceElement, - context: CheckerContext, - enclosingSource: KtSourceElement? = null, - ) { - cache[firProperty.symbol] - .with(firProperty.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) - } - - fun visitParameter( - firParameter: FirValueParameter, - source: KtSourceElement, - context: CheckerContext, - enclosingSource: KtSourceElement? = null, - ) { - cache[firParameter.symbol] - .with(firParameter.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) - } - - fun visitTypeParameter( - firTypeParameter: FirTypeParameter, - source: KtSourceElement, - context: CheckerContext, - enclosingSource: KtSourceElement? = null, - ) { - cache[firTypeParameter.symbol] - .with(firTypeParameter.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) - } - - fun visitTypeAlias( - firTypeAlias: FirTypeAlias, - source: KtSourceElement, - context: CheckerContext, - enclosingSource: KtSourceElement? = null, - ) { - cache[firTypeAlias.symbol] - .with(firTypeAlias.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) - } - - fun visitPropertyAccessor( - firPropertyAccessor: FirPropertyAccessor, - source: KtSourceElement, - context: CheckerContext, - enclosingSource: KtSourceElement? = null, - ) { - cache[firPropertyAccessor.symbol] - .with(firPropertyAccessor.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) - } - - fun visitSimpleNameExpression( - firResolvedNamedReference: FirResolvedNamedReference, - source: KtSourceElement, - context: CheckerContext, - ) { - cache[firResolvedNamedReference.resolvedSymbol] - .with(firResolvedNamedReference.resolvedSymbol) - .emitAll(source, isDefinition = false, context) - } -} 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 deleted file mode 100644 index 57f5b1b69..000000000 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt +++ /dev/null @@ -1,223 +0,0 @@ -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.providers.symbolProvider -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 - -class GlobalSymbolsCache(testing: Boolean = false) : Iterable { - private val globals = - if (testing) LinkedHashMap, Symbol>() - else HashMap, Symbol>() - private val packages = - if (testing) LinkedHashMap() else HashMap() - - operator fun get(symbol: FirBasedSymbol<*>, locals: LocalSymbolsCache): Sequence = - sequence { - emitSymbols(symbol, locals) - } - - operator fun get(symbol: FqName): Sequence = sequence { emitSymbols(symbol) } - - /** - * called whenever a new symbol should be yielded in the sequence e.g. for properties we also - * want to yield for every implicit getter/setter, but wouldn't want to yield for e.g. the - * package symbol parts that a class symbol is composed of. - */ - @OptIn(SymbolInternals::class) - private suspend fun SequenceScope.emitSymbols( - symbol: FirBasedSymbol<*>, - locals: LocalSymbolsCache, - ) { - yield(getSymbol(symbol, locals)) - if (symbol is FirPropertySymbol) { - if (symbol.fir.getter?.origin is FirDeclarationOrigin.Synthetic) - emitSymbols(symbol.fir.getter!!.symbol, locals) - if (symbol.fir.setter?.origin is FirDeclarationOrigin.Synthetic) - emitSymbols(symbol.fir.setter!!.symbol, locals) - } - } - - private suspend fun SequenceScope.emitSymbols(symbol: FqName) { - yield(getSymbol(symbol)) - } - - /** - * Entrypoint for building or looking-up a symbol without yielding a value in the sequence. - * Called recursively for every part of a symbol, unless a cached result short circuits. - */ - private fun getSymbol(symbol: FirBasedSymbol<*>, locals: LocalSymbolsCache): Symbol { - globals[symbol]?.let { - return it - } - locals[symbol]?.let { - return it - } - return uncachedSymbol(symbol, locals).also { if (it.isGlobal()) globals[symbol] = it } - } - - private fun getSymbol(symbol: FqName): Symbol { - packages[symbol]?.let { - return it - } - return uncachedSymbol(symbol).also { if (it.isGlobal()) packages[symbol] = it } - } - - @OptIn(SymbolInternals::class) - private fun uncachedSymbol(symbol: FirBasedSymbol<*>?, locals: LocalSymbolsCache): Symbol { - if (symbol == null || symbol is FirAnonymousFunctionSymbol) return Symbol.NONE - - if (symbol.fir.isLocalMember) return locals + symbol - - val owner = getParentSymbol(symbol, locals) - - if (owner.isLocal() || owner == Symbol.NONE) return locals + symbol - - val scipDescriptor = scipDescriptor(symbol) - - return Symbol.createGlobal(owner, scipDescriptor) - } - - private fun uncachedSymbol(symbol: FqName): Symbol { - if (symbol.isRoot) return Symbol.ROOT_PACKAGE - - val owner = this.getSymbol(symbol.parent()) - return Symbol.createGlobal( - owner, - ScipSymbolDescriptor(Kind.PACKAGE, symbol.shortName().asString()), - ) - } - - /** - * Returns the parent DeclarationDescriptor for a given DeclarationDescriptor. For most - * descriptor types, this simply returns the 'containing' descriptor. For Module- or - * PackageFragmentDescriptors, it returns the descriptor for the parent fqName of the current - * descriptors fqName e.g. for the fqName `test.sample.main`, the parent fqName would be - * `test.sample`. - */ - @OptIn(SymbolInternals::class) - private fun getParentSymbol(symbol: FirBasedSymbol<*>, locals: LocalSymbolsCache): Symbol { - when (symbol) { - is FirTypeParameterSymbol -> - return getSymbol(symbol.containingDeclarationSymbol, locals) - is FirValueParameterSymbol -> - return getSymbol(symbol.containingDeclarationSymbol, locals) - is FirCallableSymbol -> { - val session = symbol.fir.moduleData.session - return symbol.getContainingSymbol(session)?.let { getSymbol(it, locals) } - ?: getSymbol(symbol.packageFqName()) - } - is FirClassLikeSymbol -> { - val session = symbol.fir.moduleData.session - return symbol.getContainingDeclaration(session)?.let { getSymbol(it, locals) } - ?: getSymbol(symbol.packageFqName()) - } - is FirFileSymbol -> { - return getSymbol(symbol.fir.packageFqName) - } - else -> return Symbol.NONE - } - } - - @OptIn(SymbolInternals::class) - private fun scipDescriptor(symbol: FirBasedSymbol<*>): ScipSymbolDescriptor { - return when { - symbol is FirAnonymousObjectSymbol -> - symbol.source?.let { source -> - ScipSymbolDescriptor(Kind.TYPE, "") - } ?: ScipSymbolDescriptor.NONE - 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(), - ) - symbol is FirPropertyAccessorSymbol && symbol.isGetter -> - ScipSymbolDescriptor( - Kind.METHOD, - "get" + symbol.propertySymbol.fir.name.toString().capitalizeAsciiOnly(), - ) - symbol is FirConstructorSymbol -> - ScipSymbolDescriptor(Kind.METHOD, "", methodDisambiguator(symbol)) - symbol is FirFunctionSymbol -> - ScipSymbolDescriptor( - Kind.METHOD, - symbol.name.toString(), - methodDisambiguator(symbol), - ) - symbol is FirTypeParameterSymbol -> - ScipSymbolDescriptor(Kind.TYPE_PARAMETER, symbol.name.toString()) - symbol is FirValueParameterSymbol -> - ScipSymbolDescriptor(Kind.PARAMETER, symbol.name.toString()) - symbol is FirVariableSymbol -> ScipSymbolDescriptor(Kind.TERM, symbol.name.toString()) - else -> { - err.println("unknown symbol kind ${symbol.javaClass.simpleName}") - ScipSymbolDescriptor.NONE - } - } - } - - @OptIn(SymbolInternals::class, DirectDeclarationsAccess::class) - private fun methodDisambiguator(symbol: FirFunctionSymbol<*>): String { - val session = symbol.moduleData.session - - val siblings = - when (val containingSymbol = symbol.getContainingSymbol(session)) { - is FirClassSymbol -> - (containingSymbol.fir as FirClass).declarations.map { it.symbol } - is FirFileSymbol -> containingSymbol.fir.declarations.map { it.symbol } - null -> - symbol.moduleData.session.symbolProvider.getTopLevelCallableSymbols( - symbol.packageFqName(), - symbol.name, - ) - else -> return "()" - } - - var count = 0 - var found = false - for (decl in siblings) { - if (decl == symbol) { - found = true - break - } - - if (decl.memberDeclarationNameOrNull?.equals(symbol.name) == true) { - count++ - } - } - - if (count == 0 || !found) return "()" - return "(+${count})" - } - - override fun iterator(): Iterator = globals.values.iterator() -} - -typealias LocalSymbolsCache = SharedLocalSymbolsCache, Symbol> - -@Suppress("FunctionName") -fun LocalSymbolsCache(): LocalSymbolsCache = - SharedLocalSymbolsCache(HashMap()) { Symbol.createLocal(it) } - -operator fun LocalSymbolsCache.plus(symbol: FirBasedSymbol<*>): Symbol = put(symbol) - -class SymbolsCache(private val globals: GlobalSymbolsCache, private val locals: LocalSymbolsCache) { - operator fun get(symbol: FirBasedSymbol<*>) = globals[symbol, locals] - - operator fun get(symbol: FqName) = globals[symbol] -} diff --git a/scip-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor b/scip-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor deleted file mode 100644 index 13fc07284..000000000 --- a/scip-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor +++ /dev/null @@ -1 +0,0 @@ -org.scip_code.scip_java.kotlinc.AnalyzerCommandLineProcessor diff --git a/scip-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar b/scip-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar deleted file mode 100644 index 64d92f95c..000000000 --- a/scip-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +++ /dev/null @@ -1 +0,0 @@ -org.scip_code.scip_java.kotlinc.AnalyzerRegistrar 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 deleted file mode 100644 index 36e94ad56..000000000 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ /dev/null @@ -1,1432 +0,0 @@ -package org.scip_code.scip_java.kotlinc.test - -import com.tschuchort.compiletesting.KotlinCompilation -import com.tschuchort.compiletesting.PluginOption -import com.tschuchort.compiletesting.SourceFile -import io.kotest.assertions.fail -import io.kotest.matchers.collections.shouldContainAll -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import java.io.File -import java.nio.file.Path -import kotlin.test.Test -import kotlin.test.assertEquals -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_java.kotlinc.* - -@OptIn(ExperimentalCompilerApi::class) -class AnalyzerTest { - fun compileScip(path: Path, @Language("kotlin") code: String): Document { - val buildPath = File(path.resolve("build").toString()).apply { mkdir() } - val source = SourceFile.testKt(code) - lateinit var document: Document - - val result = - KotlinCompilation() - .apply { - sources = listOf(source) - compilerPluginRegistrars = listOf(AnalyzerRegistrar { document = it }) - verbose = false - pluginOptions = - listOf( - PluginOption("scip-kotlinc", "sourceroot", path.toString()), - PluginOption("scip-kotlinc", "targetroot", buildPath.toString()), - ) - commandLineProcessors = listOf(AnalyzerCommandLineProcessor()) - workingDir = path.toFile() - } - .compile() - - result.exitCode shouldBe KotlinCompilation.ExitCode.OK - document shouldNotBe null - return document - } - - @Test - fun `basic test`(@TempDir path: Path) { - val document = - compileScip( - path, - """ - package sample - class Banana { - fun foo() { } - }""", - ) - - val occurrences = - arrayOf( - scipOccurrence { - role = REFERENCE - symbol = "sample/" - range { - startLine = 0 - startCharacter = 8 - endLine = 0 - endCharacter = 14 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/Banana#" - range { - startLine = 1 - startCharacter = 6 - endLine = 1 - endCharacter = 12 - } - enclosingRange { - startLine = 1 - endLine = 3 - endCharacter = 1 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/Banana#foo()." - range { - startLine = 2 - startCharacter = 8 - endLine = 2 - endCharacter = 11 - } - enclosingRange { - startLine = 2 - startCharacter = 4 - endLine = 2 - endCharacter = 17 - } - }, - ) - document.occurrencesList.shouldContainAll(*occurrences) - - val symbols = - arrayOf( - scipSymbol { - symbol = "sample/Banana#" - displayName = "Banana" - signatureText = "public final class Banana : Any" - }, - scipSymbol { - symbol = "sample/Banana#foo()." - displayName = "foo" - signatureText = "public final fun foo(): Unit" - }, - ) - document.symbolsList.shouldContainAll(*symbols) - } - - @Test - fun imports(@TempDir path: Path) { - val document = - compileScip( - path, - """ - package sample - - import kotlin.Boolean - import kotlin.Int as KInt - """, - ) - - val occurrences = - arrayOf( - scipOccurrence { - role = REFERENCE - symbol = "sample/" - range { - startLine = 0 - startCharacter = 8 - endLine = 0 - endCharacter = 14 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "kotlin/" - range { - startLine = 2 - startCharacter = 7 - endLine = 2 - endCharacter = 13 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "kotlin/Boolean#" - range { - startLine = 2 - startCharacter = 14 - endLine = 2 - endCharacter = 21 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "kotlin/" - range { - startLine = 3 - startCharacter = 7 - endLine = 3 - endCharacter = 13 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "kotlin/Int#" - range { - startLine = 3 - startCharacter = 14 - endLine = 3 - endCharacter = 17 - } - }, - ) - document.occurrencesList.shouldContainAll(*occurrences) - } - - @Test - fun `local classes`(@TempDir path: Path) { - val document = - compileScip( - path, - """ - package sample - - fun foo() { - class LocalClass { - fun localClassMethod() {} - } - } - """, - ) - - val occurrences = - arrayOf( - scipOccurrence { - role = DEFINITION - symbol = "sample/foo()." - range { - startLine = 2 - startCharacter = 4 - endLine = 2 - endCharacter = 7 - } - enclosingRange { - startLine = 2 - endLine = 6 - endCharacter = 1 - } - }, - // LocalClass - scipOccurrence { - role = DEFINITION - symbol = "local 0" - range { - startLine = 3 - startCharacter = 8 - endLine = 3 - endCharacter = 18 - } - enclosingRange { - startLine = 3 - startCharacter = 2 - endLine = 5 - endCharacter = 3 - } - }, - // LocalClass constructor - scipOccurrence { - role = DEFINITION - symbol = "local 1" - range { - startLine = 3 - startCharacter = 8 - endLine = 3 - endCharacter = 18 - } - enclosingRange { - startLine = 3 - startCharacter = 2 - endLine = 5 - endCharacter = 3 - } - }, - // localClassMethod - scipOccurrence { - role = DEFINITION - symbol = "local 2" - range { - startLine = 4 - startCharacter = 8 - endLine = 4 - endCharacter = 24 - } - enclosingRange { - startLine = 4 - startCharacter = 4 - endLine = 4 - endCharacter = 29 - } - }, - ) - document.occurrencesList.shouldContainAll(*occurrences) - - val symbols = - arrayOf( - scipSymbol { - symbol = "sample/foo()." - displayName = "foo" - signatureText = "public final fun foo(): Unit" - }, - scipSymbol { - symbol = "local 0" - displayName = "LocalClass" - signatureText = "local final class LocalClass : Any" - }, - scipSymbol { - symbol = "local 1" - displayName = "LocalClass" - signatureText = "public constructor(): LocalClass" - }, - scipSymbol { - symbol = "local 2" - displayName = "localClassMethod" - signatureText = "public final fun localClassMethod(): Unit" - }, - ) - document.symbolsList.shouldContainAll(*symbols) - } - - @Test - fun overrides(@TempDir path: Path) { - val document = - compileScip( - path, - """ - package sample - - interface Interface { - fun foo() - } - - class Class : Interface { - override fun foo() {} - } - """, - ) - - val occurrences = - arrayOf( - scipOccurrence { - role = REFERENCE - symbol = "sample/" - range { - startLine = 0 - startCharacter = 8 - endLine = 0 - endCharacter = 14 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/Interface#" - range { - startLine = 2 - startCharacter = 10 - endLine = 2 - endCharacter = 19 - } - enclosingRange { - startLine = 2 - endLine = 4 - endCharacter = 1 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/Interface#foo()." - range { - startLine = 3 - startCharacter = 8 - endLine = 3 - endCharacter = 11 - } - enclosingRange { - startLine = 3 - startCharacter = 4 - endLine = 3 - endCharacter = 13 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/Class#" - range { - startLine = 6 - startCharacter = 6 - endLine = 6 - endCharacter = 11 - } - enclosingRange { - startLine = 6 - endLine = 8 - endCharacter = 1 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "sample/Interface#" - range { - startLine = 6 - startCharacter = 14 - endLine = 6 - endCharacter = 23 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/Class#foo()." - range { - startLine = 7 - startCharacter = 17 - endLine = 7 - endCharacter = 20 - } - enclosingRange { - startLine = 7 - startCharacter = 4 - endLine = 7 - endCharacter = 25 - } - }, - ) - document.occurrencesList.shouldContainAll(*occurrences) - - val symbols = - arrayOf( - scipSymbol { - symbol = "sample/Interface#" - displayName = "Interface" - signatureText = "public abstract interface Interface : Any" - }, - scipSymbol { - symbol = "sample/Interface#foo()." - displayName = "foo" - signatureText = "public abstract fun foo(): Unit\n" - }, - scipSymbol { - symbol = "sample/Class#" - displayName = "Class" - signatureText = "public final class Class : Interface" - addOverriddenSymbols("sample/Interface#") - }, - scipSymbol { - symbol = "sample/Class#foo()." - displayName = "foo" - signatureText = "public open override fun foo(): Unit" - addOverriddenSymbols("sample/Interface#foo().") - }, - ) - document.symbolsList.shouldContainAll(*symbols) - } - - @Test - fun `anonymous object`(@TempDir path: Path) { - val document = - compileScip( - path, - """ - package sample - - interface Interface { - fun foo() - } - - fun main() { - val a = object : Interface { - override fun foo() {} - } - val b = object : Interface { - override fun foo() {} - } - } - """, - ) - - val occurrences = - arrayOf( - scipOccurrence { - role = REFERENCE - symbol = "sample/" - range { - startLine = 0 - startCharacter = 8 - endLine = 0 - endCharacter = 14 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/Interface#" - range { - startLine = 2 - startCharacter = 10 - endLine = 2 - endCharacter = 19 - } - enclosingRange { - startLine = 2 - endLine = 4 - endCharacter = 1 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/Interface#foo()." - range { - startLine = 3 - startCharacter = 8 - endLine = 3 - endCharacter = 11 - } - enclosingRange { - startLine = 3 - startCharacter = 4 - endLine = 3 - endCharacter = 13 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/``#" - range { - startLine = 7 - startCharacter = 12 - endLine = 7 - endCharacter = 18 - } - enclosingRange { - startLine = 7 - startCharacter = 12 - endLine = 9 - endCharacter = 5 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/``#``()." - range { - startLine = 7 - startCharacter = 12 - endLine = 7 - endCharacter = 18 - } - enclosingRange { - startLine = 7 - startCharacter = 12 - endLine = 9 - endCharacter = 5 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "sample/Interface#" - range { - startLine = 7 - startCharacter = 21 - endLine = 7 - endCharacter = 30 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/``#foo()." - range { - startLine = 8 - startCharacter = 21 - endLine = 8 - endCharacter = 24 - } - enclosingRange { - startLine = 8 - startCharacter = 8 - endLine = 8 - endCharacter = 29 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/``#" - range { - startLine = 10 - startCharacter = 12 - endLine = 10 - endCharacter = 18 - } - enclosingRange { - startLine = 10 - startCharacter = 12 - endLine = 12 - endCharacter = 5 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/``#``()." - range { - startLine = 10 - startCharacter = 12 - endLine = 10 - endCharacter = 18 - } - enclosingRange { - startLine = 10 - startCharacter = 12 - endLine = 12 - endCharacter = 5 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "sample/Interface#" - range { - startLine = 10 - startCharacter = 21 - endLine = 10 - endCharacter = 30 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/``#foo()." - range { - startLine = 11 - startCharacter = 21 - endLine = 11 - endCharacter = 24 - } - enclosingRange { - startLine = 11 - startCharacter = 8 - endLine = 11 - endCharacter = 29 - } - }, - ) - document.occurrencesList.shouldContainAll(*occurrences) - - val symbols = - arrayOf( - scipSymbol { - symbol = "sample/Interface#" - displayName = "Interface" - signatureText = "public abstract interface Interface : Any" - }, - scipSymbol { - symbol = "sample/``#" - displayName = "" - signatureText = "object : Interface" - addOverriddenSymbols("sample/Interface#") - }, - scipSymbol { - symbol = "sample/``#foo()." - displayName = "foo" - signatureText = "public open override fun foo(): Unit" - addOverriddenSymbols("sample/Interface#foo().") - }, - scipSymbol { - symbol = "sample/``#" - displayName = "" - signatureText = "object : Interface" - addOverriddenSymbols("sample/Interface#") - }, - scipSymbol { - symbol = "sample/``#foo()." - displayName = "foo" - signatureText = "public open override fun foo(): Unit" - addOverriddenSymbols("sample/Interface#foo().") - }, - ) - document.symbolsList.shouldContainAll(*symbols) - } - - @Test - fun `function return type`(@TempDir path: Path) { - val document = - compileScip( - path, - """ - package sample - - fun foo(arg: Int): Boolean = true - """, - ) - - val occurrences = - arrayOf( - scipOccurrence { - role = DEFINITION - symbol = "sample/foo()." - range { - startLine = 2 - startCharacter = 4 - endLine = 2 - endCharacter = 7 - } - enclosingRange { - startLine = 2 - endLine = 2 - endCharacter = 33 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/foo().(arg)" - range { - startLine = 2 - startCharacter = 8 - endLine = 2 - endCharacter = 11 - } - enclosingRange { - startLine = 2 - startCharacter = 8 - endLine = 2 - endCharacter = 16 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "kotlin/Int#" - range { - startLine = 2 - startCharacter = 13 - endLine = 2 - endCharacter = 16 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "kotlin/Boolean#" - range { - startLine = 2 - startCharacter = 19 - endLine = 2 - endCharacter = 26 - } - }, - ) - document.occurrencesList.shouldContainAll(*occurrences) - } - - @Test - fun `type operators`(@TempDir path: Path) { - val document = - compileScip( - path, - """ - package sample - - fun foo(x: Any) { - when (x) { - is Int -> true - else -> x as Float - } - } - """, - ) - - val occurrences = - arrayOf( - scipOccurrence { - role = REFERENCE - symbol = "kotlin/Int#" - range { - startLine = 4 - startCharacter = 11 - endLine = 4 - endCharacter = 14 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "kotlin/Float#" - range { - startLine = 5 - startCharacter = 21 - endLine = 5 - endCharacter = 26 - } - }, - ) - document.occurrencesList.shouldContainAll(*occurrences) - } - - @Test - fun `exception test`(@TempDir path: Path) { - val buildPath = File(path.resolve("build").toString()).apply { mkdir() } - val result = - KotlinCompilation() - .apply { - sources = listOf(SourceFile.testKt("")) - compilerPluginRegistrars = - listOf(AnalyzerRegistrar { throw Exception("sample text") }) - verbose = false - pluginOptions = - listOf( - PluginOption("scip-kotlinc", "sourceroot", path.toString()), - PluginOption("scip-kotlinc", "targetroot", buildPath.toString()), - ) - commandLineProcessors = listOf(AnalyzerCommandLineProcessor()) - workingDir = path.toFile() - } - .compile() - - result.exitCode shouldBe KotlinCompilation.ExitCode.OK - } - - @Test - // shamelessly stolen code snippet from https://learnxinyminutes.com/docs/kotlin/ - fun `learn x in y test`(@TempDir path: Path) { - val buildPath = File(path.resolve("build").toString()).apply { mkdir() } - - val source = - SourceFile.testKt( - """ - @file:Suppress("UNUSED_VARIABLE", "UNUSED_PARAMETER", "NAME_SHADOWING", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE", "UNUSED_VALUE") - package sample - - fun main(args: Array) { - val fooVal = 10 // we cannot later reassign fooVal to something else - var fooVar = 10 - fooVar = 20 // fooVar can be reassigned - - /* - In most cases, Kotlin can determine what the type of a variable is, - so we don't have to explicitly specify it every time. - We can explicitly declare the type of a variable like so: - */ - val foo: Int = 7 - - /* - Strings can be represented in a similar way as in Java. - Escaping is done with a backslash. - */ - val fooString = "My String Is Here!" - val barString = "Printing on a new line?\nNo Problem!" - val bazString = "Do you want to add a tab?\tNo Problem!" - println(fooString) - println(barString) - println(bazString) - - /* - Strings can contain template expressions. - A template expression starts with a dollar sign (${'$'}). - */ - val fooTemplateString = "$'fooString' has ${"fooString.length"} characters" - println(fooTemplateString) // => My String Is Here! has 18 characters - - /* - For a variable to hold null it must be explicitly specified as nullable. - A variable can be specified as nullable by appending a ? to its type. - We can access a nullable variable by using the ?. operator. - We can use the ?: operator to specify an alternative value to use - if a variable is null. - */ - var fooNullable: String? = "abc" - println(fooNullable?.length) // => 3 - println(fooNullable?.length ?: -1) // => 3 - fooNullable = null - println(fooNullable?.length) // => null - println(fooNullable?.length ?: -1) // => -1 - - /* - Functions can be declared using the "fun" keyword. - Function arguments are specified in brackets after the function name. - Function arguments can optionally have a default value. - The function return type, if required, is specified after the arguments. - */ - fun hello(name: String = "world"): String { - return "Hello, $'name'!" - } - println(hello("foo")) // => Hello, foo! - println(hello(name = "bar")) // => Hello, bar! - println(hello()) // => Hello, world! - - /* - A function parameter may be marked with the "vararg" keyword - to allow a variable number of arguments to be passed to the function. - */ - fun varargExample(vararg names: Int) { - println("Argument has ${"names.size"} elements") - } - varargExample() // => Argument has 0 elements - varargExample(1) // => Argument has 1 elements - varargExample(1, 2, 3) // => Argument has 3 elements - - /* - When a function consists of a single expression then the curly brackets can - be omitted. The body is specified after the = symbol. - */ - fun odd(x: Int): Boolean = x % 2 == 1 - println(odd(6)) // => false - println(odd(7)) // => true - - // If the return type can be inferred then we don't need to specify it. - fun even(x: Int) = x % 2 == 0 - println(even(6)) // => true - println(even(7)) // => false - - // Functions can take functions as arguments and return functions. - fun not(f: (Int) -> Boolean): (Int) -> Boolean { - return {n -> !f.invoke(n)} - } - // Named functions can be specified as arguments using the :: operator. - val notOdd = not(::odd) - val notEven = not(::even) - // Lambda expressions can be specified as arguments. - val notZero = not {n -> n == 0} - /* - If a lambda has only one parameter - then its declaration can be omitted (along with the ->). - The name of the single parameter will be "it". - */ - val notPositive = not {it > 0} - for (i in 0..4) { - println("${"notOdd(i)"} ${"notEven(i)"} ${"notZero(i)"} ${"notPositive(i)"}") - } - - // The "class" keyword is used to declare classes. - class ExampleClass(val x: Int) { - fun memberFunction(y: Int): Int { - return x + y - } - - infix fun infixMemberFunction(y: Int): Int { - return x * y - } - } - /* - To create a new instance we call the constructor. - Note that Kotlin does not have a "new" keyword. - */ - val fooExampleClass = ExampleClass(7) - // Member functions can be called using dot notation. - println(fooExampleClass.memberFunction(4)) // => 11 - /* - If a function has been marked with the "infix" keyword then it can be - called using infix notation. - */ - println(fooExampleClass infixMemberFunction 4) // => 28 - - /* - Data classes are a concise way to create classes that just hold data. - The "hashCode"/"equals" and "toString" methods are automatically generated. - */ - data class DataClassExample (val x: Int, val y: Int, val z: Int) - val fooData = DataClassExample(1, 2, 4) - println(fooData) // => DataClassExample(x=1, y=2, z=4) - - // Data classes have a "copy" function. - val fooCopy = fooData.copy(y = 100) - println(fooCopy) // => DataClassExample(x=1, y=100, z=4) - - // Objects can be destructured into multiple variables. - val (a, b, c) = fooCopy - println("$'a' $'b' $'c'") // => 1 100 4 - - // destructuring in "for" loop - for ((a, b, c) in listOf(fooData)) { - println("$'a' $'b' $'c'") // => 1 2 4 - } - - val mapData = mapOf("a" to 1, "b" to 2) - // Map.Entry is destructurable as well - for ((key, value) in mapData) { - println("$'key' -> $'value'") - } - - // The "with" function is similar to the JavaScript "with" statement. - data class MutableDataClassExample (var x: Int, var y: Int, var z: Int) - val fooMutableData = MutableDataClassExample(7, 4, 9) - with (fooMutableData) { - x -= 2 - y += 2 - z-- - } - println(fooMutableData) // => MutableDataClassExample(x=5, y=6, z=8) - - /* - We can create a list using the "listOf" function. - The list will be immutable - elements cannot be added or removed. - */ - val fooList = listOf("a", "b", "c") - println(fooList.size) // => 3 - println(fooList.first()) // => a - println(fooList.last()) // => c - // Elements of a list can be accessed by their index. - println(fooList[1]) // => b - - // A mutable list can be created using the "mutableListOf" function. - val fooMutableList = mutableListOf("a", "b", "c") - fooMutableList.add("d") - println(fooMutableList.last()) // => d - println(fooMutableList.size) // => 4 - - // We can create a set using the "setOf" function. - val fooSet = setOf("a", "b", "c") - println(fooSet.contains("a")) // => true - println(fooSet.contains("z")) // => false - - // We can create a map using the "mapOf" function. - val fooMap = mapOf("a" to 8, "b" to 7, "c" to 9) - // Map values can be accessed by their key. - println(fooMap["a"]) // => 8 - - /* - Sequences represent lazily-evaluated collections. - We can create a sequence using the "generateSequence" function. - */ - val fooSequence = generateSequence(1, { it + 1 }) - val x = fooSequence.take(10).toList() - println(x) // => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - - // An example of using a sequence to generate Fibonacci numbers: - fun fibonacciSequence(): Sequence { - var a = 0L - var b = 1L - - fun next(): Long { - val result = a + b - a = b - b = result - return a - } - - return generateSequence(::next) - } - val y = fibonacciSequence().take(10).toList() - println(y) // => [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] - - // Kotlin provides higher-order functions for working with collections. - val z = (1..9).map {it * 3} - .filter {it < 20} - .groupBy {it % 2 == 0} - .mapKeys {if (it.key) "even" else "odd"} - println(z) // => {odd=[3, 9, 15], even=[6, 12, 18]} - - // A "for" loop can be used with anything that provides an iterator. - for (c in "hello") { - println(c) - } - - // "while" loops work in the same way as other languages. - var ctr = 0 - while (ctr < 5) { - println(ctr) - ctr++ - } - do { - println(ctr) - ctr++ - } while (ctr < 10) - - /* - "if" can be used as an expression that returns a value. - For this reason the ternary ?: operator is not needed in Kotlin. - */ - val num = 5 - val message = if (num % 2 == 0) "even" else "odd" - println("$'num' is $'message'") // => 5 is odd - - // "when" can be used as an alternative to "if-else if" chains. - val i = 10 - when { - i < 7 -> println("first block") - fooString.startsWith("hello") -> println("second block") - else -> println("else block") - } - - // "when" can be used with an argument. - when (i) { - 0, 21 -> println("0 or 21") - in 1..20 -> println("in the range 1 to 20") - else -> println("none of the above") - } - - // "when" can be used as a function that returns a value. - var result = when (i) { - 0, 21 -> "0 or 21" - in 1..20 -> "in the range 1 to 20" - else -> "none of the above" - } - println(result) - - /* - We can check if an object is of a particular type by using the "is" operator. - If an object passes a type check then it can be used as that type without - explicitly casting it. - */ - fun smartCastExample(x: Any) : Boolean { - if (x is Boolean) { - // x is automatically cast to Boolean - return x - } else if (x is Int) { - // x is automatically cast to Int - return x > 0 - } else if (x is String) { - // x is automatically cast to String - return x.isNotEmpty() - } else { - return false - } - } - println(smartCastExample("Hello, world!")) // => true - println(smartCastExample("")) // => false - println(smartCastExample(5)) // => true - println(smartCastExample(0)) // => false - println(smartCastExample(true)) // => true - - // Smartcast also works with when block - fun smartCastWhenExample(x: Any) = when (x) { - is Boolean -> x - is Int -> x > 0 - is String -> x.isNotEmpty() - else -> false - } - - /* - Extensions are a way to add new functionality to a class. - This is similar to C# extension methods. - */ - fun String.remove(c: Char): String { - return this.filter {it != c} - } - println("Hello, world!".remove('l')) // => Heo, word! - } - - // Enum classes are similar to Java enum types. - enum class EnumExample { - A, B, C // Enum constants are separated with commas. - } - fun printEnum() = println(EnumExample.A) // => A - - // Since each enum is an instance of the enum class, they can be initialized as: - enum class EnumExample1(val value: Int) { - A(value = 1), - B(value = 2), - C(value = 3) - } - fun printProperty() = println(EnumExample1.A.value) // => 1 - - // Every enum has properties to obtain its name and ordinal(position) in the enum class declaration: - fun printName() = println(EnumExample1.A.name) // => A - fun printPosition() = println(EnumExample1.A.ordinal) // => 0 - - /* - The "object" keyword can be used to create singleton objects. - We cannot instantiate it but we can refer to its unique instance by its name. - This is similar to Scala singleton objects. - */ - object ObjectExample { - fun hello(): String { - return "hello" - } - - override fun toString(): String { - return "Hello, it's me, ${"ObjectExample::class.simpleName"}" - } - } - - - fun useSingletonObject() { - println(ObjectExample.hello()) // => hello - // In Kotlin, "Any" is the root of the class hierarchy, just like "Object" is in Java - val someRef: Any = ObjectExample - println(someRef) // => Hello, it's me, ObjectExample - } - - - /* The not-null assertion operator (!!) converts any value to a non-null type and - throws an exception if the value is null. - */ - var b: String? = "abc" - val l = b!!.length - - data class Counter(var value: Int) { - // overload Counter += Int - operator fun plusAssign(increment: Int) { - this.value += increment - } - - // overload Counter++ and ++Counter - operator fun inc() = Counter(value + 1) - - // overload Counter + Counter - operator fun plus(other: Counter) = Counter(this.value + other.value) - - // overload Counter * Counter - operator fun times(other: Counter) = Counter(this.value * other.value) - - // overload Counter * Int - operator fun times(value: Int) = Counter(this.value * value) - - // overload Counter in Counter - operator fun contains(other: Counter) = other.value == this.value - - // overload Counter[Int] = Int - operator fun set(index: Int, value: Int) { - this.value = index + value - } - - // overload Counter instance invocation - operator fun invoke() = println("The value of the counter is $'value'") - - } - /* You can also overload operators through extension methods */ - // overload -Counter - operator fun Counter.unaryMinus() = Counter(-this.value) - - fun operatorOverloadingDemo() { - var counter1 = Counter(0) - var counter2 = Counter(5) - counter1 += 7 - println(counter1) // => Counter(value=7) - println(counter1 + counter2) // => Counter(value=12) - println(counter1 * counter2) // => Counter(value=35) - println(counter2 * 2) // => Counter(value=10) - println(counter1 in Counter(5)) // => false - println(counter1 in Counter(7)) // => true - counter1[26] = 10 - println(counter1) // => Counter(value=36) - counter1() // => The value of the counter is 36 - println(-counter2) // => Counter(value=-5) - } - """ - ) - - val result = - KotlinCompilation() - .apply { - sources = listOf(source) - compilerPluginRegistrars = listOf(AnalyzerRegistrar()) - verbose = false - pluginOptions = - listOf( - PluginOption("scip-kotlinc", "sourceroot", path.toString()), - PluginOption("scip-kotlinc", "targetroot", buildPath.toString()), - ) - commandLineProcessors = listOf(AnalyzerCommandLineProcessor()) - workingDir = path.toFile() - } - .compile() - - result.exitCode shouldBe KotlinCompilation.ExitCode.OK - } - - @Test - fun `compound package name semicolon test`(@TempDir path: Path) { - val document = - compileScip( - path, - """ - package hello.sample; - class Apple - """ - .trimIndent(), - ) - - val occurrences = - arrayOf( - scipOccurrence { - role = REFERENCE - symbol = "hello/" - range { - startLine = 0 - startCharacter = 8 - endLine = 0 - endCharacter = 13 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "hello/sample/" - range { - startLine = 0 - startCharacter = 14 - endLine = 0 - endCharacter = 20 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "hello/sample/Apple#" - range { - startLine = 1 - startCharacter = 6 - endLine = 1 - endCharacter = 11 - } - enclosingRange { - startLine = 1 - endLine = 1 - endCharacter = 11 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "hello/sample/Apple#``()." - range { - startLine = 1 - startCharacter = 6 - endLine = 1 - endCharacter = 11 - } - enclosingRange { - startLine = 1 - endLine = 1 - endCharacter = 11 - } - }, - ) - - document.occurrencesList.shouldContainAll(*occurrences) - - val symbols = - arrayOf( - scipSymbol { - symbol = "hello/sample/Apple#" - displayName = "Apple" - signatureText = "public final class Apple : Any" - } - ) - - document.symbolsList.shouldContainAll(*symbols) - } - - @Test - fun `simple package name semicolon test`(@TempDir path: Path) { - val document = - compileScip( - path, - """ - package sample; - class Banana { - fun foo() { } - }""", - ) - - val occurrences = - arrayOf( - scipOccurrence { - role = REFERENCE - symbol = "sample/" - range { - startLine = 0 - startCharacter = 8 - endLine = 0 - endCharacter = 14 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/Banana#" - range { - startLine = 1 - startCharacter = 6 - endLine = 1 - endCharacter = 12 - } - enclosingRange { - startLine = 1 - endLine = 3 - endCharacter = 1 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/Banana#foo()." - range { - startLine = 2 - startCharacter = 8 - endLine = 2 - endCharacter = 11 - } - enclosingRange { - startLine = 2 - startCharacter = 4 - endLine = 2 - endCharacter = 17 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "sample/Banana#" - range { - startLine = 1 - startCharacter = 6 - endLine = 1 - endCharacter = 12 - } - enclosingRange { - startLine = 1 - endLine = 3 - endCharacter = 1 - } - }, - ) - document.occurrencesList.shouldContainAll(*occurrences) - - val symbols = - arrayOf( - scipSymbol { - symbol = "sample/Banana#" - displayName = "Banana" - signatureText = "public final class Banana : Any" - }, - scipSymbol { - symbol = "sample/Banana#foo()." - displayName = "foo" - signatureText = "public final fun foo(): Unit" - }, - ) - document.symbolsList.shouldContainAll(*symbols) - } - - @Test - fun documentation(@TempDir path: Path) { - val document = - compileScip( - path, - """ - package sample - import java.io.Serializable - abstract class DocstringSuperclass - - /** Example class docstring */ - class Docstrings: DocstringSuperclass(), Serializable - - /** - * Example method docstring - * - **/ - inline fun docstrings(msg: String): Int { return msg.length } - """ - .trimIndent(), - ) - document.assertDocumentation("sample/Docstrings#", "Example class docstring") - document.assertDocumentation("sample/docstrings().", "Example method docstring") - } - - private fun Document.assertDocumentation(symbol: String, expectedDocumentation: String) { - val info = - this.symbolsList.find { it.symbol == symbol } - ?: fail("no SymbolInformation for symbol $symbol") - val obtainedDocumentation = info.documentationList.joinToString("\n").trim() - assertEquals(expectedDocumentation, obtainedDocumentation) - } -} 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 deleted file mode 100644 index 1e8ee95a7..000000000 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipBuilders.kt +++ /dev/null @@ -1,121 +0,0 @@ -package org.scip_code.scip_java.kotlinc.test - -import org.scip_code.scip.Occurrence -import org.scip_code.scip.SymbolInformation -import org.scip_code.scip.SymbolRole -import org.scip_code.scip.relationship -import org.scip_code.scip.signature -import org.scip_code.scip.symbolInformation -import org.scip_code.scip_java.shared.ScipRange - -/** - * Tiny DSL for building SCIP [Occurrence] / [SymbolInformation] test fixtures with the same shape - * as the original SCIP-based one used by the Kotlin tests. - * - *

Example: - * ``` - * scipOccurrence { - * role = DEFINITION - * symbol = "sample/Banana#" - * range { startLine = 1; startCharacter = 6; endLine = 1; endCharacter = 12 } - * enclosingRange { startLine = 1; endLine = 3; endCharacter = 1 } - * } - * ``` - */ -internal val REFERENCE: Int = SymbolRole.UnspecifiedSymbolRole.number -internal val DEFINITION: Int = SymbolRole.Definition.number - -@DslMarker annotation class ScipBuilderDsl - -@ScipBuilderDsl -class ScipRangeBuilder { - var startLine: Int = 0 - var startCharacter: Int = 0 - /** - * Default sentinel: when [endLine] is left untouched, the produced range is single-line at - * [startLine]. - */ - var endLine: Int = -1 - var endCharacter: Int = 0 - - internal fun toScipRange(): ScipRange { - val line = if (endLine < 0) startLine else endLine - return ScipRange.range(startLine, startCharacter, line, endCharacter) - } -} - -@ScipBuilderDsl -class ScipOccurrenceBuilder { - var role: Int = REFERENCE - var symbol: String = "" - private var range: ScipRange? = null - private var enclosingRange: ScipRange? = null - - fun range(block: ScipRangeBuilder.() -> Unit) { - range = ScipRangeBuilder().apply(block).toScipRange() - } - - fun enclosingRange(block: ScipRangeBuilder.() -> Unit) { - enclosingRange = ScipRangeBuilder().apply(block).toScipRange() - } - - internal fun build(): Occurrence { - val builder = Occurrence.newBuilder().setSymbol(symbol).setSymbolRoles(role) - range?.let { - if (it.isSingleLine) builder.singleLineRange = it.toSingleLineRange() - else builder.multiLineRange = it.toMultiLineRange() - } - enclosingRange?.let { - if (it.isSingleLine) builder.singleLineEnclosingRange = it.toSingleLineRange() - else builder.multiLineEnclosingRange = it.toMultiLineRange() - } - return builder.build() - } -} - -@ScipBuilderDsl -class ScipSymbolInformationBuilder { - var symbol: String = "" - var displayName: String = "" - var signatureText: String? = null - private val docs = mutableListOf() - private val overrides = mutableListOf() - - fun documentation(text: String) { - docs += text - } - - /** - * Appends an `is_implementation` [Relationship]. Mirrors the old SCIP-flavored - * `addOverriddenSymbols` so existing test fixtures port over with minimal diff. - */ - fun addOverriddenSymbols(vararg symbols: String) { - overrides.addAll(symbols) - } - - internal fun build(): SymbolInformation = symbolInformation { - symbol = this@ScipSymbolInformationBuilder.symbol - if (this@ScipSymbolInformationBuilder.displayName.isNotEmpty()) { - displayName = this@ScipSymbolInformationBuilder.displayName - } - signatureText?.let { sigText -> - signatureDocumentation = signature { - language = "kotlin" - text = sigText - } - } - for (d in docs) documentation += d - for (s in overrides) { - relationships += relationship { - symbol = s - isImplementation = true - } - } - } -} - -internal fun scipOccurrence(block: ScipOccurrenceBuilder.() -> Unit): Occurrence = - ScipOccurrenceBuilder().apply(block).build() - -internal fun scipSymbol(block: ScipSymbolInformationBuilder.() -> Unit): SymbolInformation = - ScipSymbolInformationBuilder().apply(block).build() 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 deleted file mode 100644 index 048d33cee..000000000 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt +++ /dev/null @@ -1,793 +0,0 @@ -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_java.kotlinc.* -import org.scip_code.scip_java.kotlinc.test.ExpectedSymbols.ScipData -import org.scip_code.scip_java.kotlinc.test.ExpectedSymbols.SymbolCacheData - -@ExperimentalCompilerApi -class ScipSymbolsTest { - @TestFactory - fun `method disambiguator`() = - listOf( - ExpectedSymbols( - "Basic two methods", - SourceFile.testKt( - """ - |class Test { - | fun sample() {} - | fun sample(x: Int) {} - |} - |""" - .trimMargin() - ), - symbolsCacheData = - SymbolCacheData( - listOf("Test#sample().".symbol(), "Test#sample(+1).".symbol()) - ), - ), - ExpectedSymbols( - "Inline class constructor", - SourceFile.testKt( - """ - |class Test(val x: Int) - |""" - .trimMargin() - ), - symbolsCacheData = SymbolCacheData(listOf("Test#``().(x)".symbol())), - ), - ExpectedSymbols( - "Inline + secondary class constructors", - SourceFile.testKt( - """ - |class Test(val x: Int) { - | constructor(y: Long): this(y.toInt()) - | constructor(z: String): this(z.toInt()) - |} - |""" - .trimMargin() - ), - symbolsCacheData = - SymbolCacheData( - listOf( - "Test#``().(x)".symbol(), - "Test#``(+1).(y)".symbol(), - "Test#``(+2).(z)".symbol(), - ) - ), - ), - ExpectedSymbols( - "Disambiguator number is not affected by different named methods", - SourceFile.testKt( - """ - |class Test { - | fun sample() {} - | fun test() {} - | fun test(x: Int) {} - |} - |""" - .trimMargin() - ), - symbolsCacheData = - SymbolCacheData(listOf("Test#test().".symbol(), "Test#test(+1).".symbol())), - ), - ExpectedSymbols( - "Top level overloaded functions", - SourceFile.testKt( - """ - |fun test() {} - |fun test(x: Int) {} - |""" - .trimMargin() - ), - symbolsCacheData = - SymbolCacheData(listOf("test().".symbol(), "test(+1).(x)".symbol())), - ), - ExpectedSymbols( - "Annotations incl annotation type alias", - SourceFile.testKt( - """ - |import kotlin.contracts.ExperimentalContracts - |import kotlin.test.Test - | - |@ExperimentalContracts - |class Banaan { - | @Test - | fun test() {} - |} - |""" - .trimMargin() - ), - symbolsCacheData = - SymbolCacheData( - listOf( - "kotlin/contracts/ExperimentalContracts#".symbol(), - "kotlin/test/Test#".symbol(), - ) - ), - ), - // https://kotlinlang.slack.com/archives/C7L3JB43G/p1624995376114900 - /*ExpectedSymbols( - "Method call with type parameters", - SourceFile.testKt(""" - import org.junit.jupiter.api.io.TempDir - val burger = LinkedHashMap() - """), - symbolsCacheData = SymbolCacheData( - listOf("kotlin/collection/TypeAliasesKt#LinkedHashMap#``().".symbol()) - ) - )*/ - ) - .mapCheckExpectedSymbols() - - @TestFactory - fun `check package symbols`() = - listOf( - ExpectedSymbols( - "single component package name", - SourceFile.testKt( - """ - |package main - | - |class Test - |""" - .trimMargin() - ), - symbolsCacheData = SymbolCacheData(listOf("main/Test#".symbol()), 0), - ), - ExpectedSymbols( - "multi component package name", - SourceFile.testKt( - """ - |package test.sample.main - | - |class Test - |""" - .trimMargin() - ), - symbolsCacheData = SymbolCacheData(listOf("test/sample/main/Test#".symbol()), 0), - ), - ExpectedSymbols( - "no package name", - SourceFile.testKt( - """ - |class Test - |""" - .trimMargin() - ), - symbolsCacheData = SymbolCacheData(listOf("Test#".symbol()), 0), - ), - ) - .mapCheckExpectedSymbols() - - @TestFactory - fun `check locals counts`() = - listOf( - ExpectedSymbols( - "simple variables", - SourceFile.testKt( - """ - |fun test() { - | val x = "hello" - | println(x) - |} - |""" - .trimMargin() - ), - symbolsCacheData = SymbolCacheData(localsCount = 1), - ) - ) - .mapCheckExpectedSymbols() - - @TestFactory - fun `builtin symbols`() = - listOf( - ExpectedSymbols( - "types", - SourceFile.testKt( - """ - |var x: Int = 1 - |lateinit var y: Unit - |lateinit var z: Any - |lateinit var w: Nothing - |""" - .trimMargin() - ), - symbolsCacheData = - SymbolCacheData( - listOf( - "kotlin/Int#".symbol(), - "kotlin/Unit#".symbol(), - "kotlin/Any#".symbol(), - "kotlin/Nothing#".symbol(), - ) - ), - ), - ExpectedSymbols( - "functions", - SourceFile.testKt( - """ - |val x = mapOf() - |fun main() { - | println() - |} - |""" - .trimMargin() - ), - symbolsCacheData = - SymbolCacheData( - listOf( - "kotlin/collections/mapOf(+2).".symbol(), - "kotlin/io/println(+10).".symbol(), - ) - ), - ), - ) - .mapCheckExpectedSymbols() - - @TestFactory - fun `reference expressions`() = - listOf( - ExpectedSymbols( - "dot qualified expression", - SourceFile.testKt( - """ - |import java.lang.System - | - |fun main() { - | System.err - |} - |""" - .trimMargin() - ), - symbolsCacheData = SymbolCacheData(listOf("java/lang/System#err.".symbol())), - ) - ) - .mapCheckExpectedSymbols() - - @TestFactory - fun `properties with getters-setters`() = - listOf( - ExpectedSymbols( - "top level properties - implicit", - SourceFile.testKt( - """ - |var x: Int = 5 - |""" - .trimMargin() - ), - scip = - ScipData( - expectedOccurrences = - listOf( - scipOccurrence { - role = DEFINITION - symbol = "x." - range { - startLine = 0 - startCharacter = 4 - endLine = 0 - endCharacter = 5 - } - enclosingRange { endCharacter = 14 } - }, - scipOccurrence { - role = DEFINITION - symbol = "getX()." - range { - startLine = 0 - startCharacter = 4 - endLine = 0 - endCharacter = 5 - } - enclosingRange { endCharacter = 14 } - }, - scipOccurrence { - role = DEFINITION - symbol = "setX()." - range { - startLine = 0 - startCharacter = 4 - endLine = 0 - endCharacter = 5 - } - enclosingRange { endCharacter = 14 } - }, - ) - ), - ), - ExpectedSymbols( - "top level properties - explicit getter", - SourceFile.testKt( - """ - |var x: Int = 5 - | get() = field + 10 - |""" - .trimMargin() - ), - scip = - ScipData( - expectedOccurrences = - listOf( - scipOccurrence { - role = DEFINITION - symbol = "x." - range { - startLine = 0 - startCharacter = 4 - endLine = 0 - endCharacter = 5 - } - enclosingRange { - endLine = 1 - endCharacter = 22 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "setX()." - range { - startLine = 0 - startCharacter = 4 - endLine = 0 - endCharacter = 5 - } - enclosingRange { - endLine = 1 - endCharacter = 22 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "getX()." - range { - startLine = 1 - startCharacter = 4 - endLine = 1 - endCharacter = 7 - } - enclosingRange { - startLine = 1 - startCharacter = 4 - endLine = 1 - endCharacter = 22 - } - }, - ) - ), - ), - ExpectedSymbols( - "top level properties - explicit setter", - SourceFile.testKt( - """ - |var x: Int = 5 - | set(value) { field = value + 5 } - |""" - .trimMargin() - ), - scip = - ScipData( - expectedOccurrences = - listOf( - scipOccurrence { - role = DEFINITION - symbol = "x." - range { - startLine = 0 - startCharacter = 4 - endLine = 0 - endCharacter = 5 - } - enclosingRange { - endLine = 1 - endCharacter = 36 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "getX()." - range { - startLine = 0 - startCharacter = 4 - endLine = 0 - endCharacter = 5 - } - enclosingRange { - endLine = 1 - endCharacter = 36 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "setX()." - range { - startLine = 1 - startCharacter = 4 - endLine = 1 - endCharacter = 7 - } - enclosingRange { - startLine = 1 - startCharacter = 4 - endLine = 1 - endCharacter = 36 - } - }, - ) - ), - ), - ExpectedSymbols( - "top level properties - explicit getter & setter", - SourceFile.testKt( - """ - |var x: Int = 5 - | get() = field + 10 - | set(value) { field = value + 10 } - |""" - .trimMargin() - ), - scip = - ScipData( - expectedOccurrences = - listOf( - scipOccurrence { - role = DEFINITION - symbol = "x." - range { - startLine = 0 - startCharacter = 4 - endLine = 0 - endCharacter = 5 - } - enclosingRange { - endLine = 2 - endCharacter = 37 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "getX()." - range { - startLine = 1 - startCharacter = 4 - endLine = 1 - endCharacter = 7 - } - enclosingRange { - startLine = 1 - startCharacter = 4 - endLine = 1 - endCharacter = 22 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "setX()." - range { - startLine = 2 - startCharacter = 4 - endLine = 2 - endCharacter = 7 - } - enclosingRange { - startLine = 2 - startCharacter = 4 - endLine = 2 - endCharacter = 37 - } - }, - ) - ), - ), - ExpectedSymbols( - "class constructor properties", - SourceFile.testKt( - """ - |class Test(var sample: Int, text: String): Throwable(sample.toString()) { - | fun test() { - | println(sample) - | } - |} - |""" - .trimMargin() - ), - scip = - ScipData( - expectedOccurrences = - listOf( - scipOccurrence { - role = DEFINITION - symbol = "Test#``().(sample)" - range { - startLine = 0 - startCharacter = 15 - endLine = 0 - endCharacter = 21 - } - enclosingRange { - startCharacter = 11 - endCharacter = 26 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "Test#sample." - range { - startLine = 0 - startCharacter = 15 - endLine = 0 - endCharacter = 21 - } - enclosingRange { - startCharacter = 11 - endCharacter = 26 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "Test#getSample()." - range { - startLine = 0 - startCharacter = 15 - endLine = 0 - endCharacter = 21 - } - enclosingRange { - startCharacter = 11 - endCharacter = 26 - } - }, - scipOccurrence { - role = DEFINITION - symbol = "Test#setSample()." - range { - startLine = 0 - startCharacter = 15 - endLine = 0 - endCharacter = 21 - } - enclosingRange { - startCharacter = 11 - endCharacter = 26 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "Test#``().(sample)" - range { - startLine = 0 - startCharacter = 53 - endLine = 0 - endCharacter = 59 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "Test#sample." - range { - startLine = 2 - startCharacter = 16 - endLine = 2 - endCharacter = 22 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "Test#getSample()." - range { - startLine = 2 - startCharacter = 16 - endLine = 2 - endCharacter = 22 - } - }, - ) - ), - ), - ) - .mapCheckExpectedSymbols() - - @TestFactory - fun `class constructors`() = - listOf( - ExpectedSymbols( - "implicit primary constructor", - SourceFile.testKt( - """ - |class Banana - |""" - .trimMargin() - ), - scip = - ScipData( - expectedOccurrences = - listOf( - scipOccurrence { - role = DEFINITION - symbol = "Banana#" - range { - startLine = 0 - startCharacter = 6 - endLine = 0 - endCharacter = 12 - } - enclosingRange { endCharacter = 12 } - }, - scipOccurrence { - role = DEFINITION - symbol = "Banana#``()." - range { - startLine = 0 - startCharacter = 6 - endLine = 0 - endCharacter = 12 - } - enclosingRange { endCharacter = 12 } - }, - ) - ), - ), - ExpectedSymbols( - "explicit primary constructor without keyword", - SourceFile.testKt( - """ - |class Banana(size: Int) - |""" - .trimMargin() - ), - scip = - ScipData( - expectedOccurrences = - listOf( - scipOccurrence { - role = DEFINITION - symbol = "Banana#" - range { - startLine = 0 - startCharacter = 6 - endLine = 0 - endCharacter = 12 - } - enclosingRange { endCharacter = 23 } - }, - scipOccurrence { - role = DEFINITION - symbol = "Banana#``()." - range { - startLine = 0 - startCharacter = 6 - endLine = 0 - endCharacter = 12 - } - enclosingRange { - startCharacter = 12 - endCharacter = 23 - } - }, - ) - ), - ), - ExpectedSymbols( - "explicit primary constructor with keyword", - SourceFile.testKt( - """ - |class Banana constructor(size: Int) - |""" - .trimMargin() - ), - scip = - ScipData( - expectedOccurrences = - listOf( - scipOccurrence { - role = DEFINITION - symbol = "Banana#" - range { - startLine = 0 - startCharacter = 6 - endLine = 0 - endCharacter = 12 - } - enclosingRange { endCharacter = 35 } - }, - scipOccurrence { - role = DEFINITION - symbol = "Banana#``()." - range { - startLine = 0 - startCharacter = 13 - endLine = 0 - endCharacter = 24 - } - enclosingRange { - startCharacter = 13 - endCharacter = 35 - } - }, - ) - ), - ), - ) - .mapCheckExpectedSymbols() - - @TestFactory - fun `Single Abstract Method interface`() = - listOf( - ExpectedSymbols( - "basic java.lang.Runnable", - SourceFile.testKt( - """ - |val x = Runnable { }.run() - |""" - .trimMargin() - ), - scip = - ScipData( - expectedOccurrences = - listOf( - scipOccurrence { - role = REFERENCE - symbol = "java/lang/Runnable#" - range { - startLine = 0 - startCharacter = 8 - endLine = 0 - endCharacter = 16 - } - }, - scipOccurrence { - role = REFERENCE - symbol = "java/lang/Runnable#run()." - range { - startLine = 0 - startCharacter = 21 - endLine = 0 - endCharacter = 24 - } - }, - ) - ), - ) - ) - .mapCheckExpectedSymbols() - - @TestFactory - fun kdoc() = - listOf( - ExpectedSymbols( - "empty kdoc line", - SourceFile.testKt( - """ - |/** - | - |hello world - |* test content - |*/ - |val x = "" - |""" - .trimMargin() - ), - scip = - ScipData( - expectedSymbols = - listOf( - scipSymbol { - symbol = "x." - displayName = "x" - signatureText = "public final val x: String" - documentation("hello world\n test content") - }, - scipSymbol { - symbol = "getX()." - displayName = "x" - signatureText = "public get(): String" - documentation("hello world\n test content") - }, - ) - ), - ) - ) - .mapCheckExpectedSymbols() -} 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 deleted file mode 100644 index 4576e9893..000000000 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt +++ /dev/null @@ -1,206 +0,0 @@ -package org.scip_code.scip_java.kotlinc.test - -import com.tschuchort.compiletesting.KotlinCompilation -import com.tschuchort.compiletesting.SourceFile -import io.kotest.assertions.assertSoftly -import io.kotest.assertions.throwables.shouldNotThrowAny -import io.kotest.matchers.collections.shouldContainInOrder -import io.kotest.matchers.shouldBe -import java.nio.file.Path -import java.nio.file.Paths -import org.intellij.lang.annotations.Language -import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension -import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar -import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi -import org.jetbrains.kotlin.config.CompilerConfiguration -import org.jetbrains.kotlin.diagnostics.DiagnosticReporter -import org.jetbrains.kotlin.fir.FirSession -import org.jetbrains.kotlin.fir.analysis.checkers.MppCheckerKind -import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext -import org.jetbrains.kotlin.fir.analysis.checkers.declaration.DeclarationCheckers -import org.jetbrains.kotlin.fir.analysis.checkers.declaration.FirFileChecker -import org.jetbrains.kotlin.fir.declarations.FirFile -import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar -import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrarAdapter -import org.junit.jupiter.api.Assumptions.assumeFalse -import org.junit.jupiter.api.DynamicTest -import org.junit.jupiter.api.DynamicTest.dynamicTest -import org.scip_code.scip.Document -import org.scip_code.scip.Occurrence -import org.scip_code.scip.SymbolInformation -import org.scip_code.scip_java.kotlinc.* -import org.scip_code.scip_java.kotlinc.AnalyzerCheckers.Companion.visitors -import org.scip_code.scip_java.shared.ScipOptions - -data class ExpectedSymbols( - val testName: String, - val source: SourceFile, - val symbolsCacheData: SymbolCacheData? = null, - val scip: ScipData? = null, -) { - /** Subset of a SCIP [Document] that a single test wants to assert on. */ - data class ScipData( - val expectedOccurrences: List? = null, - val expectedSymbols: List? = null, - ) - - data class SymbolCacheData( - val expectedGlobals: List? = null, - val localsCount: Int? = null, - ) -} - -fun SourceFile.Companion.testKt(@Language("kotlin") contents: String): SourceFile = - kotlin("Test.kt", contents) - -@ExperimentalCompilerApi -fun List.mapCheckExpectedSymbols(): List = - this.flatMap { (testName, source, symbolsData, scipData) -> - val globals = GlobalSymbolsCache(testing = true) - val locals = LocalSymbolsCache() - lateinit var document: Document - val compilation = configureTestCompiler(source, globals, locals) { document = it } - listOf( - dynamicTest("$testName - compilation") { - val result = shouldNotThrowAny { compilation.compile() } - result.exitCode shouldBe KotlinCompilation.ExitCode.OK - }, - dynamicTest("$testName - symbols") { - symbolsData?.apply { - println( - "checking symbols: ${expectedGlobals?.size ?: 0} globals and presence of $localsCount locals" - ) - checkContainsExpectedSymbols(globals, locals, expectedGlobals, localsCount) - } ?: assumeFalse(true) - }, - dynamicTest("$testName - scip") { - scipData?.apply { - println( - "checking scip: ${expectedOccurrences?.size ?: 0} occurrences and ${expectedSymbols?.size ?: 0} symbols" - ) - checkContainsExpectedScip(document, expectedOccurrences, expectedSymbols) - } ?: assumeFalse(true) - }, - ) - } - -fun checkContainsExpectedSymbols( - globals: GlobalSymbolsCache, - locals: LocalSymbolsCache, - expectedGlobals: List?, - localsCount: Int? = null, -) { - assertSoftly(globals) { expectedGlobals?.let { this.shouldContainInOrder(it) } } - localsCount?.also { locals.size shouldBe it } -} - -fun checkContainsExpectedScip( - document: Document, - expectedOccurrences: List?, - expectedSymbols: List?, -) { - assertSoftly(document.occurrencesList) { - expectedOccurrences?.let { this.shouldContainInOrder(it) } - } - assertSoftly(document.symbolsList) { expectedSymbols?.let { this.shouldContainInOrder(it) } } -} - -@OptIn(ExperimentalCompilerApi::class) -private fun configureTestCompiler( - source: SourceFile, - globals: GlobalSymbolsCache, - locals: LocalSymbolsCache, - hook: (Document) -> Unit = {}, -): KotlinCompilation { - val compilation = - KotlinCompilation().apply { - sources = listOf(source) - inheritClassPath = true - verbose = false - } - - val analyzer = scipVisitorAnalyzer(globals, locals, compilation.workingDir.toPath(), hook) - compilation.apply { compilerPluginRegistrars = listOf(analyzer) } - return compilation -} - -private class TestAnalyzerDeclarationCheckers( - globals: GlobalSymbolsCache, - locals: LocalSymbolsCache, - sourceRoot: Path, -) : AnalyzerCheckers.AnalyzerDeclarationCheckers(sourceRoot) { - override val fileCheckers: Set = - setOf( - object : FirFileChecker(MppCheckerKind.Common) { - context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirFile) { - val ktFile = declaration.sourceFile ?: return - val lineMap = LineMap(declaration) - val visitor = ScipVisitor(sourceRoot, ktFile, lineMap, globals, locals) - visitors[ktFile] = visitor - } - }, - AnalyzerCheckers.SemanticImportsChecker(), - ) -} - -private class TestAnalyzerCheckers(session: FirSession) : AnalyzerCheckers(session) { - override val declarationCheckers: DeclarationCheckers - get() = - TestAnalyzerDeclarationCheckers( - session.testAnalyzerParamsProvider.globals, - session.testAnalyzerParamsProvider.locals, - session.testAnalyzerParamsProvider.sourceroot, - ) -} - -class TestAnalyzerParamsProvider( - session: FirSession, - var globals: GlobalSymbolsCache, - var locals: LocalSymbolsCache, - sourceroot: Path, -) : AnalyzerParamsProvider(session, ScipOptions().apply { this.sourceroot = sourceroot }) { - companion object { - fun getFactory( - globals: GlobalSymbolsCache, - locals: LocalSymbolsCache, - sourceroot: Path, - ): Factory { - return Factory { TestAnalyzerParamsProvider(it, globals, locals, sourceroot) } - } - } -} - -val FirSession.testAnalyzerParamsProvider: TestAnalyzerParamsProvider by - FirSession.sessionComponentAccessor() - -@OptIn(ExperimentalCompilerApi::class) -fun scipVisitorAnalyzer( - globals: GlobalSymbolsCache, - locals: LocalSymbolsCache, - sourceroot: Path, - hook: (Document) -> Unit = {}, -): CompilerPluginRegistrar { - return object : CompilerPluginRegistrar() { - override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { - FirExtensionRegistrarAdapter.registerExtension( - object : FirExtensionRegistrar() { - override fun ExtensionRegistrarContext.configurePlugin() { - +TestAnalyzerParamsProvider.getFactory(globals, locals, sourceroot) - +::TestAnalyzerCheckers - } - } - ) - IrGenerationExtension.registerExtension( - PostAnalysisExtension( - sourceRoot = sourceroot, - targetRoot = Paths.get(""), - callback = hook, - ) - ) - } - - override val supportsK2: Boolean - get() = true - } -} diff --git a/scip-snapshots/README.md b/scip-snapshots/README.md index ebd30fc3d..d51cece11 100644 --- a/scip-snapshots/README.md +++ b/scip-snapshots/README.md @@ -1,7 +1,7 @@ # SCIP snapshots This directory contains end-to-end snapshot fixtures for `scip-javac`, -`scip-kotlinc`, and future mixed Java/Kotlin cases. +`scip-kotlin-analysis`, and future mixed Java/Kotlin cases. ## Layout diff --git a/scip-snapshots/cases/kotlin/common/build.gradle.kts b/scip-snapshots/cases/kotlin/common/build.gradle.kts index be5226192..e3fc46602 100644 --- a/scip-snapshots/cases/kotlin/common/build.gradle.kts +++ b/scip-snapshots/cases/kotlin/common/build.gradle.kts @@ -1,9 +1,7 @@ import org.scip_code.scip_java.buildlogic.cleanDirectoryBeforeRunning import org.scip_code.scip_java.buildlogic.publishDirectoryArtifact -import org.scip_code.scip_java.buildlogic.scipKotlincPluginArgs import org.scip_code.scip_java.buildlogic.shadowJarArtifact import org.scip_code.scip_java.buildlogic.useScipJavac -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("scip.java-library") @@ -11,14 +9,12 @@ plugins { } val javacShadowJar = shadowJarArtifact(":scip-javac", "javacShadowJar") -val kotlincShadowJar = shadowJarArtifact(":scip-kotlinc", "kotlincShadowJar") dependencies { implementation(libs.kotlin.stdlib) } -// Runtime classpath of the Analysis-API-based indexer, used by the -// scipIndexKotlinAnalysis comparison task below. +// Runtime classpath of the Analysis-API-based Kotlin indexer. val scipKotlinAnalysis: Configuration by configurations.creating { isCanBeConsumed = false } @@ -31,38 +27,21 @@ val scipTargetroot = layout.buildDirectory.dir("scip-targetroot") val sourceroot = rootProject.rootDir.absolutePath val targetroot = scipTargetroot.get().asFile.absolutePath -tasks.named("compileKotlin") { - inputs.files(kotlincShadowJar) - outputs.dir(scipTargetroot) - compilerOptions.freeCompilerArgs.addAll(scipKotlincPluginArgs(kotlincShadowJar.elements, sourceroot, targetroot)) - cleanDirectoryBeforeRunning(scipTargetroot) -} - -tasks.named("compileJava") { - useScipJavac(rootDir, javacShadowJar, scipTargetroot) - options.annotationProcessorPath = javacShadowJar - options.compilerArgs.add("-Xplugin:scip -sourceroot:$sourceroot -targetroot:$targetroot") -} - -publishDirectoryArtifact("scipTargetrootElements", scipTargetroot, tasks.named("classes")) - -// Indexes the same Kotlin sources with the standalone Analysis API indexer into a -// separate targetroot, so its SCIP output can be compared against scip-kotlinc's. -val scipAnalysisTargetroot = layout.buildDirectory.dir("scip-targetroot-analysis") - -tasks.register("scipIndexKotlinAnalysis") { +// Kotlin sources are indexed by the standalone scip-kotlin-analysis indexer; +// compileKotlin only produces the classes that compileJava needs. +val scipIndexKotlin = tasks.register("scipIndexKotlin") { classpath = scipKotlinAnalysis mainClass.set("org.scip_code.scip_java.kotlin_analysis.MainKt") val kotlinSources = layout.projectDirectory.dir("src/main/kotlin") val compileClasspath = sourceSets.main.map { it.compileClasspath } inputs.dir(kotlinSources) inputs.files(compileClasspath) - outputs.dir(scipAnalysisTargetroot) - cleanDirectoryBeforeRunning(scipAnalysisTargetroot) + outputs.dir(scipTargetroot) + cleanDirectoryBeforeRunning(scipTargetroot) // Locals only: the argument provider must not capture the build script // (configuration cache). val sourcerootArg = sourceroot - val targetrootArg = scipAnalysisTargetroot + val targetrootArg = scipTargetroot val kotlinSourcesArg = kotlinSources.asFile.absolutePath argumentProviders.add( CommandLineArgumentProvider { @@ -78,3 +57,18 @@ tasks.register("scipIndexKotlinAnalysis") { } ) } + +tasks.named("compileJava") { + useScipJavac(rootDir, javacShadowJar, scipTargetroot) + options.annotationProcessorPath = javacShadowJar + options.compilerArgs.add("-Xplugin:scip -sourceroot:$sourceroot -targetroot:$targetroot") + // The Kotlin indexer cleans the targetroot before writing its shards; javac + // must add its shards afterwards. + mustRunAfter(scipIndexKotlin) +} + +tasks.named("classes") { + dependsOn(scipIndexKotlin) +} + +publishDirectoryArtifact("scipTargetrootElements", scipTargetroot, tasks.named("classes")) diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt index a458ba724..e86954de2 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt @@ -3,7 +3,10 @@ //⌄ enclosing_range_start scip-java maven . . snapshots/Tagged# @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +// ^^^^^^ reference scip-java maven . . kotlin/annotation/Target# +// ^^^^^^^^^^^^^^^^ reference scip-java maven . . kotlin/annotation/AnnotationTarget# // ^^^^^ reference scip-java maven . . kotlin/annotation/AnnotationTarget#CLASS. +// ^^^^^^^^^^^^^^^^ reference scip-java maven . . kotlin/annotation/AnnotationTarget# // ^^^^^^^^ reference scip-java maven . . kotlin/annotation/AnnotationTarget#FUNCTION. // ⌄ enclosing_range_start scip-java maven . . snapshots/Tagged#``(). // ⌄ enclosing_range_start scip-java maven . . snapshots/Tagged#``().(tag) @@ -42,6 +45,7 @@ //⌄ enclosing_range_start scip-java maven . . snapshots/AnnotatedService# //⌄ enclosing_range_start scip-java maven . . snapshots/AnnotatedService#``(). @Tagged("service") +// ^^^^^^ reference scip-java maven . . snapshots/Tagged# class AnnotatedService { // ^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/AnnotatedService# // display_name AnnotatedService @@ -53,6 +57,7 @@ // > public constructor(): AnnotatedService // ⌄ enclosing_range_start scip-java maven . . snapshots/AnnotatedService#run(). @Tagged("run") +// ^^^^^^ reference scip-java maven . . snapshots/Tagged# fun run(): String = "running" // ^^^ definition scip-java maven . . snapshots/AnnotatedService#run(). // display_name run diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Class.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Class.kt index 7ee742752..12e88e573 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Class.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Class.kt @@ -92,6 +92,7 @@ // display_name doStuff // signature_documentation // > public final fun doStuff(): Unit +// ^^^^ reference scip-java maven . . kotlin/Unit# // ⌃ enclosing_range_end scip-java maven . . snapshots/``#doStuff(). } // ⌃ enclosing_range_end scip-java maven . . snapshots/Class#asdf. @@ -131,6 +132,7 @@ // > public final fun run(): Unit println(Class::class) // ^^^^^^^ reference scip-java maven . . kotlin/io/println(). +// ^^^^^ reference scip-java maven . . snapshots/Class# println("I eat $banana for lunch") // ^^^^^^^ reference scip-java maven . . kotlin/io/println(). // ^^^^^^ reference scip-java maven . . snapshots/Class#banana. diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/CompanionOwner.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/CompanionOwner.kt index d81175cdb..37ace2dd4 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/CompanionOwner.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/CompanionOwner.kt @@ -42,6 +42,7 @@ // signature_documentation // > public final fun create(): Int // ^^^ reference scip-java maven . . kotlin/Int# +// ^^^^^^^^^^^^^^ reference scip-java maven . . snapshots/CompanionOwner#Companion# // ^^^^^^ reference scip-java maven . . snapshots/CompanionOwner#Companion#create(). // ^^^^^^^^ reference scip-java maven . . kotlin/Any#hashCode(). // ⌃ enclosing_range_end scip-java maven . . snapshots/CompanionOwner#create(). diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt index 3361608b0..a87d36f11 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt @@ -6,13 +6,12 @@ //⌄ enclosing_range_start scip-java maven . . snapshots/Suit#valueOf(). //⌄ enclosing_range_start scip-java maven . . snapshots/Suit#valueOf().(value) //⌄ enclosing_range_start scip-java maven . . snapshots/Suit#entries. -//⌄ enclosing_range_start scip-java maven . . snapshots/getEntries(). +//⌄ enclosing_range_start scip-java maven . . snapshots/Suit#getEntries(). // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#``(). // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#``().(symbol) // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#symbol. // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#getSymbol(). enum class Suit(val symbol: Char) { -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ reference scip-java maven . . kotlin/Enum# //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ reference scip-java maven . . snapshots/Suit# // ^^^^ definition scip-java maven . . snapshots/Suit# // display_name Suit @@ -39,7 +38,7 @@ // display_name entries // signature_documentation // > public final static val entries: EnumEntries -// ^^^^ definition scip-java maven . . snapshots/getEntries(). +// ^^^^ definition scip-java maven . . snapshots/Suit#getEntries(). // display_name entries // signature_documentation // > public get(): EnumEntries @@ -61,34 +60,20 @@ // ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#symbol. // ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#getSymbol(). // ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#``(). -// ⌄ enclosing_range_start scip-java maven . . snapshots/``# -// ⌄ enclosing_range_start scip-java maven . . snapshots/``#``(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#HEARTS. HEARTS('h'), -// ^^^^^^ definition scip-java maven . . snapshots/``# -// display_name -// signature_documentation -// > object : Suit -// relationship scip-java maven . . snapshots/Suit# implementation -// ^^^^^^ definition scip-java maven . . snapshots/``#``(). +// ^^^^^^ definition scip-java maven . . snapshots/Suit#HEARTS. // display_name HEARTS // signature_documentation -// > private constructor(): -// ⌃ enclosing_range_end scip-java maven . . snapshots/``# -// ⌃ enclosing_range_end scip-java maven . . snapshots/``#``(). -// ⌄ enclosing_range_start scip-java maven . . snapshots/``# -// ⌄ enclosing_range_start scip-java maven . . snapshots/``#``(). +// > public final val HEARTS: Suit +// ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#HEARTS. +// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#SPADES. SPADES('s'); -// ^^^^^^ definition scip-java maven . . snapshots/``# -// display_name -// signature_documentation -// > object : Suit -// relationship scip-java maven . . snapshots/Suit# implementation -// ^^^^^^ definition scip-java maven . . snapshots/``#``(). +// ^^^^^^ definition scip-java maven . . snapshots/Suit#SPADES. // display_name SPADES // signature_documentation -// > private constructor(): -// ⌃ enclosing_range_end scip-java maven . . snapshots/``# -// ⌃ enclosing_range_end scip-java maven . . snapshots/``#``(). +// > public final val SPADES: Suit +// ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#SPADES. // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#isRed(). fun isRed(): Boolean = symbol == 'h' @@ -99,6 +84,7 @@ // ^^^^^^^ reference scip-java maven . . kotlin/Boolean# // ^^^^^^ reference scip-java maven . . snapshots/Suit#symbol. // ^^^^^^ reference scip-java maven . . snapshots/Suit#getSymbol(). +// ^^ reference scip-java maven . . kotlin/Char#equals(). // ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#isRed(). // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#Companion# @@ -125,9 +111,9 @@ // signature_documentation // > symbol: Char // ^^^^ reference scip-java maven . . kotlin/Char# -// ^^^^^ reference scip-java maven . . snapshots/Suit# +// ^^^^ reference scip-java maven . . snapshots/Suit# // ^^^^^^^ reference scip-java maven . . snapshots/Suit#entries. -// ^^^^^^^ reference scip-java maven . . snapshots/getEntries(). +// ^^^^^^^ reference scip-java maven . . snapshots/Suit#getEntries(). // ^^^^ reference scip-java maven . . kotlin/collections/find(+9). // ^^^^^^^^^^^^^^^^^^^^^^^ definition local 0 // display_name it @@ -136,6 +122,7 @@ // ^^ reference local 0 // ^^^^^^ reference scip-java maven . . snapshots/Suit#symbol. // ^^^^^^ reference scip-java maven . . snapshots/Suit#getSymbol(). +// ^^ reference scip-java maven . . kotlin/Char#equals(). // ^^^^^^ reference scip-java maven . . snapshots/Suit#Companion#fromSymbol().(symbol) // ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#Companion#fromSymbol().(symbol) // ⌃ enclosing_range_end scip-java maven . . snapshots/Suit#Companion#fromSymbol(). @@ -149,7 +136,7 @@ //⌃ enclosing_range_end scip-java maven . . snapshots/Suit#valueOf(). //⌃ enclosing_range_end scip-java maven . . snapshots/Suit#valueOf().(value) //⌃ enclosing_range_end scip-java maven . . snapshots/Suit#entries. -//⌃ enclosing_range_end scip-java maven . . snapshots/getEntries(). +//⌃ enclosing_range_end scip-java maven . . snapshots/Suit#getEntries(). //⌄ enclosing_range_start scip-java maven . . snapshots/describe(). // ⌄ enclosing_range_start scip-java maven . . snapshots/describe().(suit) @@ -174,8 +161,10 @@ // ^^^^ reference scip-java maven . . snapshots/describe().(suit) // ⌃ enclosing_range_end local 1 Suit.HEARTS -> "red" +// ^^^^ reference scip-java maven . . snapshots/Suit# // ^^^^^^ reference scip-java maven . . snapshots/Suit#HEARTS. Suit.SPADES -> "black" +// ^^^^ reference scip-java maven . . snapshots/Suit# // ^^^^^^ reference scip-java maven . . snapshots/Suit#SPADES. } // ⌃ enclosing_range_end scip-java maven . . snapshots/describe(). diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt index f2731f886..2d37f628a 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt @@ -17,9 +17,11 @@ // signature_documentation // > public constructor>(items: MutableList): Container // ^ definition scip-java maven . . snapshots/Container#[T] -// display_name FirTypeParameterSymbol T +// display_name T // signature_documentation // > T : Comparable +// ^^^^^^^^^^ reference scip-java maven . . kotlin/Comparable# +// ^ reference scip-java maven . . snapshots/Container#[T] // ^^^^^ definition scip-java maven . . snapshots/Container#``().(items) // display_name items // signature_documentation @@ -33,7 +35,8 @@ // display_name items // signature_documentation // > private get(): MutableList -// ^^^^^^^^^^^^^^ reference scip-java maven . . kotlin/collections/MutableList# +// ^^^^^^^^^^^ reference scip-java maven . . kotlin/collections/MutableList# +// ^ reference scip-java maven . . snapshots/Container#[T] // ⌃ enclosing_range_end scip-java maven . . snapshots/Container#[T] // ⌃ enclosing_range_end scip-java maven . . snapshots/Container#``().(items) // ⌃ enclosing_range_end scip-java maven . . snapshots/Container#items. @@ -50,7 +53,9 @@ // display_name item // signature_documentation // > item: T -// ^^^^^^^^^^^^ reference scip-java maven . . snapshots/Container# +// ^ reference scip-java maven . . snapshots/Container#[T] +// ^^^^^^^^^ reference scip-java maven . . snapshots/Container# +// ^ reference scip-java maven . . snapshots/Container#[T] // ⌃ enclosing_range_end scip-java maven . . snapshots/Container#add().(item) items.add(item) // ^^^^^ reference scip-java maven . . snapshots/Container#items. @@ -66,9 +71,10 @@ // ⌄ enclosing_range_start scip-java maven . . snapshots/Container#mapItems().(transform) fun mapItems(transform: (T) -> R?): List = items.mapNotNull(transform) // ^ definition scip-java maven . . snapshots/Container#mapItems().[R] -// display_name FirTypeParameterSymbol R +// display_name R // signature_documentation // > R : Any +// ^^^ reference scip-java maven . . kotlin/Any# // ^^^^^^^^ definition scip-java maven . . snapshots/Container#mapItems(). // display_name mapItems // signature_documentation @@ -77,8 +83,10 @@ // display_name transform // signature_documentation // > transform: (T) -> R? -// ^^^^^^^^^ reference scip-java maven . . kotlin/Function1# -// ^^^^^^^ reference scip-java maven . . kotlin/collections/List# +// ^ reference scip-java maven . . snapshots/Container#[T] +// ^ reference scip-java maven . . snapshots/Container#mapItems().[R] +// ^^^^ reference scip-java maven . . kotlin/collections/List# +// ^ reference scip-java maven . . snapshots/Container#mapItems().[R] // ^^^^^ reference scip-java maven . . snapshots/Container#items. // ^^^^^ reference scip-java maven . . snapshots/Container#getItems(). // ^^^^^^^^^^ reference scip-java maven . . kotlin/collections/mapNotNull(+1). @@ -95,7 +103,7 @@ // ⌄ enclosing_range_start scip-java maven . . snapshots/firstOrSelf().(fallback) fun firstOrSelf(items: List, fallback: T): T = items.firstOrNull() ?: fallback // ^ definition scip-java maven . . snapshots/firstOrSelf().[T] -// display_name FirTypeParameterSymbol T +// display_name T // signature_documentation // > T // ^^^^^^^^^^^ definition scip-java maven . . snapshots/firstOrSelf(). @@ -106,11 +114,14 @@ // display_name items // signature_documentation // > items: List -// ^^^^^^^ reference scip-java maven . . kotlin/collections/List# +// ^^^^ reference scip-java maven . . kotlin/collections/List# +// ^ reference scip-java maven . . snapshots/firstOrSelf().[T] // ^^^^^^^^ definition scip-java maven . . snapshots/firstOrSelf().(fallback) // display_name fallback // signature_documentation // > fallback: T +// ^ reference scip-java maven . . snapshots/firstOrSelf().[T] +// ^ reference scip-java maven . . snapshots/firstOrSelf().[T] // ^^^^^ reference scip-java maven . . snapshots/firstOrSelf().(items) // ^^^^^^^^^^^ reference scip-java maven . . kotlin/collections/firstOrNull(+19). // ^^^^^^^^ reference scip-java maven . . snapshots/firstOrSelf().(fallback) @@ -122,10 +133,12 @@ //⌄ enclosing_range_start scip-java maven . . snapshots/StringContainer# typealias StringContainer = Container // ^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/StringContainer# -// display_name FirTypeAliasSymbol snapshots/StringContainer +// display_name StringContainer // signature_documentation // > public final typealias StringContainer = Container // > +// ^^^^^^^^^ reference scip-java maven . . snapshots/Container# +// ^^^^^^ reference scip-java maven . . kotlin/String# // ⌃ enclosing_range_end scip-java maven . . snapshots/StringContainer# //⌄ enclosing_range_start scip-java maven . . snapshots/useContainer(). @@ -134,11 +147,11 @@ // ^^^^^^^^^^^^ definition scip-java maven . . snapshots/useContainer(). // display_name useContainer // signature_documentation -// > public final fun useContainer(container: {snapshots/StringContainer=} Container): {snapshots/StringContainer=} Container +// > public final fun useContainer(container: StringContainer): StringContainer // ^^^^^^^^^ definition scip-java maven . . snapshots/useContainer().(container) // display_name container // signature_documentation -// > container: {snapshots/StringContainer=} Container +// > container: StringContainer // ^^^^^^^^^^^^^^^ reference scip-java maven . . snapshots/Container# // ^^^^^^^^^^^^^^^ reference scip-java maven . . snapshots/Container# // ^^^^^^^^^ reference scip-java maven . . snapshots/useContainer().(container) diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Implementations.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Implementations.kt index c9c88d892..f0c4f2d03 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Implementations.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Implementations.kt @@ -7,7 +7,7 @@ // ^^^^^^^^^ definition scip-java maven . . snapshots/Overrides# // display_name Overrides // signature_documentation -// > public final class Overrides : {kotlin/AutoCloseable=} AutoCloseable +// > public final class Overrides : AutoCloseable // relationship scip-java maven jdk 17 java/lang/AutoCloseable# implementation // ^^^^^^^^^ definition scip-java maven . . snapshots/Overrides#``(). // display_name Overrides diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Lambdas.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Lambdas.kt index 0d80eb5bc..5266b8d31 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Lambdas.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Lambdas.kt @@ -15,6 +15,7 @@ // signature_documentation // > public get(): Unit // ^^^^^^^^^^^ reference scip-java maven . . kotlin/collections/arrayListOf(). +// ^^^^^^ reference scip-java maven . . kotlin/String# // ^^^^^^^^^^^^^^ reference scip-java maven . . kotlin/collections/forEachIndexed(+9). // ^ definition local 0 // display_name i diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/ObjectKt.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/ObjectKt.kt index 3b803c7c3..8de86a9f3 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/ObjectKt.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/ObjectKt.kt @@ -28,7 +28,7 @@ // display_name message // signature_documentation // > message: String? -// ^^^^^^^ reference scip-java maven . . kotlin/String# +// ^^^^^^ reference scip-java maven . . kotlin/String# // ^^^^^^^ reference scip-java maven . . kotlin/Nothing# // ⌃ enclosing_range_end scip-java maven . . snapshots/ObjectKt#fail().(message) throw RuntimeException(message) diff --git a/settings.gradle.kts b/settings.gradle.kts index 6b12f3a5b..785b0b434 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,7 +23,6 @@ rootProject.name = "scip-java" include( "scip-shared", "scip-javac", - "scip-kotlinc", "scip-kotlin-analysis", "scip-aggregator", "scip-maven-plugin", From 7e1d25e21991c900b2265012879d31dc47bf9432 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 12:55:50 +0200 Subject: [PATCH 07/15] Update Kotlin to 2.4.0 and Analysis API engine to 2.4.0 --- .github/renovate.json | 13 ++++++++++++- gradle/libs.versions.toml | 21 ++++++++++++--------- scip-kotlin-analysis/build.gradle.kts | 3 +++ settings.gradle.kts | 10 +++++++--- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index ae9a1a811..fc3794e54 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -3,5 +3,16 @@ "extends": [ "config:recommended" ], - "semanticCommits": false + "semanticCommits": false, + "packageRules": [ + { + "description": "The Analysis API engine pin is managed manually: it lives in the JetBrains intellij-dependencies repository, uses IDE-aligned version schemes, and must move in lockstep with the `kotlin` toolchain version.", + "matchPackagePatterns": [ + "^org\\.jetbrains\\.kotlin:kotlin-compiler$", + "-for-ide$", + "^com\\.intellij\\.platform:" + ], + "enabled": false + } + ] } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6a26038e0..69b1fbd55 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,15 +2,17 @@ clikt = "5.1.0" gradle-api = "8.11.1" junit-jupiter = "5.11.4" -kotlin = "2.2.0" -# Kotlin Analysis API engine bundled by scip-kotlin-analysis. Independent from -# the `kotlin` toolchain version: `-for-ide` artifacts are only published as -# IDE-aligned builds (see https://redirector.kotlinlang.org/maven/kotlin-ide-plugin-dependencies). -kotlin-analysis-api = "2.2.0-ij251-98" +kotlin = "2.4.0" +# Kotlin Analysis API engine bundled by scip-kotlin-analysis. Managed manually +# (see .github/renovate.json): the artifacts live in the JetBrains +# intellij-dependencies repository and the pin must stay <= one minor version +# ahead of `kotlin` so the toolchain can read its metadata. +kotlin-analysis-api = "2.4.0" kotlinx-collections-immutable = "0.3.8" -# Matches the coroutines dependency declared by the kotlin-compiler POM at the -# kotlin-analysis-api version above. -kotlinx-coroutines = "1.8.0" +# The IntelliJ fork of kotlinx-coroutines required by the Analysis API engine +# (provides kotlinx.coroutines.internal.intellij.IntellijCoroutines). Managed +# manually together with kotlin-analysis-api. +kotlinx-coroutines-intellij = "1.8.0-intellij-13" kotlinx-serialization = "1.11.0" lombok = "1.18.46" maven-plugin-annotations = "3.15.2" @@ -34,13 +36,14 @@ kotlin-analysis-api-k2 = { module = "org.jetbrains.kotlin:analysis-api-k2-for-id kotlin-analysis-api-platform-interface = { module = "org.jetbrains.kotlin:analysis-api-platform-interface-for-ide", version.ref = "kotlin-analysis-api" } kotlin-analysis-api-standalone = { module = "org.jetbrains.kotlin:analysis-api-standalone-for-ide", version.ref = "kotlin-analysis-api" } kotlin-analysis-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler", version.ref = "kotlin-analysis-api" } +kotlin-analysis-compiler-common = { module = "org.jetbrains.kotlin:kotlin-compiler-common-for-ide", version.ref = "kotlin-analysis-api" } kotlin-analysis-low-level-api-fir = { module = "org.jetbrains.kotlin:low-level-api-fir-for-ide", version.ref = "kotlin-analysis-api" } kotlin-analysis-symbol-light-classes = { module = "org.jetbrains.kotlin:symbol-light-classes-for-ide", version.ref = "kotlin-analysis-api" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" } kotlinx-collections-immutable-jvm = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm", version.ref = "kotlinx-collections-immutable" } -kotlinx-coroutines-core-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-core-jvm = { module = "com.intellij.platform:kotlinx-coroutines-core-jvm", version.ref = "kotlinx-coroutines-intellij" } kotlinx-serialization-json-jvm = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version.ref = "kotlinx-serialization" } lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" } maven-plugin-annotations = { module = "org.apache.maven.plugin-tools:maven-plugin-annotations", version.ref = "maven-plugin-annotations" } diff --git a/scip-kotlin-analysis/build.gradle.kts b/scip-kotlin-analysis/build.gradle.kts index 59adaf598..f500d908c 100644 --- a/scip-kotlin-analysis/build.gradle.kts +++ b/scip-kotlin-analysis/build.gradle.kts @@ -19,6 +19,9 @@ dependencies { runtimeOnly(libs.kotlin.analysis.api.impl.base) { isTransitive = false } runtimeOnly(libs.kotlin.analysis.api.k2) { isTransitive = false } + // Provides org.jetbrains.kotlin.analysis.decompiler.* which is no longer + // bundled in kotlin-compiler as of the 2.4 line. + runtimeOnly(libs.kotlin.analysis.compiler.common) { isTransitive = false } runtimeOnly(libs.kotlin.analysis.low.level.api.fir) { isTransitive = false } runtimeOnly(libs.kotlin.analysis.api.platform.`interface`) { isTransitive = false } runtimeOnly(libs.kotlin.analysis.symbol.light.classes) { isTransitive = false } diff --git a/settings.gradle.kts b/settings.gradle.kts index 785b0b434..6747622d7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,9 +11,13 @@ dependencyResolutionManagement { repositories { mavenCentral() gradlePluginPortal() - // Kotlin Analysis API (`*-for-ide`) artifacts used by scip-kotlin-analysis. - maven("https://redirector.kotlinlang.org/maven/kotlin-ide-plugin-dependencies") { - content { includeGroupAndSubgroups("org.jetbrains.kotlin") } + // Kotlin Analysis API (`*-for-ide`) artifacts and the IntelliJ coroutines + // fork used by scip-kotlin-analysis. + maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") { + content { + includeGroupAndSubgroups("org.jetbrains.kotlin") + includeGroup("com.intellij.platform") + } } } } From db737938b1cc185a2ea947a9358a6ea254cea353 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 13:03:28 +0200 Subject: [PATCH 08/15] Use current renovate matchPackageNames syntax --- .github/renovate.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index fc3794e54..fad67a28d 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -7,10 +7,10 @@ "packageRules": [ { "description": "The Analysis API engine pin is managed manually: it lives in the JetBrains intellij-dependencies repository, uses IDE-aligned version schemes, and must move in lockstep with the `kotlin` toolchain version.", - "matchPackagePatterns": [ - "^org\\.jetbrains\\.kotlin:kotlin-compiler$", - "-for-ide$", - "^com\\.intellij\\.platform:" + "matchPackageNames": [ + "org.jetbrains.kotlin:kotlin-compiler", + "*-for-ide", + "com.intellij.platform:*" ], "enabled": false } From 0c68574bc6511d610af13baf279e2b4e41f79185 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 13:19:42 +0200 Subject: [PATCH 09/15] Make primary constructor enclosing ranges contain their anchor --- .../scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt | 6 +++++- .../kotlin/common/src/main/kotlin/snapshots/Annotations.kt | 2 +- .../common/src/main/kotlin/snapshots/Destructuring.kt | 2 +- .../cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt | 2 +- .../kotlin/common/src/main/kotlin/snapshots/Generics.kt | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt index 66f2f335b..a16a3f1ae 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt @@ -270,7 +270,11 @@ class KotlinAnalysisIndexer( emitDefinition( symbol = cache.symbolForDeclaration(primaryConstructor), range = range, - enclosing = primaryConstructor.textRange, + // Span from the class identifier (the definition anchor) through the + // parameter list, so the enclosing range contains its own anchor — + // scip-kotlinc used the parameter list alone, which did not. + enclosing = + TextRange(range.startOffset, primaryConstructor.textRange.endOffset), displayName = displayName, signatureText = signatures.constructorSignature(primaryConstructor), documentation = docComment(primaryConstructor), diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt index e86954de2..53d3ba257 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt @@ -8,7 +8,7 @@ // ^^^^^ reference scip-java maven . . kotlin/annotation/AnnotationTarget#CLASS. // ^^^^^^^^^^^^^^^^ reference scip-java maven . . kotlin/annotation/AnnotationTarget# // ^^^^^^^^ reference scip-java maven . . kotlin/annotation/AnnotationTarget#FUNCTION. -// ⌄ enclosing_range_start scip-java maven . . snapshots/Tagged#``(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Tagged#``(). // ⌄ enclosing_range_start scip-java maven . . snapshots/Tagged#``().(tag) // ⌄ enclosing_range_start scip-java maven . . snapshots/Tagged#tag. // ⌄ enclosing_range_start scip-java maven . . snapshots/Tagged#getTag(). diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt index 56bc9c232..3c3f8f453 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Destructuring.kt @@ -2,7 +2,7 @@ // ^^^^^^^^^ reference scip-java maven . . snapshots/ //⌄ enclosing_range_start scip-java maven . . snapshots/Point# -// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#``(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Point#``(). // ⌄ enclosing_range_start scip-java maven . . snapshots/Point#copy(). // ⌄ enclosing_range_start scip-java maven . . snapshots/Point#``().(x) // ⌄ enclosing_range_start scip-java maven . . snapshots/Point#x. diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt index a87d36f11..b16f4a7ba 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt @@ -7,7 +7,7 @@ //⌄ enclosing_range_start scip-java maven . . snapshots/Suit#valueOf().(value) //⌄ enclosing_range_start scip-java maven . . snapshots/Suit#entries. //⌄ enclosing_range_start scip-java maven . . snapshots/Suit#getEntries(). -// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#``(). +// ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#``(). // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#``().(symbol) // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#symbol. // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#getSymbol(). diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt index 2d37f628a..921285395 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt @@ -2,8 +2,8 @@ // ^^^^^^^^^ reference scip-java maven . . snapshots/ //⌄ enclosing_range_start scip-java maven . . snapshots/Container# +// ⌄ enclosing_range_start scip-java maven . . snapshots/Container#``(). // ⌄ enclosing_range_start scip-java maven . . snapshots/Container#[T] -// ⌄ enclosing_range_start scip-java maven . . snapshots/Container#``(). // ⌄ enclosing_range_start scip-java maven . . snapshots/Container#``().(items) // ⌄ enclosing_range_start scip-java maven . . snapshots/Container#items. // ⌄ enclosing_range_start scip-java maven . . snapshots/Container#getItems(). From ff0396262fa058ed0303130d4019c8753dce1056 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 13:40:17 +0200 Subject: [PATCH 10/15] Tolerate FAIL_ON_PROJECT_REPOS in ScipGradlePlugin --- .../scip_code/scip_java/gradle/ScipGradlePlugin.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java b/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java index bf792bc7c..b915cf4ce 100644 --- a/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java +++ b/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java @@ -20,8 +20,16 @@ private void configureProject(Project project) { // Inject Maven Central/local so the indexer (and plugins like protobuf that // resolve their own artifacts) can resolve dependencies even when the build // being indexed doesn't declare any repositories of its own. - project.getRepositories().add(project.getRepositories().mavenCentral()); - project.getRepositories().add(project.getRepositories().mavenLocal()); + try { + project.getRepositories().add(project.getRepositories().mavenCentral()); + project.getRepositories().add(project.getRepositories().mavenLocal()); + } catch (Exception exc) { + // The build uses RepositoriesMode.FAIL_ON_PROJECT_REPOS, which rejects + // project-level repositories at the point they are added. Repositories + // are then declared in settings via dependencyResolutionManagement, so + // the injection isn't needed in the first place. + project.getLogger().debug("scip-java: not injecting repositories ({})", exc.getMessage()); + } Map extraProperties = project.getExtensions().getExtraProperties().getProperties(); From a1e077ad1368dc0ce5f141722d2a3bf9c029bd46 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 13:40:17 +0200 Subject: [PATCH 11/15] Add Kotlin 1.9/2.3/2.4 repos to indexing workflow --- .github/workflows/repos.yaml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/workflows/repos.yaml b/.github/workflows/repos.yaml index 094963d12..19b1d55e5 100644 --- a/.github/workflows/repos.yaml +++ b/.github/workflows/repos.yaml @@ -119,6 +119,39 @@ jobs: expected_language: kotlin covers: Gradle, Kotlin multiplatform JVM target, Kotlin 2.2.0, Java 17 runtime + - name: gradle-kotlin19-kotlinpoet + repository: square/kotlinpoet + ref: 1.16.0 + directory: . + build_tool: gradle + build_command: ":kotlinpoet:compileKotlinJvm" + java: 17 + bazel_version: "" + expected_language: kotlin + covers: Gradle, Kotlin multiplatform JVM target, legacy Kotlin 1.9.22, Java 17 runtime + + - name: gradle-kotlin23-turbine + repository: cashapp/turbine + ref: ab1e9a0acbbbb175081a0c5d94cb4f4cf4da444a + directory: . + build_tool: gradle + build_command: "compileKotlinJvm" + java: 17 + bazel_version: "" + expected_language: kotlin + covers: Gradle, Kotlin multiplatform JVM target, Kotlin 2.3.21, Java 17 runtime + + - name: gradle-kotlin24-detekt + repository: detekt/detekt + ref: 04f97401d462e899383156b25d026b7b8c181401 + directory: . + build_tool: gradle + build_command: ":detekt-api:compileKotlin" + java: 21 + bazel_version: "" + expected_language: kotlin + covers: Gradle, Kotlin JVM, Kotlin 2.4.0, settings repositories (FAIL_ON_PROJECT_REPOS), Java 21 runtime + - name: gradle-mixed-okio-jmh repository: square/okio ref: parent-3.16.0 From 85be5996efe8133a3b23a06006f0b3a02c6ac529 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 14:21:43 +0200 Subject: [PATCH 12/15] Remove comments from libs.versions.toml --- gradle/libs.versions.toml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69b1fbd55..21b9685c5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,15 +3,8 @@ clikt = "5.1.0" gradle-api = "8.11.1" junit-jupiter = "5.11.4" kotlin = "2.4.0" -# Kotlin Analysis API engine bundled by scip-kotlin-analysis. Managed manually -# (see .github/renovate.json): the artifacts live in the JetBrains -# intellij-dependencies repository and the pin must stay <= one minor version -# ahead of `kotlin` so the toolchain can read its metadata. kotlin-analysis-api = "2.4.0" kotlinx-collections-immutable = "0.3.8" -# The IntelliJ fork of kotlinx-coroutines required by the Analysis API engine -# (provides kotlinx.coroutines.internal.intellij.IntellijCoroutines). Managed -# manually together with kotlin-analysis-api. kotlinx-coroutines-intellij = "1.8.0-intellij-13" kotlinx-serialization = "1.11.0" lombok = "1.18.46" From 84b7463f8070f7bd5c40d15607e20b0f302d8cd3 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 14:28:03 +0200 Subject: [PATCH 13/15] Import types instead of fully qualifying in ScipGradlePlugin --- .../scip_java/gradle/ScipGradlePlugin.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java b/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java index b915cf4ce..5e1a218ef 100644 --- a/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java +++ b/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java @@ -1,5 +1,9 @@ package org.scip_code.scip_java.gradle; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -7,6 +11,7 @@ import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; +import org.gradle.api.file.FileCollection; import org.gradle.api.tasks.compile.JavaCompile; public class ScipGradlePlugin implements Plugin { @@ -179,22 +184,18 @@ private static void configureKotlinCompileTask(Task task, String sourceRoot, Str // Referring to KotlinCompile directly triggers NoClassDefFoundError - // the plugin classpath is murky and we deliberately don't bundle the // Kotlin Gradle plugin. So we commit the sins of reflection. - org.gradle.api.file.FileCollection libraries; + FileCollection libraries; try { - libraries = - (org.gradle.api.file.FileCollection) - task.getClass().getMethod("getLibraries").invoke(task); + libraries = (FileCollection) task.getClass().getMethod("getLibraries").invoke(task); } catch (ReflectiveOperationException exc) { - libraries = - (org.gradle.api.file.FileCollection) - task.getClass().getMethod("getClasspath").invoke(task); + libraries = (FileCollection) task.getClass().getMethod("getClasspath").invoke(task); } - for (java.io.File entry : libraries.getFiles()) { + for (File entry : libraries.getFiles()) { lines.add("classpath " + entry.getAbsolutePath()); } int sources = 0; - for (java.io.File file : task.getInputs().getSourceFiles().getFiles()) { + for (File file : task.getInputs().getSourceFiles().getFiles()) { if (file.getName().endsWith(".kt") || file.getName().endsWith(".kts")) { lines.add("source " + file.getAbsolutePath()); sources++; @@ -204,10 +205,10 @@ private static void configureKotlinCompileTask(Task task, String sourceRoot, Str return; } - java.nio.file.Path configDir = java.nio.file.Paths.get(targetRoot, "kotlin-configs"); - java.nio.file.Files.createDirectories(configDir); + Path configDir = Paths.get(targetRoot, "kotlin-configs"); + Files.createDirectories(configDir); String fileName = task.getPath().replaceAll("[^A-Za-z0-9]", "-") + ".txt"; - java.nio.file.Files.write(configDir.resolve(fileName), lines); + Files.write(configDir.resolve(fileName), lines); } catch (Exception exc) { throw new RuntimeException( "scip-java: failed to extract Kotlin compile configuration for task '" From 5ee2f24c749a19142508990e9f336a60aad7f383 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 14:44:25 +0200 Subject: [PATCH 14/15] Trim comments and tighten new indexer code --- .../scip_java/gradle/ScipGradlePlugin.java | 15 +- .../org/scip_code/scip_java/ScipJava.kt | 6 +- .../scip_java/buildtools/GradleBuildTool.kt | 6 +- .../scip_java/buildtools/ScipBuildTool.kt | 6 +- .../kotlin_analysis/KotlinAnalysisIndexer.kt | 146 +++++++----------- .../scip_java/kotlin_analysis/ScipSymbols.kt | 16 +- .../kotlin_analysis/SignatureRenderer.kt | 13 +- .../scip_java/kotlin_analysis/SymbolsCache.kt | 67 ++++---- 8 files changed, 113 insertions(+), 162 deletions(-) diff --git a/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java b/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java index 5e1a218ef..927d2f072 100644 --- a/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java +++ b/scip-gradle-plugin/src/main/java/org/scip_code/scip_java/gradle/ScipGradlePlugin.java @@ -29,10 +29,9 @@ private void configureProject(Project project) { project.getRepositories().add(project.getRepositories().mavenCentral()); project.getRepositories().add(project.getRepositories().mavenLocal()); } catch (Exception exc) { - // The build uses RepositoriesMode.FAIL_ON_PROJECT_REPOS, which rejects - // project-level repositories at the point they are added. Repositories - // are then declared in settings via dependencyResolutionManagement, so - // the injection isn't needed in the first place. + // RepositoriesMode.FAIL_ON_PROJECT_REPOS rejects project-level repositories + // as they are added; the build declares them in settings instead, so the + // injection isn't needed. project.getLogger().debug("scip-java: not injecting repositories ({})", exc.getMessage()); } @@ -164,11 +163,9 @@ private static boolean tryAddJavacPlugin(Project project, Object javacPluginDep) } /** - * Kotlin sources are no longer indexed inside kotlinc: after each Kotlin compile task runs, its - * sources and classpath are dumped to {@code /kotlin-configs/.txt}, and the - * scip-java CLI indexes them with the standalone Analysis API indexer after the build. This - * removes any binary-compatibility coupling with the Kotlin compiler version of the build being - * indexed. + * Kotlin sources are not indexed inside kotlinc: after each Kotlin compile task runs, its sources + * and classpath are dumped to {@code /kotlin-configs/.txt} for the scip-java + * CLI to index after the build, decoupling indexing from the build's Kotlin compiler version. */ private static void configureKotlinCompileTask(Task task, String sourceRoot, String targetRoot) { if (!task.getClass().getSimpleName().contains("KotlinCompile")) { diff --git a/scip-java/src/main/kotlin/org/scip_code/scip_java/ScipJava.kt b/scip-java/src/main/kotlin/org/scip_code/scip_java/ScipJava.kt index 47b535700..2e6ddbde6 100644 --- a/scip-java/src/main/kotlin/org/scip_code/scip_java/ScipJava.kt +++ b/scip-java/src/main/kotlin/org/scip_code/scip_java/ScipJava.kt @@ -1,6 +1,7 @@ package org.scip_code.scip_java import java.io.PrintStream +import kotlin.system.exitProcess /** * Public entry point for the scip-java CLI. The single [app] instance is shared across test suites @@ -17,9 +18,8 @@ object ScipJava { @JvmStatic fun main(args: Array) { app.runAndExitIfNonZero(args.toList()) - // The Kotlin Analysis API indexer leaves non-daemon threads behind; exit - // explicitly instead of waiting on them. - kotlin.system.exitProcess(0) + // The Analysis API indexer leaves non-daemon threads behind; exit explicitly. + exitProcess(0) } fun printHelp(out: PrintStream) { diff --git a/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/GradleBuildTool.kt b/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/GradleBuildTool.kt index fee71085b..432c17af9 100644 --- a/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/GradleBuildTool.kt +++ b/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/GradleBuildTool.kt @@ -100,10 +100,8 @@ This means our SCIP compiler plugin was not attached to one or more JavaCompile } /** - * The Gradle plugin dumps each Kotlin compile task's sources and classpath under - * `/kotlin-configs/`; index them with the standalone Analysis API indexer. The - * Kotlin version of the indexed build is irrelevant here — the indexer bundles its own analysis - * engine. + * Indexes the sources/classpath dumps the Gradle plugin wrote to `/kotlin-configs/` + * after each Kotlin compile task. */ private fun indexKotlinConfigs() { val configsDir = targetroot().resolve("kotlin-configs") diff --git a/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/ScipBuildTool.kt b/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/ScipBuildTool.kt index 5b8844cde..cf3fe7650 100644 --- a/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/ScipBuildTool.kt +++ b/scip-java/src/main/kotlin/org/scip_code/scip_java/buildtools/ScipBuildTool.kt @@ -153,9 +153,9 @@ class ScipBuildTool(index: IndexCommand) : BuildTool("SCIP", index) { val classpath = config.classpath.map { sourceroot.resolve(it).normalize() } val jdkHome = config.javaHome?.let { Paths.get(it) } ?: Paths.get(System.getProperty("java.home")) - // Unlike the old scip-kotlinc compiler plugin, the Analysis API indexer does - // not compile the sources: unresolved code degrades individual occurrences - // instead of failing the build, so only indexer crashes surface as errors. + // The Analysis API indexer does not compile the sources: unresolved code + // degrades individual occurrences instead of failing the build, so only + // indexer crashes surface as errors. return try { KotlinAnalysisIndexer( sourceroot = sourceroot, diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt index a16a3f1ae..73e8f270e 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt @@ -4,8 +4,11 @@ import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.TextRange import java.nio.file.Path import java.nio.file.Paths +import java.util.Arrays +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.KaSession import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource import org.jetbrains.kotlin.analysis.api.standalone.buildStandaloneAnalysisAPISession import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol @@ -17,18 +20,23 @@ import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtLibraryMod import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSdkModule import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSourceModule import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.jetbrains.kotlin.psi.KtCatchClause import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtClassOrObject import org.jetbrains.kotlin.psi.KtConstructor import org.jetbrains.kotlin.psi.KtConstructorCalleeExpression import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtDestructuringDeclaration import org.jetbrains.kotlin.psi.KtDestructuringDeclarationEntry import org.jetbrains.kotlin.psi.KtEnumEntry +import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtForExpression import org.jetbrains.kotlin.psi.KtFunctionLiteral import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.KtObjectDeclaration @@ -43,8 +51,10 @@ import org.jetbrains.kotlin.psi.KtTreeVisitorVoid import org.jetbrains.kotlin.psi.KtTypeAlias import org.jetbrains.kotlin.psi.KtTypeParameter import org.jetbrains.kotlin.psi.KtVariableDeclaration +import org.jetbrains.kotlin.psi.KtWhenExpression import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject import org.jetbrains.kotlin.psi.psiUtil.parents +import org.jetbrains.kotlin.types.Variance import org.scip_code.scip.Document import org.scip_code.scip.Occurrence import org.scip_code.scip.SymbolRole @@ -59,13 +69,12 @@ import org.scip_code.scip_java.shared.ScipShardWriter /** * Indexes Kotlin sources into SCIP documents using the Kotlin Analysis API in standalone mode. * - * Unlike scip-kotlinc, this indexer does not run inside the user's kotlinc process: it bundles its - * own analysis engine, so the Kotlin version of the indexed project only matters as a - * language-version setting, never as a binary compatibility constraint. + * The indexer bundles its own analysis engine, so the Kotlin version of the indexed project only + * matters as a language-version setting, never as a binary-compatibility constraint. * * One SCIP shard is written per source file under - * `/META-INF/scip/.scip`, matching the layout produced by the - * scip-javac and scip-kotlinc compiler plugins. + * `/META-INF/scip/.scip`, the layout produced by the scip-javac + * compiler plugin. */ class KotlinAnalysisIndexer( private val sourceroot: Path, @@ -150,7 +159,7 @@ class KotlinAnalysisIndexer( } } - @OptIn(org.jetbrains.kotlin.analysis.api.KaExperimentalApi::class) + @OptIn(KaExperimentalApi::class) private class IndexingVisitor( private val session: KaSession, private val cache: SymbolsCache, @@ -160,9 +169,8 @@ class KotlinAnalysisIndexer( private val signatures = SignatureRenderer(session) override fun visitDeclaration(dcl: KtDeclaration) { - // Definitions are emitted before visiting children so that same-range - // occurrence order and local-symbol numbering match scip-kotlinc, - // which processed declarations before their bodies. + // Emit before visiting children so same-range occurrence order and + // local-symbol numbering match scip-kotlinc. when (dcl) { is KtEnumEntry -> emitEnumEntry(dcl) is KtClassOrObject -> emitClassLike(dcl) @@ -186,9 +194,6 @@ class KotlinAnalysisIndexer( val target = with(session) { expression.mainReference.resolveToSymbol() } ?: return val range = expression.getReferencedNameElement().textRange - // The implicit `it` lambda parameter has no declaration PSI; its - // definition is anchored at the whole lambda literal, emitted at the - // first reference. val targetPsi = target.psi if ( targetPsi is KtFunctionLiteral && @@ -212,9 +217,8 @@ class KotlinAnalysisIndexer( return } - // Property references fan out to the property and its accessors, - // e.g. `banana` yields `Class#banana.`, `Class#getBanana().` and - // `Class#setBanana().`. + // Property references fan out to the property and its accessors + // (`banana` → `Class#banana.`, `Class#getBanana().`, `Class#setBanana().`). if (target is KaPropertySymbol) { propertyReferenceSymbols(target).forEach { addReference(it, range) } return @@ -223,18 +227,13 @@ class KotlinAnalysisIndexer( addReference(cache.symbolForReference(session, target), range) } - // --------------------------------------------------------------- - // Declarations - // --------------------------------------------------------------- - private fun emitClassLike(declaration: KtClassOrObject) { val isAnonymous = declaration is KtObjectDeclaration && declaration.isObjectLiteral() val range = when { declaration.nameIdentifier != null -> declaration.nameIdentifier!!.textRange isAnonymous -> - (declaration as KtObjectDeclaration).getObjectKeyword()?.textRange - ?: declaration.textRange + declaration.getObjectKeyword()?.textRange ?: declaration.textRange else -> declaration.textRange // unnamed companion object } val displayName = if (isAnonymous) "" else declaration.name ?: "Companion" @@ -248,11 +247,10 @@ class KotlinAnalysisIndexer( documentation = docComment(declaration), relationships = superTypeRelationships(declaration), ) - // Classes and objects (but not interfaces) declare an implicit primary - // constructor when no explicit one is present in source. A primary - // constructor without the `constructor` keyword also anchors at the class - // identifier and is emitted here so the class definition stays first at - // that range (matching scip-kotlinc's order); emitConstructor skips it. + // Classes and objects (not interfaces) get an implicit primary constructor + // when none is present. A keyword-less primary constructor also anchors at + // the class identifier; both are emitted here so the class definition stays + // first at that range (emitConstructor skips the keyword-less case). val isInterface = declaration is KtClass && declaration.isInterface() val primaryConstructor = declaration.primaryConstructor if (!isInterface && primaryConstructor == null) { @@ -270,9 +268,9 @@ class KotlinAnalysisIndexer( emitDefinition( symbol = cache.symbolForDeclaration(primaryConstructor), range = range, - // Span from the class identifier (the definition anchor) through the - // parameter list, so the enclosing range contains its own anchor — - // scip-kotlinc used the parameter list alone, which did not. + // Span from the definition anchor through the parameter list so the + // enclosing range contains its own anchor (scip-kotlinc's + // parameter-list-only range did not). enclosing = TextRange(range.startOffset, primaryConstructor.textRange.endOffset), displayName = displayName, @@ -298,10 +296,9 @@ class KotlinAnalysisIndexer( } /** - * Compiler-generated enum members: `values()`, `valueOf(value)` and `entries` (plus its - * getter), all anchored at the enum class identifier like in scip-kotlinc. Unlike - * scip-kotlinc, the entries getter is owned by the enum class instead of the package — the - * old symbol (`snapshots/getEntries().`) was a bug. + * Compiler-generated enum members — `values()`, `valueOf(value)`, `entries` and its getter + * — anchored at the enum class identifier. Unlike scip-kotlinc, the entries getter is owned + * by the enum class; the old package-owned symbol was a bug. */ private fun emitEnumSynthetics(declaration: KtClass, range: TextRange) { val name = declaration.name.orEmpty() @@ -383,7 +380,7 @@ class KotlinAnalysisIndexer( private fun emitFunction(declaration: KtNamedFunction) { val identifier = declaration.nameIdentifier ?: return val relationships = - if (declaration.hasModifier(org.jetbrains.kotlin.lexer.KtTokens.OVERRIDE_KEYWORD)) { + if (declaration.hasModifier(KtTokens.OVERRIDE_KEYWORD)) { overriddenSymbols(declaration) } else { emptyList() @@ -435,10 +432,9 @@ class KotlinAnalysisIndexer( private fun emitParameter(declaration: KtParameter) { val loopOrCatchIdentifier = declaration.nameIdentifier // `for` loop variables and `catch` parameters are local declarations, - // mirroring the FIR indexer (which also assigned their local numbers at - // the declaration site). + // numbered at the declaration site like in the FIR indexer. if (loopOrCatchIdentifier != null) { - if (declaration.parent is org.jetbrains.kotlin.psi.KtForExpression) { + if (declaration.parent is KtForExpression) { val type = declaration.typeReference?.text ?: renderedParameterType(declaration) @@ -453,10 +449,7 @@ class KotlinAnalysisIndexer( ) return } - if ( - (declaration.parent as? KtParameterList)?.parent - is org.jetbrains.kotlin.psi.KtCatchClause - ) { + if ((declaration.parent as? KtParameterList)?.parent is KtCatchClause) { emitDefinition( symbol = cache.symbolForDeclaration(declaration), range = loopOrCatchIdentifier.textRange, @@ -480,9 +473,9 @@ class KotlinAnalysisIndexer( signatureText = signatures.parameterSignature(declaration), documentation = null, ) - // A `val`/`var` constructor parameter also declares a property: the - // property initializer references the parameter at the same range, and - // the property plus its synthetic accessors are all anchored there. + // A `val`/`var` constructor parameter also declares a property; it and + // its synthetic accessors are all anchored at the parameter identifier, + // which the property initializer also references. if (declaration.hasValOrVar()) { emitDefinition( symbol = cache.parameterPropertySymbol(declaration), @@ -557,11 +550,11 @@ class KotlinAnalysisIndexer( } /** - * `when (subject)` introduces a synthetic `` local in the FIR desugaring; - * scip-kotlinc emitted a definition for it at the subject expression. Emitted before - * visiting children so local numbering matches. + * `when (subject)` introduces a synthetic `` local in the FIR desugaring, + * defined at the subject expression. Emitted before visiting children so local numbering + * matches. */ - override fun visitWhenExpression(expression: org.jetbrains.kotlin.psi.KtWhenExpression) { + override fun visitWhenExpression(expression: KtWhenExpression) { val subject = expression.subjectExpression if (subject != null && expression.subjectVariable == null) { emitDefinition( @@ -583,7 +576,7 @@ class KotlinAnalysisIndexer( * before visiting children so local numbering matches. */ override fun visitDestructuringDeclaration( - destructuringDeclaration: org.jetbrains.kotlin.psi.KtDestructuringDeclaration + destructuringDeclaration: KtDestructuringDeclaration ) { val isParameter = destructuringDeclaration.parent is KtParameter val type = @@ -606,11 +599,10 @@ class KotlinAnalysisIndexer( private fun emitDestructuringEntry(declaration: KtDestructuringDeclarationEntry) { emitLocalVariable(declaration) val range = declaration.nameIdentifier?.textRange ?: return - val destructuring = - declaration.parent as? org.jetbrains.kotlin.psi.KtDestructuringDeclaration ?: return - // Each entry desugars to a `componentN()` call on the destructured value. + val destructuring = declaration.parent as? KtDestructuringDeclaration ?: return + // Entries desugar to `componentN()` calls on the destructured value. // Standalone analysis does not resolve destructuring-entry references, so - // the symbol is derived from the destructured type and the entry position. + // the symbol is derived from the destructured type and entry position. val index = destructuring.entries.indexOf(declaration) destructuredClassId(destructuring)?.let { classId -> val name = "component${index + 1}" @@ -644,9 +636,7 @@ class KotlinAnalysisIndexer( addReference(cache.syntheticLocalSymbol(destructuring), range) } - private fun destructuredClassId( - destructuring: org.jetbrains.kotlin.psi.KtDestructuringDeclaration - ): ClassId? = + private fun destructuredClassId(destructuring: KtDestructuringDeclaration): ClassId? = with(session) { val type = destructuring.initializer?.expressionType @@ -656,14 +646,11 @@ class KotlinAnalysisIndexer( (type as? KaClassType)?.classId } - private fun renderedExpressionType( - expression: org.jetbrains.kotlin.psi.KtExpression - ): String = + private fun renderedExpressionType(expression: KtExpression): String = with(session) { expression.expressionType?.render( - org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource - .WITH_SHORT_NAMES, - position = org.jetbrains.kotlin.types.Variance.INVARIANT, + KaTypeRendererForSource.WITH_SHORT_NAMES, + position = Variance.INVARIANT, ) } ?: "Any" @@ -672,10 +659,8 @@ class KotlinAnalysisIndexer( (parameter.symbol as? KaCallableSymbol) ?.returnType ?.render( - org.jetbrains.kotlin.analysis.api.renderer.types.impl - .KaTypeRendererForSource - .WITH_SHORT_NAMES, - position = org.jetbrains.kotlin.types.Variance.INVARIANT, + KaTypeRendererForSource.WITH_SHORT_NAMES, + position = Variance.INVARIANT, ) } @@ -756,8 +741,8 @@ class KotlinAnalysisIndexer( private val emittedItParameters = HashSet() /** - * A lambda with a single implicit `it` parameter declares it without any PSI: the FIR - * indexer anchored its definition at the whole lambda literal. + * The implicit `it` parameter has no declaration PSI: its definition is anchored at the + * whole lambda literal and emitted at the first reference. */ private fun emitImplicitItDefinition(literal: KtFunctionLiteral, target: KaCallableSymbol) { if (!emittedItParameters.add(literal)) return @@ -771,14 +756,9 @@ class KotlinAnalysisIndexer( ) } - // --------------------------------------------------------------- - // Symbol helpers - // --------------------------------------------------------------- - private fun propertyReferenceSymbols(target: KaPropertySymbol): List { - val psi = target.psi - return when { - psi is KtProperty -> { + return when (val psi = target.psi) { + is KtProperty -> { val property = cache.symbolForDeclaration(psi) if (property.isLocal()) return listOf(property) buildList { @@ -791,7 +771,7 @@ class KotlinAnalysisIndexer( } } } - psi is KtParameter -> + is KtParameter -> buildList { add(cache.parameterPropertySymbol(psi)) add(cache.syntheticAccessorSymbol(psi, setter = false)) @@ -829,10 +809,6 @@ class KotlinAnalysisIndexer( .toList() } - // --------------------------------------------------------------- - // Emission - // --------------------------------------------------------------- - private fun emitDefinition( symbol: Symbol, range: TextRange, @@ -946,11 +922,9 @@ internal class LineIndex(private val text: String) { } /** - * Occurrence anchor ranges are always single-line in the scip-kotlinc output: multiline - * declarations (unnamed companion objects, multiline secondary constructors) were collapsed - * onto their start line with the end character measured from that line's start. The true extent - * lives in the enclosing range. Reproduced here so anchors stay byte-identical and renderable - * by `scip snapshot`. + * Occurrence anchors are always single-line, like in scip-kotlinc: multiline declarations are + * collapsed onto their start line with the end character measured from that line's start. The + * true extent lives in the enclosing range. */ fun anchorRange(range: TextRange): ScipRange? { if (range.startOffset > text.length || range.endOffset > text.length) return null @@ -966,7 +940,7 @@ internal class LineIndex(private val text: String) { private fun lineStart(line: Int): Int = lineStartOffsets[line] private fun position(offset: Int): Pair { - var line = java.util.Arrays.binarySearch(lineStartOffsets, offset) + var line = Arrays.binarySearch(lineStartOffsets, offset) if (line < 0) line = -line - 2 return line to (offset - lineStartOffsets[line]) } diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/ScipSymbols.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/ScipSymbols.kt index afe9c19a9..2ed76e9bd 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/ScipSymbols.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/ScipSymbols.kt @@ -2,19 +2,14 @@ package org.scip_code.scip_java.kotlin_analysis import org.scip_code.scip_java.shared.ScipSymbols as SharedSymbols -// Ported from scip-kotlinc's ScipSymbols.kt so that the Analysis-API-based indexer -// produces byte-identical SCIP symbol strings. scip-kotlinc is deleted once this -// indexer reaches parity, at which point this file is the only copy. @JvmInline value class Symbol(private val symbol: String) { companion object { val NONE = Symbol(SharedSymbols.NONE) val ROOT_PACKAGE = Symbol(SharedSymbols.ROOT_PACKAGE) - // Note: this intentionally diverges from `SharedSymbols.global` when - // `desc == NONE` — Java returns `NONE`, Kotlin returns the owner. - // SymbolsCache relies on this behavior; do not delegate without first - // updating those call sites. + // Diverges from `SharedSymbols.global` when `desc == NONE`: that returns + // NONE, this returns the owner — SymbolsCache relies on it. fun createGlobal(owner: Symbol, desc: ScipSymbolDescriptor): Symbol = when { desc == ScipSymbolDescriptor.NONE -> owner @@ -32,14 +27,11 @@ value class Symbol(private val symbol: String) { override fun toString(): String = symbol } -fun String.symbol(): Symbol = Symbol(this) - data class ScipSymbolDescriptor( val kind: Kind, val name: String, - // Default differs from `SharedSymbols.Descriptor` (which is "") because - // Kotlin call sites — getters/setters in particular — rely on the no-arg - // overload producing `name().` rather than `name.` for METHOD kinds. + // Defaults to "()" (`SharedSymbols.Descriptor` defaults to ""): call sites + // rely on the no-arg form producing `name().` for METHOD kinds. val disambiguator: String = "()", ) { companion object { diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt index 947e10cfa..8733bc187 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtClassOrObject import org.jetbrains.kotlin.psi.KtConstructor import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtNamedDeclaration import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.KtObjectDeclaration import org.jetbrains.kotlin.psi.KtParameter @@ -20,10 +21,9 @@ import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject import org.jetbrains.kotlin.types.Variance /** - * Renders hover signatures in the exact textual format that scip-kotlinc's FirRenderer setup - * produced (see the goldens under scip-snapshots/expected/kotlin), so that migrating the indexer - * does not churn signature documentation. Explicitly written type annotations are taken verbatim - * from source; inferred types are rendered with short names via the Analysis API. + * Renders hover signatures in the textual format of scip-kotlinc's FirRenderer setup (see the + * goldens under scip-snapshots/expected/kotlin). Explicit type annotations are taken verbatim from + * source; inferred types are rendered with short names via the Analysis API. */ @OptIn(KaExperimentalApi::class) internal class SignatureRenderer(private val session: KaSession) { @@ -82,7 +82,6 @@ internal class SignatureRenderer(private val session: KaSession) { declaration.getTypeReference()?.text.orEmpty() + "\n" - /** `values()` / `valueOf(value: String)` / `entries` signatures of an enum class. */ fun enumValuesSignature(name: String): String = "public final static fun values(): Array<$name>" fun enumValueOfSignature(name: String): String = @@ -138,7 +137,7 @@ internal class SignatureRenderer(private val session: KaSession) { } fun propertySignature(declaration: KtDeclaration): String { - val name = (declaration as? org.jetbrains.kotlin.psi.KtNamedDeclaration)?.name.orEmpty() + val name = (declaration as? KtNamedDeclaration)?.name.orEmpty() val const = if (declaration.hasModifier(KtTokens.CONST_KEYWORD)) "const " else "" val lateinit = if (declaration.hasModifier(KtTokens.LATEINIT_KEYWORD)) "lateinit " else "" val valOrVar = if (isVar(declaration)) "var" else "val" @@ -265,7 +264,7 @@ internal class SignatureRenderer(private val session: KaSession) { else -> "final" } - /** Modality + `override`, with a trailing space; empty for abstract-less contexts. */ + /** Modality plus `override`, with a trailing space. */ private fun memberModality(declaration: KtDeclaration): String { val containingInterface = (declaration.containingClassOrObject as? KtClass)?.isInterface() == true diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt index 3f260a000..fa2a49068 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt @@ -45,10 +45,9 @@ import org.scip_code.scip_java.shared.LocalSymbolsCache as SharedLocalSymbolsCac /** * Computes SCIP symbol strings for Kotlin declarations and resolved references. * - * This is a PSI-driven port of the FIR-based `SymbolsCache` in scip-kotlinc. Declarations with - * Kotlin source are classified from their PSI structure, which keeps the version-sensitive Analysis - * API surface minimal. References that resolve to declarations without Kotlin source (classpath or - * JDK symbols) fall back to `ClassId`/`CallableId`-derived symbols. + * Declarations with Kotlin source are classified from their PSI structure, which keeps the + * version-sensitive Analysis API surface minimal. References that resolve to declarations without + * Kotlin source (classpath or JDK symbols) fall back to `ClassId`/`CallableId`-derived symbols. * * Instantiate one cache per indexed file: local symbols are numbered per SCIP document. */ @@ -92,9 +91,9 @@ class SymbolsCache { ) /** - * The SCIP symbol of the synthetic getter/setter of a property (or `val`/`var` constructor - * parameter) that has no explicit accessor PSI. Accessors are owned by the property's container - * (`Class#getBanana().`), not by the constructor that declares the parameter. + * The synthetic getter/setter of a property (or `val`/`var` constructor parameter) without + * explicit accessor PSI. Owned by the property's container (`Class#getBanana().`), not by the + * constructor declaring the parameter. */ fun syntheticAccessorSymbol(property: KtDeclaration, setter: Boolean): Symbol { val name = (property as? KtNamedDeclaration)?.name.orEmpty() @@ -121,7 +120,14 @@ class SymbolsCache { } /** The SCIP symbol of a class referenced by its (expanded) [ClassId]. */ - fun classSymbol(classId: ClassId): Symbol = classIdSymbol(classId) + fun classSymbol(classId: ClassId): Symbol { + var symbol = packageSymbol(classId.packageFqName) + for (segment in classId.relativeClassName.pathSegments()) { + symbol = + Symbol.createGlobal(symbol, ScipSymbolDescriptor(Kind.TYPE, segment.asString())) + } + return symbol + } /** Symbol of a compiler-generated member method (enum `values()`, data-class `copy()`, …). */ fun memberMethodSymbol(owner: KtClassOrObject, name: String): Symbol = @@ -158,9 +164,8 @@ class SymbolsCache { ) /** - * The SCIP symbol of the implicit `it` parameter of a lambda. The function literal PSI is used - * as the cache key; this cannot collide with the literal itself because anonymous functions - * never cache a symbol. + * The implicit `it` parameter of a lambda, keyed by the function literal PSI — safe because + * anonymous functions never cache a symbol of their own. */ fun implicitItSymbol(literal: KtFunctionLiteral): Symbol = syntheticLocalSymbol(literal) @@ -168,8 +173,7 @@ class SymbolsCache { fun symbolForReference(session: KaSession, target: KaSymbol): Symbol { val psi = target.psi if (psi is KtDeclaration) { - // A constructor symbol whose PSI is the class itself is an implicit - // primary constructor. + // A constructor symbol whose PSI is the class itself is an implicit primary. if (target is KaConstructorSymbol && psi is KtClassOrObject) { return implicitConstructorSymbol(psi) } @@ -180,7 +184,7 @@ class SymbolsCache { // Source type aliases are expanded like library ones, matching scip-kotlinc. if (psi is KtTypeAlias && target is KaTypeAliasSymbol) { return with(session) { (target.expandedType as? KaClassType)?.classId } - ?.let(::classIdSymbol) ?: symbolForDeclaration(psi) + ?.let(::classSymbol) ?: symbolForDeclaration(psi) } // Compiler-generated callables (data-class componentN/copy, enum // `entries`, …) resolve to PSI of the class or parameter they derive @@ -204,17 +208,17 @@ class SymbolsCache { is KaConstructorSymbol -> target.containingClassId?.let { classId -> Symbol.createGlobal( - classIdSymbol(classId), + classSymbol(classId), ScipSymbolDescriptor(Kind.METHOD, ""), ) } ?: Symbol.NONE - // Library type aliases are expanded to the aliased class, matching the - // FIR indexer (`AutoCloseable` → `java/lang/AutoCloseable#`) and - // keeping Kotlin and Java references to the same class unified. + // Library type aliases are expanded to the aliased class (`AutoCloseable` + // → `java/lang/AutoCloseable#`), unifying Kotlin and Java references. is KaTypeAliasSymbol -> - with(session) { (target.expandedType as? KaClassType)?.classId } - ?.let(::classIdSymbol) ?: target.classId?.let(::classIdSymbol) ?: Symbol.NONE - is KaClassLikeSymbol -> target.classId?.let(::classIdSymbol) ?: Symbol.NONE + with(session) { (target.expandedType as? KaClassType)?.classId }?.let(::classSymbol) + ?: target.classId?.let(::classSymbol) + ?: Symbol.NONE + is KaClassLikeSymbol -> target.classId?.let(::classSymbol) ?: Symbol.NONE is KaCallableSymbol -> externalCallableSymbol(session, target) else -> Symbol.NONE } @@ -227,8 +231,7 @@ class SymbolsCache { setter: Boolean, ): Symbol { val callableId = target.callableId ?: return Symbol.NONE - val owner = - callableId.classId?.let(::classIdSymbol) ?: packageSymbol(callableId.packageName) + val owner = callableId.classId?.let(::classSymbol) ?: packageSymbol(callableId.packageName) return Symbol.createGlobal( owner, accessorDescriptor(callableId.callableName.asString(), setter), @@ -242,13 +245,11 @@ class SymbolsCache { ) private fun uncachedSymbol(declaration: KtDeclaration): Symbol { - // Anonymous functions and lambdas have no symbol of their own, mirroring - // the FIR indexer which returned NONE for FirAnonymousFunctionSymbol. + // Anonymous functions and lambdas have no symbol of their own, mirroring FIR. if (declaration is KtFunctionLiteral) return Symbol.NONE if (declaration is KtNamedFunction && declaration.name == null) return Symbol.NONE - // Anonymous objects are global symbols owned by the file's package, - // mirroring FIR where getContainingDeclaration returns null for them. + // Anonymous objects are global symbols owned by the file's package, mirroring FIR. if (declaration is KtObjectDeclaration && declaration.isObjectLiteral()) { return Symbol.createGlobal( packageSymbol(declaration.containingKtFile.packageFqName), @@ -400,8 +401,7 @@ class SymbolsCache { */ private fun externalCallableSymbol(session: KaSession, target: KaCallableSymbol): Symbol { val callableId = target.callableId ?: return Symbol.NONE - val owner = - callableId.classId?.let(::classIdSymbol) ?: packageSymbol(callableId.packageName) + val owner = callableId.classId?.let(::classSymbol) ?: packageSymbol(callableId.packageName) val name = callableId.callableName.asString() val descriptor = when (target) { @@ -431,13 +431,4 @@ class SymbolsCache { val index = overloads.indexOfFirst { it == target } return if (index <= 0) "()" else "(+$index)" } - - private fun classIdSymbol(classId: ClassId): Symbol { - var symbol = packageSymbol(classId.packageFqName) - for (segment in classId.relativeClassName.pathSegments()) { - symbol = - Symbol.createGlobal(symbol, ScipSymbolDescriptor(Kind.TYPE, segment.asString())) - } - return symbol - } } From a77e13338e9779641a27264aa685468c7781e039 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 15:08:20 +0200 Subject: [PATCH 15/15] Keep occurrence ranges within source lines --- .../kotlin_analysis/KotlinAnalysisIndexer.kt | 21 ++++++++++++++----- .../main/kotlin/snapshots/CompanionOwner.kt | 16 +++++++------- .../common/src/main/kotlin/snapshots/Enums.kt | 17 +++++++-------- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt index 73e8f270e..c33ab80dc 100644 --- a/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt @@ -34,6 +34,7 @@ import org.jetbrains.kotlin.psi.KtDeclaration import org.jetbrains.kotlin.psi.KtDestructuringDeclaration import org.jetbrains.kotlin.psi.KtDestructuringDeclarationEntry import org.jetbrains.kotlin.psi.KtEnumEntry +import org.jetbrains.kotlin.psi.KtEnumEntrySuperclassReferenceExpression import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtForExpression @@ -191,6 +192,10 @@ class KotlinAnalysisIndexer( super.visitSimpleNameExpression(expression) val referencedName = expression.getReferencedName() if (referencedName == "this" || referencedName == "super") return + // The synthetic superclass reference of an enum entry initializer + // (`HEARTS('h')`) has no source token of its own: its "name element" + // is the whole enum class. + if (expression is KtEnumEntrySuperclassReferenceExpression) return val target = with(session) { expression.mainReference.resolveToSymbol() } ?: return val range = expression.getReferencedNameElement().textRange @@ -922,9 +927,9 @@ internal class LineIndex(private val text: String) { } /** - * Occurrence anchors are always single-line, like in scip-kotlinc: multiline declarations are - * collapsed onto their start line with the end character measured from that line's start. The - * true extent lives in the enclosing range. + * Occurrence anchors are always single-line: multiline declarations are collapsed onto their + * start line, clamped to that line's end so the range stays within the source. The true extent + * lives in the enclosing range. */ fun anchorRange(range: TextRange): ScipRange? { if (range.startOffset > text.length || range.endOffset > text.length) return null @@ -933,12 +938,18 @@ internal class LineIndex(private val text: String) { if (startLine == endLine) { return ScipRange.singleLine(startLine, startCharacter, endCharacter) } - val flattenedEnd = range.endOffset - lineStart(startLine) - return ScipRange.singleLine(startLine, startCharacter, flattenedEnd) + return ScipRange.singleLine(startLine, startCharacter, lineLength(startLine)) } private fun lineStart(line: Int): Int = lineStartOffsets[line] + /** Length of [line] excluding its trailing newline. */ + private fun lineLength(line: Int): Int { + val end = + if (line + 1 < lineStartOffsets.size) lineStartOffsets[line + 1] - 1 else text.length + return end - lineStart(line) + } + private fun position(offset: Int): Pair { var line = Arrays.binarySearch(lineStartOffsets, offset) if (line < 0) line = -line - 2 diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/CompanionOwner.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/CompanionOwner.kt index 37ace2dd4..48e740412 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/CompanionOwner.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/CompanionOwner.kt @@ -15,14 +15,14 @@ // ⌄ enclosing_range_start scip-java maven . . snapshots/CompanionOwner#Companion# // ⌄ enclosing_range_start scip-java maven . . snapshots/CompanionOwner#Companion#``(). companion object { -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/CompanionOwner#Companion# -// display_name Companion -// signature_documentation -// > public final companion object Companion : Any -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/CompanionOwner#Companion#``(). -// display_name Companion -// signature_documentation -// > private constructor(): CompanionOwner.Companion +// ^^^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/CompanionOwner#Companion# +// display_name Companion +// signature_documentation +// > public final companion object Companion : Any +// ^^^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/CompanionOwner#Companion#``(). +// display_name Companion +// signature_documentation +// > private constructor(): CompanionOwner.Companion // ⌄ enclosing_range_start scip-java maven . . snapshots/CompanionOwner#Companion#create(). fun create(): CompanionOwner = CompanionOwner() // ^^^^^^ definition scip-java maven . . snapshots/CompanionOwner#Companion#create(). diff --git a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt index b16f4a7ba..12739eb43 100644 --- a/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt @@ -12,7 +12,6 @@ // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#symbol. // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#getSymbol(). enum class Suit(val symbol: Char) { -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ reference scip-java maven . . snapshots/Suit# // ^^^^ definition scip-java maven . . snapshots/Suit# // display_name Suit // signature_documentation @@ -90,14 +89,14 @@ // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#Companion# // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#Companion#``(). companion object { -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/Suit#Companion# -// display_name Companion -// signature_documentation -// > public final companion object Companion : Any -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/Suit#Companion#``(). -// display_name Companion -// signature_documentation -// > private constructor(): Suit.Companion +// ^^^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/Suit#Companion# +// display_name Companion +// signature_documentation +// > public final companion object Companion : Any +// ^^^^^^^^^^^^^^^^^^ definition scip-java maven . . snapshots/Suit#Companion#``(). +// display_name Companion +// signature_documentation +// > private constructor(): Suit.Companion // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#Companion#fromSymbol(). // ⌄ enclosing_range_start scip-java maven . . snapshots/Suit#Companion#fromSymbol().(symbol) // ⌄ enclosing_range_start local 0