diff --git a/.github/renovate.json b/.github/renovate.json index ae9a1a811..fad67a28d 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.", + "matchPackageNames": [ + "org.jetbrains.kotlin:kotlin-compiler", + "*-for-ide", + "com.intellij.platform:*" + ], + "enabled": false + } + ] } 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/.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 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 93d930c20..21b9685c5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,9 +2,10 @@ 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 = "2.4.0" +kotlin-analysis-api = "2.4.0" +kotlinx-collections-immutable = "0.3.8" +kotlinx-coroutines-intellij = "1.8.0-intellij-13" kotlinx-serialization = "1.11.0" lombok = "1.18.46" maven-plugin-annotations = "3.15.2" @@ -18,20 +19,24 @@ 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-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-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-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 = "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-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..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 @@ -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 { @@ -20,8 +25,15 @@ 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) { + // 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()); + } Map extraProperties = project.getExtensions().getExtraProperties().getProperties(); @@ -92,12 +104,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 +162,59 @@ private static boolean tryAddJavacPlugin(Project project, Object javacPluginDep) } } - private static void configureKotlinCompileTask( - Task task, Object scipKotlinc, String sourceRoot, String targetRoot) { + /** + * 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")) { 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. + FileCollection libraries; + try { + libraries = (FileCollection) task.getClass().getMethod("getLibraries").invoke(task); + } catch (ReflectiveOperationException exc) { + libraries = (FileCollection) task.getClass().getMethod("getClasspath").invoke(task); + } + for (File entry : libraries.getFiles()) { + lines.add("classpath " + entry.getAbsolutePath()); + } + + int sources = 0; + for (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; + } + + Path configDir = Paths.get(targetRoot, "kotlin-configs"); + Files.createDirectories(configDir); + String fileName = task.getPath().replaceAll("[^A-Za-z0-9]", "-") + ".txt"; + 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 835e24adc..26111ee58 100644 --- a/scip-java/build.gradle.kts +++ b/scip-java/build.gradle.kts @@ -13,17 +13,12 @@ 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")) + 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) @@ -46,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/ScipJava.kt b/scip-java/src/main/kotlin/org/scip_code/scip_java/ScipJava.kt index a15ba62a4..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,6 +18,8 @@ object ScipJava { @JvmStatic fun main(args: Array) { app.runAndExitIfNonZero(args.toList()) + // 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 900287442..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 @@ -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,62 @@ 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 } + /** + * 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") + 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 +163,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-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..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 @@ -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")) + // 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? { diff --git a/scip-kotlin-analysis/build.gradle.kts b/scip-kotlin-analysis/build.gradle.kts new file mode 100644 index 000000000..f500d908c --- /dev/null +++ b/scip-kotlin-analysis/build.gradle.kts @@ -0,0 +1,35 @@ +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 } + // 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 } + 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..c33ab80dc --- /dev/null +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/KotlinAnalysisIndexer.kt @@ -0,0 +1,958 @@ +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 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 +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.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.KtEnumEntrySuperclassReferenceExpression +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 +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.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 +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 +import org.scip_code.scip_java.shared.ScipShardWriter + +/** + * Indexes Kotlin sources into SCIP documents using the Kotlin Analysis API in standalone mode. + * + * 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`, the layout produced by the scip-javac + * compiler plugin. + */ +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" + ) + } + } + + @OptIn(KaExperimentalApi::class) + private class IndexingVisitor( + private val session: KaSession, + private val cache: SymbolsCache, + private val lines: LineIndex, + private val out: ScipDocumentBuilder, + ) : KtTreeVisitorVoid() { + private val signatures = SignatureRenderer(session) + + override fun visitDeclaration(dcl: KtDeclaration) { + // 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) + 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 KtTypeAlias -> emitTypeAlias(dcl) + is KtDestructuringDeclarationEntry -> emitDestructuringEntry(dcl) + else -> {} + } + super.visitDeclaration(dcl) + } + + override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) { + 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 + + 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 + // (`banana` → `Class#banana.`, `Class#getBanana().`, `Class#setBanana().`). + if (target is KaPropertySymbol) { + propertyReferenceSymbols(target).forEach { addReference(it, range) } + return + } + + addReference(cache.symbolForReference(session, target), range) + } + + private fun emitClassLike(declaration: KtClassOrObject) { + val isAnonymous = declaration is KtObjectDeclaration && declaration.isObjectLiteral() + val range = + when { + declaration.nameIdentifier != null -> declaration.nameIdentifier!!.textRange + isAnonymous -> + declaration.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 (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) { + emitDefinition( + symbol = cache.implicitConstructorSymbol(declaration), + range = range, + enclosing = declaration.textRange, + displayName = displayName, + signatureText = signatures.implicitConstructorSignature(declaration), + documentation = docComment(declaration), + ) + } else if ( + primaryConstructor != null && primaryConstructor.getConstructorKeyword() == null + ) { + emitDefinition( + symbol = cache.symbolForDeclaration(primaryConstructor), + range = range, + // 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, + 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)`, `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() + 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( + 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) { + // Keyword-less primary constructors are emitted by emitClassLike. + is KtPrimaryConstructor -> + declaration.getConstructorKeyword()?.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 emitFunction(declaration: KtNamedFunction) { + val identifier = declaration.nameIdentifier ?: return + val relationships = + if (declaration.hasModifier(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 emitParameter(declaration: KtParameter) { + val loopOrCatchIdentifier = declaration.nameIdentifier + // `for` loop variables and `catch` parameters are local declarations, + // numbered at the declaration site like in the FIR indexer. + if (loopOrCatchIdentifier != null) { + if (declaration.parent is 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 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) + 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; 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), + 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, + ) + 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, + * defined at the subject expression. Emitted before visiting children so local numbering + * matches. + */ + override fun visitWhenExpression(expression: 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: 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? 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 entry position. + val index = destructuring.entries.indexOf(declaration) + destructuredClassId(destructuring)?.let { classId -> + 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) + } + + private fun destructuredClassId(destructuring: 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: KtExpression): String = + with(session) { + expression.expressionType?.render( + KaTypeRendererForSource.WITH_SHORT_NAMES, + position = Variance.INVARIANT, + ) + } ?: "Any" + + private fun renderedParameterType(parameter: KtParameter): String? = + with(session) { + (parameter.symbol as? KaCallableSymbol) + ?.returnType + ?.render( + KaTypeRendererForSource.WITH_SHORT_NAMES, + position = Variance.INVARIANT, + ) + } + + 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 (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() + + /** + * 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 + emitDefinition( + symbol = cache.implicitItSymbol(literal), + range = literal.textRange, + enclosing = literal.textRange, + displayName = "it", + signatureText = signatures.implicitItSignature(target), + documentation = null, + ) + } + + private fun propertyReferenceSymbols(target: KaPropertySymbol): List { + return when (val psi = target.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)) + } + } + } + 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() + } + + 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. */ +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) + } + + /** + * 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 + val (startLine, startCharacter) = position(range.startOffset) + val (endLine, endCharacter) = position(range.endOffset) + if (startLine == endLine) { + return ScipRange.singleLine(startLine, startCharacter, endCharacter) + } + 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 + return line to (offset - lineStartOffsets[line]) + } +} 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-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipSymbols.kt b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/ScipSymbols.kt similarity index 76% rename from scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipSymbols.kt rename to scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/ScipSymbols.kt index b3ffb4520..2ed76e9bd 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipSymbols.kt +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/ScipSymbols.kt @@ -1,4 +1,4 @@ -package org.scip_code.scip_java.kotlinc +package org.scip_code.scip_java.kotlin_analysis import org.scip_code.scip_java.shared.ScipSymbols as SharedSymbols @@ -8,10 +8,8 @@ value class Symbol(private val symbol: String) { 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 @@ -29,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 new file mode 100644 index 000000000..8733bc187 --- /dev/null +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SignatureRenderer.kt @@ -0,0 +1,294 @@ +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.KtNamedDeclaration +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.KtTypeAlias +import org.jetbrains.kotlin.psi.KtVariableDeclaration +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject +import org.jetbrains.kotlin.types.Variance + +/** + * 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) { + + 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" + 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" + 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) ?: constructorVisibility(owner) + return "$visibility constructor${classTypeParameters(owner)}" + + "(${parametersText(constructor.valueParameters)}): " + + constructorReturnType(owner) + } + + fun implicitConstructorSignature(declaration: KtClassOrObject): String { + 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 = + "${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 + // (see the Animal#sound() golden). + return if (declaration.hasBody()) signature else signature + "\n" + } + + fun typeAliasSignature(declaration: KtTypeAlias): String = + "public final typealias ${declaration.name.orEmpty()} = " + + declaration.getTypeReference()?.text.orEmpty() + + "\n" + + 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): ${ownerName(declaration)}\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() + 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? 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)}$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 "${annotationsPrefix(declaration)}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 } + 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 = + 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 plus `override`, with a trailing space. */ + 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 new file mode 100644 index 000000000..fa2a49068 --- /dev/null +++ b/scip-kotlin-analysis/src/main/kotlin/org/scip_code/scip_java/kotlin_analysis/SymbolsCache.kt @@ -0,0 +1,434 @@ +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.KtTypeAlias +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. + * + * 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 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 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() + 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 { + 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 = + 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 -> + (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 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) + + /** The SCIP symbol for the target of a resolved reference. */ + 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. + if (target is KaConstructorSymbol && psi is KtClassOrObject) { + return implicitConstructorSymbol(psi) + } + // The implicit `it` parameter's PSI is the enclosing function literal. + 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(::classSymbol) ?: 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) { + is KaPackageSymbol -> packageSymbol(target.fqName) + is KaConstructorSymbol -> + target.containingClassId?.let { classId -> + Symbol.createGlobal( + classSymbol(classId), + ScipSymbolDescriptor(Kind.METHOD, ""), + ) + } ?: Symbol.NONE + // 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(::classSymbol) + ?: target.classId?.let(::classSymbol) + ?: Symbol.NONE + is KaClassLikeSymbol -> target.classId?.let(::classSymbol) ?: Symbol.NONE + 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(::classSymbol) ?: 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 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. + 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) + if (owner.isLocal() || owner == Symbol.NONE) return locals.put(declaration) + + return Symbol.createGlobal(owner, scipDescriptor(declaration)) + } + + /** + * 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 -> {} + 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) { + 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 -> + accessorDescriptor(declaration.property.name.orEmpty(), setter = true) + declaration is KtPropertyAccessor -> + accessorDescriptor(declaration.property.name.orEmpty(), setter = false) + declaration is KtConstructor<*> -> + ScipSymbolDescriptor(Kind.METHOD, "", constructorDisambiguator(declaration)) + declaration is KtNamedFunction -> + ScipSymbolDescriptor( + Kind.METHOD, + 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 -> + 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 + + /** + * 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(::classSymbol) ?: packageSymbol(callableId.packageName) + val name = callableId.callableName.asString() + val descriptor = + when (target) { + 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)" + } +} 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/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/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 0dc573c2d..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,27 +9,66 @@ 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 Kotlin indexer. +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 -tasks.named("compileKotlin") { - inputs.files(kotlincShadowJar) +// 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(scipTargetroot) - compilerOptions.freeCompilerArgs.addAll(scipKotlincPluginArgs(kotlincShadowJar.elements, sourceroot, targetroot)) cleanDirectoryBeforeRunning(scipTargetroot) + // Locals only: the argument provider must not capture the build script + // (configuration cache). + val sourcerootArg = sourceroot + val targetrootArg = scipTargetroot + val kotlinSourcesArg = kotlinSources.asFile.absolutePath + argumentProviders.add( + CommandLineArgumentProvider { + listOf( + "--sourceroot", + sourcerootArg, + "--targetroot", + targetrootArg.get().asFile.absolutePath, + "--classpath", + compileClasspath.get().asPath, + kotlinSourcesArg, + ) + } + ) } 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/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..53d3ba257 --- /dev/null +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Annotations.kt @@ -0,0 +1,71 @@ + 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/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) +// ⌄ 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") +// ^^^^^^ reference scip-java maven . . snapshots/Tagged# + 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") +// ^^^^^^ reference scip-java maven . . snapshots/Tagged# + 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/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..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(). @@ -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/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..3c3f8f453 --- /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..12739eb43 --- /dev/null +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Enums.kt @@ -0,0 +1,170 @@ + 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/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) { +// ^^^^ 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/Suit#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/Suit#HEARTS. + HEARTS('h'), +// ^^^^^^ definition scip-java maven . . snapshots/Suit#HEARTS. +// display_name HEARTS +// signature_documentation +// > 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/Suit#SPADES. +// display_name SPADES +// signature_documentation +// > 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' +// ^^^^^ 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(). +// ^^ 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# +// ⌄ 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/Suit#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 . . 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(). +// ⌃ 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/Suit#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# +// ^^^^^^ 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 new file mode 100644 index 000000000..921285395 --- /dev/null +++ b/scip-snapshots/expected/kotlin/common/scip-snapshots/cases/kotlin/common/src/main/kotlin/snapshots/Generics.kt @@ -0,0 +1,161 @@ + package snapshots +// ^^^^^^^^^ 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#``().(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 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 +// > 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# +// ^ 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. +// ⌃ 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#[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. +// ^^^^^ 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 R +// signature_documentation +// > R : Any +// ^^^ reference scip-java maven . . kotlin/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 . . 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). +// ^^^^^^^^^ 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 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# +// ^ 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) +// ⌃ 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 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(). +// ⌄ 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: StringContainer): StringContainer +// ^^^^^^^^^ definition scip-java maven . . snapshots/useContainer().(container) +// display_name container +// signature_documentation +// > container: StringContainer +// ^^^^^^^^^^^^^^^ 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(). + 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 7a3395b07..6747622d7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,14 @@ dependencyResolutionManagement { repositories { mavenCentral() gradlePluginPortal() + // 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") + } + } } } @@ -19,7 +27,7 @@ rootProject.name = "scip-java" include( "scip-shared", "scip-javac", - "scip-kotlinc", + "scip-kotlin-analysis", "scip-aggregator", "scip-maven-plugin", "scip-gradle-plugin",