diff --git a/.tool-versions b/.tool-versions index 68e582a9..c9953ae1 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -golang 1.17.5 +golang 1.23.1 diff --git a/build.sbt b/build.sbt index c2d8860a..c188c78b 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,7 @@ import scala.util.control.NoStackTrace lazy val V = new { - val protobuf = "3.15.6" + val protobuf = "3.25.6" val protoc = "3.17.3" // the oldest protoc version with Apple M1 support, see https://github.com/scalapb/ScalaPB/issues/1024#issuecomment-860126568 val coursier = "2.1.9" @@ -107,6 +107,31 @@ lazy val agent = project "Premain-Class" -> "com.sourcegraph.semanticdb_javac.SemanticdbAgent" ) ) + +import kotlin.Keys._ +lazy val kotlincPlugin = project + .in(file("semanticdb-kotlinc")) + .enablePlugins(KotlinPlugin) + .settings( + kotlinVersion := "2.0.21", + kotlincJvmTarget := "1.8", + kotlinLib("stdlib"), + kotlinLib("compiler-embeddable"), + // the source generator that sbt-protoc adds only includes java and scala files + Compile / sourceGenerators += + (Compile / PB.generate) + .map( + _.filter { file => + file.getName.endsWith(".kt") + } + ) + .taskValue, + (Compile / PB.protoSources) := + Seq((semanticdb / Compile / sourceDirectory).value / "protobuf"), + (Compile / PB.targets) := + Seq(PB.gens.kotlin(V.protobuf) -> (Compile / sourceManaged).value) + ) + lazy val gradlePlugin = project .in(file("semanticdb-gradle-plugin")) .settings( diff --git a/project/build.properties b/project/build.properties index e97b2722..cc68b53f 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.10 +sbt.version=1.10.11 diff --git a/project/plugins.sbt b/project/plugins.sbt index 75548719..5daba329 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -11,6 +11,7 @@ addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.6.1") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0") addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") +addSbtPlugin("org.jetbrains.scala" % "sbt-kotlin-plugin" % "3.1.4") // sbt-jdi-tools appears to fix an error related to this message: // [error] (plugin / Compile / compileIncremental) java.lang.NoClassDefFoundError: com/sun/tools/javac/code/Symbol addSbtPlugin("org.scala-debugger" % "sbt-jdi-tools" % "1.1.1") diff --git a/semanticdb-java/src/main/protobuf/semanticdb.proto b/semanticdb-java/src/main/protobuf/semanticdb.proto index 2e72263c..506f45c0 100644 --- a/semanticdb-java/src/main/protobuf/semanticdb.proto +++ b/semanticdb-java/src/main/protobuf/semanticdb.proto @@ -7,6 +7,8 @@ syntax = "proto3"; package com.sourcegraph.semanticdb_javac; +option java_multiple_files = true; + enum Schema { LEGACY = 0; SEMANTICDB3 = 3; diff --git a/semanticdb-kotlinc/src/main/kotlin/Run.kt b/semanticdb-kotlinc/src/main/kotlin/Run.kt new file mode 100644 index 00000000..a48e464e --- /dev/null +++ b/semanticdb-kotlinc/src/main/kotlin/Run.kt @@ -0,0 +1,6 @@ +package demo + +fun main(args: Array) { + // Test some Kotlin 1.9 features + println(args[0]) +} diff --git a/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/Analyzer.kt b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/Analyzer.kt new file mode 100644 index 00000000..01ecf4a3 --- /dev/null +++ b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/Analyzer.kt @@ -0,0 +1,106 @@ +package com.sourcegraph.semanticdb_kotlinc + +import com.sourcegraph.semanticdb_javac.Semanticdb +import java.io.PrintWriter +import java.io.Writer +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.contracts.ExperimentalContracts +import org.jetbrains.kotlin.analyzer.AnalysisResult +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +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.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension + +@ExperimentalContracts +class Analyzer( + val sourceroot: Path, + val targetroot: Path, + val callback: (Semanticdb.TextDocument) -> Unit +) : AnalysisHandlerExtension { + private val globals = GlobalSymbolsCache() + + private val messageCollector = + CompilerConfiguration() + .get( + CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, + PrintingMessageCollector(System.err, MessageRenderer.PLAIN_FULL_PATHS, false)) + + override fun analysisCompleted( + project: Project, + module: ModuleDescriptor, + bindingTrace: BindingTrace, + files: Collection + ): AnalysisResult? = + try { + val resolver = DescriptorResolver(bindingTrace).also { globals.resolver = it } + for (file in files) { + try { + val lineMap = LineMap(project, file) + val document = + SemanticdbVisitor(sourceroot, resolver, file, lineMap, globals).build() + semanticdbOutPathForFile(file)?.apply { + val builder = Semanticdb.TextDocuments.newBuilder() + builder.addDocuments(document) + Files.write(this, builder.build().toByteArray()) + } + callback(document) + } catch (e: Exception) { + handleException(e) + } + } + + super.analysisCompleted(project, module, bindingTrace, files) + } catch (e: Exception) { + handleException(e) + super.analysisCompleted(project, module, bindingTrace, files) + } + + private fun semanticdbOutPathForFile(file: KtFile): Path? { + val normalizedPath = Paths.get(file.virtualFilePath).normalize() + if (normalizedPath.startsWith(sourceroot)) { + val relative = sourceroot.relativize(normalizedPath) + val filename = relative.fileName.toString() + ".semanticdb" + val semanticdbOutPath = + targetroot + .resolve("META-INF") + .resolve("semanticdb") + .resolve(relative) + .resolveSibling(filename) + + Files.createDirectories(semanticdbOutPath.parent) + return semanticdbOutPath + } + System.err.println( + "given file is not under the sourceroot.\n\tSourceroot: $sourceroot\n\tFile path: ${file.virtualFilePath}\n\tNormalized file path: $normalizedPath") + return null + } + + 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 semanticdb-kotlin compiler plugin:") + e.printStackTrace(writer) + writer.println( + "Please report a bug to https://github.com/sourcegraph/lsif-kotlin with the stack trace above.") + writer.close() + } +} diff --git a/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/AnalyzerCommandLineProcessor.kt b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/AnalyzerCommandLineProcessor.kt new file mode 100644 index 00000000..4c67ac78 --- /dev/null +++ b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/AnalyzerCommandLineProcessor.kt @@ -0,0 +1,44 @@ +package com.sourcegraph.semanticdb_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 = "semanticdb-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 SemanticDB 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/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/AnalyzerRegistrar.kt b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/AnalyzerRegistrar.kt new file mode 100644 index 00000000..1f837d57 --- /dev/null +++ b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/AnalyzerRegistrar.kt @@ -0,0 +1,29 @@ +package com.sourcegraph.semanticdb_kotlinc + +import com.sourcegraph.semanticdb_javac.Semanticdb +import java.lang.IllegalArgumentException +import kotlin.contracts.ExperimentalContracts +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension + +@OptIn(ExperimentalCompilerApi::class) +@ExperimentalContracts +class AnalyzerRegistrar(private val callback: (Semanticdb.TextDocument) -> Unit = {}) : + ComponentRegistrar { + override fun registerProjectComponents( + project: MockProject, + configuration: CompilerConfiguration + ) { + AnalysisHandlerExtension.registerExtension( + project, + Analyzer( + sourceroot = configuration[KEY_SOURCES] + ?: throw IllegalArgumentException("configuration key $KEY_SOURCES missing"), + targetroot = configuration[KEY_TARGET] + ?: throw IllegalArgumentException("configuration key $KEY_TARGET missing"), + callback = callback)) + } +} diff --git a/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/DeclarationExtensions.kt b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/DeclarationExtensions.kt new file mode 100644 index 00000000..49654c0c --- /dev/null +++ b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/DeclarationExtensions.kt @@ -0,0 +1,11 @@ +package com.sourcegraph.semanticdb_kotlinc + +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.impl.LocalVariableDescriptor + +fun DeclarationDescriptor.isObjectDeclaration(): Boolean = + this is ClassDescriptor && this.visibility == DescriptorVisibilities.LOCAL + +fun DeclarationDescriptor.isLocalVariable(): Boolean = this is LocalVariableDescriptor diff --git a/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/DescriptorResolver.kt b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/DescriptorResolver.kt new file mode 100644 index 00000000..6c0f42f6 --- /dev/null +++ b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/DescriptorResolver.kt @@ -0,0 +1,29 @@ +package com.sourcegraph.semanticdb_kotlinc + +import org.jetbrains.kotlin.descriptors.ConstructorDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.types.KotlinType + +class DescriptorResolver(/* leave public for debugging */ val bindingTrace: BindingTrace) { + fun fromDeclaration(declaration: KtDeclaration): Sequence = sequence { + val descriptor = bindingTrace[BindingContext.DECLARATION_TO_DESCRIPTOR, declaration] + if (descriptor is ValueParameterDescriptor) { + bindingTrace[BindingContext.VALUE_PARAMETER_AS_PROPERTY, descriptor]?.let { yield(it) } + } + descriptor?.let { yield(it) } + } + + fun syntheticConstructor(klass: KtClass): ConstructorDescriptor? = + bindingTrace[BindingContext.CONSTRUCTOR, klass] + + fun fromReference(reference: KtReferenceExpression): DeclarationDescriptor? = + bindingTrace[BindingContext.REFERENCE_TARGET, reference] + + fun fromTypeReference(reference: KtTypeReference): KotlinType = + bindingTrace[BindingContext.TYPE, reference] + ?: bindingTrace[BindingContext.ABBREVIATED_TYPE, reference]!! +} diff --git a/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/LineMap.kt b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/LineMap.kt new file mode 100644 index 00000000..6c4bdc35 --- /dev/null +++ b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/LineMap.kt @@ -0,0 +1,38 @@ +package com.sourcegraph.semanticdb_kotlinc + +import org.jetbrains.kotlin.com.intellij.navigation.NavigationItem +import org.jetbrains.kotlin.com.intellij.openapi.editor.Document +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.psi.PsiDocumentManager +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.diagnostics.PsiDiagnosticUtils +import org.jetbrains.kotlin.diagnostics.PsiDiagnosticUtils.LineAndColumn +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtPropertyAccessor + +/** Maps between an element and its identifier positions */ +class LineMap(project: Project, file: KtFile) { + private val document: Document = PsiDocumentManager.getInstance(project).getDocument(file)!! + + private fun offsetToLineAndCol(offset: Int): LineAndColumn = + PsiDiagnosticUtils.offsetToLineAndColumn(document, offset) + + /** Returns the non-0-based start character */ + fun startCharacter(element: PsiElement): Int = offsetToLineAndCol(element.textOffset).column + + /** Returns the non-0-based end character */ + fun endCharacter(element: PsiElement): Int = + startCharacter(element) + nameForOffset(element).length + + /** Returns the non-0-based line number */ + fun lineNumber(element: PsiElement): Int = document.getLineNumber(element.textOffset) + 1 + + companion object { + fun nameForOffset(element: PsiElement): String = + when (element) { + is KtPropertyAccessor -> element.namePlaceholder.text + is NavigationItem -> element.name ?: element.text + else -> element.text + } + } +} diff --git a/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbSymbols.kt b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbSymbols.kt new file mode 100644 index 00000000..ffb19c7a --- /dev/null +++ b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbSymbols.kt @@ -0,0 +1,71 @@ +package com.sourcegraph.semanticdb_kotlinc + +@JvmInline +value class Symbol(private val symbol: String) { + companion object { + val NONE = Symbol("") + val ROOT_PACKAGE = Symbol("_root_/") + val EMPTY_PACKAGE = Symbol("_empty_/") + + fun createGlobal(owner: Symbol, desc: SemanticdbSymbolDescriptor): Symbol = + when { + desc == SemanticdbSymbolDescriptor.NONE -> NONE + owner != ROOT_PACKAGE -> Symbol(owner.symbol + desc.encode().symbol) + else -> desc.encode() + } + + fun createLocal(i: Int) = Symbol("local$i") + } + + fun isGlobal() = !isLocal() + + fun isLocal() = symbol.startsWith("local") + + override fun toString(): String = symbol +} + +fun String.symbol(): Symbol = Symbol(this) + +data class SemanticdbSymbolDescriptor( + val kind: Kind, + val name: String, + val disambiguator: String = "()" +) { + companion object { + val NONE = SemanticdbSymbolDescriptor(Kind.NONE, "") + + private fun encodeName(name: String): String { + if (name.isEmpty()) return "``" + val isStartOk = Character.isJavaIdentifierStart(name[0]) + var isPartsOk = true + var i = 1 + while (isPartsOk && i < name.length) { + isPartsOk = Character.isJavaIdentifierPart(name[i]) + i++ + } + return if (isStartOk && isPartsOk) name else "`$name`" + } + } + + enum class Kind { + NONE, + TERM, + METHOD, + TYPE, + PACKAGE, + PARAMETER, + TYPE_PARAMETER + } + + fun encode() = + Symbol( + when (kind) { + Kind.NONE -> "" + Kind.TERM -> "${encodeName(name)}." + Kind.METHOD -> "${encodeName(name)}${disambiguator}." + Kind.TYPE -> "${encodeName(name)}#" + Kind.PACKAGE -> "${encodeName(name)}/" + Kind.PARAMETER -> "(${encodeName(name)})" + Kind.TYPE_PARAMETER -> "[${encodeName(name)}]" + }) +} diff --git a/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbTextDocumentBuilder.kt b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbTextDocumentBuilder.kt new file mode 100644 index 00000000..f0baf526 --- /dev/null +++ b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbTextDocumentBuilder.kt @@ -0,0 +1,221 @@ +package com.sourcegraph.semanticdb_kotlinc + +import com.sourcegraph.semanticdb_javac.Semanticdb +import com.sourcegraph.semanticdb_javac.Semanticdb.TextDocument +import com.sourcegraph.semanticdb_javac.Semanticdb.SymbolOccurrence.Role +import java.lang.IllegalArgumentException +import java.lang.StringBuilder +import java.nio.file.Path +import java.nio.file.Paths +import java.security.MessageDigest +import kotlin.contracts.ExperimentalContracts +import kotlin.text.Charsets.UTF_8 +import org.jetbrains.kotlin.asJava.namedUnwrappedElement +import org.jetbrains.kotlin.backend.common.serialization.metadata.findKDocString +import org.jetbrains.kotlin.com.intellij.lang.java.JavaLanguage +import org.jetbrains.kotlin.com.intellij.navigation.NavigationItem +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.psi.KtConstructor +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtPropertyAccessor +import org.jetbrains.kotlin.renderer.DescriptorRenderer +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.descriptorUtil.getAllSuperClassifiers + +@ExperimentalContracts +class SemanticdbTextDocumentBuilder( + private val sourceroot: Path, + private val file: KtFile, + private val lineMap: LineMap, + private val cache: SymbolsCache +) { + private val occurrences = mutableListOf() + private val symbols = mutableListOf() + + fun build() = { + TextDocument.newBuilder().setText(file.text).setUri(semanticdbURI()).setMd5(semanticdbMD5()) + .setSchema(Semanticdb.Schema.SEMANTICDB4).setLanguage(Semanticdb.Language.KOTLIN) + .addAllOccurrences(occurrences).addAllSymbols(symbols) + } + + fun emitSemanticdbData( + symbol: Symbol, + descriptor: DeclarationDescriptor, + element: PsiElement, + role: Role + ) { + symbolOccurrence(symbol, element, role)?.let(occurrences::add) + if (role == Role.DEFINITION) symbols.add(symbolInformation(symbol, descriptor, element)) + } + + private val isIgnoredSuperClass = setOf("kotlin.Any", "java.lang.Object", "java.io.Serializable") + + private fun functionDescriptorOverrides(descriptor: FunctionDescriptor): Iterable { + val result = mutableListOf() + val isVisited = mutableSetOf() + val queue = ArrayDeque() + queue.add(descriptor) + while (!queue.isEmpty()) { + val current = queue.removeFirst() + if (current in isVisited) { + continue + } + + isVisited.add(current) + val directOverrides = current.overriddenDescriptors.flatMap { cache[it] }.map { it.toString() } + result.addAll(directOverrides) + queue.addAll(current.overriddenDescriptors) + } + return result + } + + private fun symbolInformation( + symbol: Symbol, + descriptor: DeclarationDescriptor, + element: PsiElement + ): Semanticdb.SymbolInformation { + val supers = + when (descriptor) { + is ClassDescriptor -> + descriptor + .getAllSuperClassifiers() + // first is the class itself + .drop(1) + .filter { + it.fqNameSafe.toString() !in isIgnoredSuperClass + } + .flatMap { cache[it] } + .map { it.toString() } + .asIterable() + + is SimpleFunctionDescriptor -> + functionDescriptorOverrides(descriptor) + + else -> emptyList().asIterable() + } + return SymbolInformation { + this.symbol = symbol.toString() + this.displayName = displayName(element) + this.documentation = semanticdbDocumentation(descriptor) + this.addAllOverriddenSymbols(supers) + this.language = + when (element.language) { + is KotlinLanguage -> Semanticdb.Language.KOTLIN + is JavaLanguage -> Semanticdb.Language.JAVA + else -> + throw IllegalArgumentException("unexpected language ${element.language}") + } + } + } + + private fun symbolOccurrence( + symbol: Symbol, + element: PsiElement, + role: Role + ): Semanticdb.SymbolOccurrence? { + /*val symbol = when(val s = globals[descriptor, locals]) { + Symbol.NONE -> return null + else -> s + }.symbol*/ + + return SymbolOccurrence { + this.symbol = symbol.toString() + this.role = role + this.range = semanticdbRange(element) + } + } + + private fun semanticdbRange(element: PsiElement): Semanticdb.Range { + return Range { + startCharacter = lineMap.startCharacter(element) - 1 + startLine = lineMap.lineNumber(element) - 1 + endCharacter = lineMap.endCharacter(element) - 1 + endLine = lineMap.lineNumber(element) - 1 + } + } + + private fun semanticdbURI(): String { + // TODO: unix-style only + val relative = sourceroot.relativize(Paths.get(file.virtualFilePath)) + return relative.toString() + } + + private fun semanticdbMD5(): String = + MessageDigest.getInstance("MD5").digest(file.text.toByteArray(UTF_8)).joinToString("") { + "%02X".format(it) + } + + private fun semanticdbDocumentation( + descriptor: DeclarationDescriptor + ): Semanticdb.Documentation = Documentation { + format = Semanticdb.Documentation.Format.MARKDOWN + val signature = + DescriptorRenderer.COMPACT_WITH_MODIFIERS + .withOptions { + withSourceFileForTopLevel = true + unitReturnType = false + } + .render(descriptor) + val kdoc = + when (descriptor) { + is DeclarationDescriptorWithSource -> descriptor.findKDocString() ?: "" + else -> "" + } + message = "```kotlin\n$signature\n```${stripKDocAsterisks(kdoc)}" + } + + // Returns the kdoc string with all leading and trailing "/*" tokens removed. Naive + // implementation that can + // be replaced with a utility method from the compiler in the future, if one exists. + private fun stripKDocAsterisks(kdoc: String): String { + if (kdoc.isEmpty()) return kdoc + val out = StringBuilder().append("\n\n").append("----").append("\n") + 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++ + } + out.append("\n").append(line, start, end) + } + return out.toString() + } + + companion object { + private fun displayName(element: PsiElement): String = + when (element) { + is KtPropertyAccessor -> element.namePlaceholder.text + is NavigationItem -> + when (element.namedUnwrappedElement) { + is KtConstructor<*> -> + (element.namedUnwrappedElement as KtConstructor<*>).name!! + + else -> element.name ?: element.text + } + + else -> element.text + } + } +} diff --git a/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbVisitor.kt b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbVisitor.kt new file mode 100644 index 00000000..3208277b --- /dev/null +++ b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbVisitor.kt @@ -0,0 +1,150 @@ +package com.sourcegraph.semanticdb_kotlinc + +import com.sourcegraph.semanticdb_javac.Semanticdb +import com.sourcegraph.semanticdb_javac.Semanticdb.SymbolOccurrence.Role +import java.nio.file.Path +import kotlin.contracts.ExperimentalContracts +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.containingClass + +@ExperimentalContracts +class SemanticdbVisitor( + sourceroot: Path, + private val resolver: DescriptorResolver, + private val file: KtFile, + private val lineMap: LineMap, + globals: GlobalSymbolsCache, + locals: LocalSymbolsCache = LocalSymbolsCache() +) : KtTreeVisitorVoid() { + private val cache = SymbolsCache(globals, locals) + private val documentBuilder = SemanticdbTextDocumentBuilder(sourceroot, file, lineMap, cache) + + private data class SymbolDescriptorPair( + val symbol: Symbol, + val descriptor: DeclarationDescriptor + ) + + fun build(): Semanticdb.TextDocument { + super.visitKtFile(file) + return documentBuilder.build() + } + + private fun Sequence?.emitAll( + element: PsiElement, + role: Semanticdb.SymbolOccurrence.Role + ): List? = + this?.onEach { (symbol, descriptor) -> + documentBuilder.emitSemanticdbData(symbol, descriptor, element, role) + } + ?.map { it.symbol } + ?.toList() + + private fun Sequence.with(descriptor: DeclarationDescriptor) = + this.map { SymbolDescriptorPair(it, descriptor) } + + override fun visitKtElement(element: KtElement) { + try { + super.visitKtElement(element) + } catch (e: VisitorException) { + throw e + } catch (e: Exception) { + throw VisitorException( + "exception throw when visiting ${element::class} in ${file.virtualFilePath}: (${ + lineMap.lineNumber( + element + ) + }, ${lineMap.startCharacter(element)})", + e) + } + } + + override fun visitObjectDeclaration(declaration: KtObjectDeclaration) { + if (declaration.name != null) { + val desc = resolver.fromDeclaration(declaration).single() + cache[desc].with(desc).emitAll(declaration, Semanticdb.SymbolOccurrence.Role.DEFINITION) + } + super.visitObjectDeclaration(declaration) + } + + override fun visitClass(klass: KtClass) { + val desc = resolver.fromDeclaration(klass).single() + cache[desc].with(desc).emitAll(klass, Semanticdb.SymbolOccurrence.Role.DEFINITION) + if (!klass.hasExplicitPrimaryConstructor()) { + resolver.syntheticConstructor(klass)?.apply { + cache[this].with(this).emitAll(klass, Semanticdb.SymbolOccurrence.Role.DEFINITION) + } + } + super.visitClass(klass) + } + + override fun visitPrimaryConstructor(constructor: KtPrimaryConstructor) { + val desc = resolver.fromDeclaration(constructor).single() + // if the constructor is not denoted by the 'constructor' keyword, we want to link it to the + // class ident + if (!constructor.hasConstructorKeyword()) { + cache[desc].with(desc).emitAll(constructor.containingClass()!!, Role.DEFINITION) + } else { + cache[desc].with(desc).emitAll(constructor.getConstructorKeyword()!!, Role.DEFINITION) + } + super.visitPrimaryConstructor(constructor) + } + + override fun visitSecondaryConstructor(constructor: KtSecondaryConstructor) { + val desc = resolver.fromDeclaration(constructor).single() + cache[desc].with(desc).emitAll(constructor.getConstructorKeyword(), Role.DEFINITION) + super.visitSecondaryConstructor(constructor) + } + + override fun visitNamedFunction(function: KtNamedFunction) { + val desc = resolver.fromDeclaration(function).single() + cache[desc].with(desc).emitAll(function, Role.DEFINITION) + super.visitNamedFunction(function) + } + + override fun visitProperty(property: KtProperty) { + val desc = resolver.fromDeclaration(property).single() + cache[desc].with(desc).emitAll(property, Role.DEFINITION) + super.visitProperty(property) + } + + override fun visitParameter(parameter: KtParameter) { + resolver + .fromDeclaration(parameter) + .flatMap { desc -> cache[desc].with(desc) } + .emitAll(parameter, Role.DEFINITION) + super.visitParameter(parameter) + } + + override fun visitTypeParameter(parameter: KtTypeParameter) { + val desc = resolver.fromDeclaration(parameter).single() + cache[desc].with(desc).emitAll(parameter, Role.DEFINITION) + super.visitTypeParameter(parameter) + } + + override fun visitTypeAlias(typeAlias: KtTypeAlias) { + val desc = resolver.fromDeclaration(typeAlias).single() + cache[desc].with(desc).emitAll(typeAlias, Role.DEFINITION) + super.visitTypeAlias(typeAlias) + } + + override fun visitPropertyAccessor(accessor: KtPropertyAccessor) { + val desc = resolver.fromDeclaration(accessor).single() + cache[desc].with(desc).emitAll(accessor, Role.DEFINITION) + super.visitPropertyAccessor(accessor) + } + + override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) { + val desc = + resolver.fromReference(expression) + ?: run { + super.visitSimpleNameExpression(expression) + return + } + cache[desc].with(desc).emitAll(expression, Role.REFERENCE) + super.visitSimpleNameExpression(expression) + } +} + +class VisitorException(msg: String, throwable: Throwable) : Exception(msg, throwable) diff --git a/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbWriter.kt b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbWriter.kt new file mode 100644 index 00000000..1dcacb77 --- /dev/null +++ b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbWriter.kt @@ -0,0 +1,7 @@ +package com.sourcegraph.semanticdb_kotlinc + +class SemanticdbWriter { + fun asdf() { + ExistentialType {} + } +} diff --git a/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SymbolsCache.kt b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SymbolsCache.kt new file mode 100644 index 00000000..b20c72db --- /dev/null +++ b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SymbolsCache.kt @@ -0,0 +1,316 @@ +package com.sourcegraph.semanticdb_kotlinc + +import com.sourcegraph.semanticdb_kotlinc.SemanticdbSymbolDescriptor.Kind +import java.lang.System.err +import kotlin.collections.ArrayList +import kotlin.collections.HashMap +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.impl.AnonymousFunctionDescriptor +import org.jetbrains.kotlin.descriptors.impl.TypeAliasConstructorDescriptor +import org.jetbrains.kotlin.descriptors.synthetic.FunctionInterfaceConstructorDescriptor +import org.jetbrains.kotlin.load.kotlin.JvmPackagePartSource +import org.jetbrains.kotlin.load.kotlin.toSourceElement +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.resolve.ImportedFromObjectCallableDescriptor +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe +import org.jetbrains.kotlin.resolve.descriptorUtil.module +import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter +import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered +import org.jetbrains.kotlin.resolve.source.getPsi +import org.jetbrains.kotlin.serialization.deserialization.descriptors.DescriptorWithContainerSource +import org.jetbrains.kotlin.types.TypeUtils +import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly + +@ExperimentalContracts +class GlobalSymbolsCache(testing: Boolean = false) : Iterable { + private val globals = + if (testing) LinkedHashMap() + else HashMap() + lateinit var resolver: DescriptorResolver + + operator fun get( + descriptor: DeclarationDescriptor, + locals: LocalSymbolsCache + ): Sequence = sequence { emitSymbols(descriptor, locals) } + + /** + * 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. + */ + private suspend fun SequenceScope.emitSymbols( + descriptor: DeclarationDescriptor, + locals: LocalSymbolsCache + ) { + yield(getSymbol(descriptor, locals)) + when (descriptor) { + is PropertyDescriptor -> { + if (descriptor.getter?.isDefault == true) emitSymbols(descriptor.getter!!, locals) + if (descriptor.setter?.isDefault == true) emitSymbols(descriptor.setter!!, locals) + } + } + } + + /** + * 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(descriptor: DeclarationDescriptor, locals: LocalSymbolsCache): Symbol { + globals[descriptor]?.let { + return it + } + locals[descriptor]?.let { + return it + } + return uncachedSemanticdbSymbol(descriptor, locals).also { + if (it.isGlobal()) globals[descriptor] = it + } + } + + private fun skip(desc: DeclarationDescriptor?): Boolean { + contract { returns(false) implies (desc != null) } + return desc == null || desc is ModuleDescriptor || desc is AnonymousFunctionDescriptor + } + + private fun uncachedSemanticdbSymbol( + descriptor: DeclarationDescriptor?, + locals: LocalSymbolsCache + ): Symbol { + if (skip(descriptor)) return Symbol.NONE + val ownerDesc = getParentDescriptor(descriptor) ?: return Symbol.ROOT_PACKAGE + + var owner = this.getSymbol(ownerDesc, locals) + if (ownerDesc.isObjectDeclaration() || + owner.isLocal() || + ownerDesc.isLocalVariable() || + ownerDesc is AnonymousFunctionDescriptor || + descriptor.isLocalVariable()) + return locals + descriptor + + // if is a top-level function or variable, Kotlin creates a wrapping class + if (((descriptor is FunctionDescriptor && + descriptor !is FunctionInterfaceConstructorDescriptor) || + descriptor is VariableDescriptor) && ownerDesc is PackageFragmentDescriptor) { + owner = + Symbol.createGlobal( + owner, + SemanticdbSymbolDescriptor( + Kind.TYPE, + sourceFileToClassSymbol( + descriptor.toSourceElement.containingFile, descriptor))) + } + + val semanticdbDescriptor = semanticdbDescriptor(descriptor) + return Symbol.createGlobal(owner, semanticdbDescriptor) + } + + /** + * 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`. + */ + private fun getParentDescriptor(descriptor: DeclarationDescriptor): DeclarationDescriptor? = + when (descriptor) { + is ModuleDescriptor -> { + val pkg = descriptor.getPackage(descriptor.fqNameSafe).fragments[0] + descriptor.getPackage(pkg.fqName.parent()).fragments[0] + } + is PackageFragmentDescriptor -> { + if (descriptor.fqNameSafe.isRoot) null + else descriptor.module.getPackage(descriptor.fqNameSafe.parent()) + } + else -> descriptor.containingDeclaration + } + + /** + * generates the synthetic class name from the source file + * https://kotlinlang.org/docs/java-to-kotlin-interop.html#package-level-functions + */ + private fun sourceFileToClassSymbol( + file: SourceFile, + descriptor: DeclarationDescriptor + ): String = + when (val name = file.name) { + null -> { + if (KotlinBuiltIns.isBuiltIn(descriptor)) "LibraryKt" + else if (descriptor is DescriptorWithContainerSource) { + val jvmPackagePartSource = descriptor.containerSource as JvmPackagePartSource + jvmPackagePartSource + .facadeClassName + ?.fqNameForClassNameWithoutDollars + ?.shortName() + ?.asString() + ?: jvmPackagePartSource.simpleName.asString() + } else { + DescriptorToSourceUtils.getEffectiveReferencedDescriptors(descriptor) + .first() + .fqNameUnsafe + .shortName() + .asString() + } + } + else -> name.replace(".kt", "Kt") + } + + private fun semanticdbDescriptor(desc: DeclarationDescriptor): SemanticdbSymbolDescriptor { + return when (desc) { + is FunctionInterfaceConstructorDescriptor -> + semanticdbDescriptor(desc.baseDescriptorForSynthetic) + is ClassDescriptor -> SemanticdbSymbolDescriptor(Kind.TYPE, desc.name.toString()) + is PropertySetterDescriptor -> + SemanticdbSymbolDescriptor( + Kind.METHOD, + "set" + desc.correspondingProperty.name.toString().capitalizeAsciiOnly()) + is PropertyGetterDescriptor -> + SemanticdbSymbolDescriptor( + Kind.METHOD, + "get" + desc.correspondingProperty.name.toString().capitalizeAsciiOnly()) + is FunctionDescriptor -> + SemanticdbSymbolDescriptor( + Kind.METHOD, desc.name.toString(), methodDisambiguator(desc)) + is TypeParameterDescriptor -> + SemanticdbSymbolDescriptor(Kind.TYPE_PARAMETER, desc.name.toString()) + is ValueParameterDescriptor -> + SemanticdbSymbolDescriptor(Kind.PARAMETER, desc.name.toString()) + is VariableDescriptor -> SemanticdbSymbolDescriptor(Kind.TERM, desc.name.toString()) + is TypeAliasDescriptor -> SemanticdbSymbolDescriptor(Kind.TYPE, desc.name.toString()) + is PackageFragmentDescriptor, is PackageViewDescriptor -> + SemanticdbSymbolDescriptor(Kind.PACKAGE, desc.name.toString()) + else -> { + err.println("unknown descriptor kind ${desc.javaClass.simpleName}") + SemanticdbSymbolDescriptor.NONE + } + } + } + + private fun methodDisambiguator(desc: FunctionDescriptor): String { + val ownerDecl = desc.containingDeclaration + val methods = + getAllMethods(desc, ownerDecl).filter { it.name == desc.name } as + ArrayList + + methods.sortWith { m1, m2 -> + compareValues( + m1.dispatchReceiverParameter == null, m2.dispatchReceiverParameter == null) + } + + val originalDesc = + when (desc) { + // if is a TypeAliasConstructorDescriptor, unwrap to get the descriptor of the + // underlying + // type. So much ceremony smh + is TypeAliasConstructorDescriptor -> desc.underlyingConstructorDescriptor + // kotlin equivalent of static import + is ImportedFromObjectCallableDescriptor<*> -> desc.callableFromObject + else -> desc.original + } + + // need to get original to get method without type projections + return when (val index = methods.indexOf(originalDesc)) { + 0 -> "()" + // help pls https://kotlinlang.slack.com/archives/C7L3JB43G/p1624995376114900 + // -1 -> throw IllegalStateException("failed to find method in parent:\n\t\tMethod: + // ${originalDesc}\n\t\tParent: ${ownerDecl.name}\n\t\tMethods: + // ${methods.joinToString("\n\t\t\t ")}") + else -> "(+$index)" + } + } + + private fun getAllMethods( + desc: FunctionDescriptor, + ownerDecl: DeclarationDescriptor + ): Collection = + when (ownerDecl) { + is PackageFragmentDescriptor -> + ownerDecl + .getMemberScope() + .getDescriptorsFiltered(DescriptorKindFilter.FUNCTIONS) + .map { it as CallableMemberDescriptor } + is ClassDescriptorWithResolutionScopes -> { + when (desc) { + is ClassConstructorDescriptor -> { + val constructors = + (desc.containingDeclaration as ClassDescriptorWithResolutionScopes) + .constructors as + ArrayList + // primary constructor always seems to be last, so move it to the start. + if (constructors.last().isPrimary) + constructors.add(0, constructors.removeLast()) + constructors + } + else -> ownerDecl.declaredCallableMembers + } + } + is FunctionDescriptor -> + ownerDecl.toSourceElement.getPsi()!! + .children + .first { it is KtBlockExpression } + .children + .filterIsInstance() + .map { resolver.fromDeclaration(it).single() as CallableMemberDescriptor } + is ClassDescriptor -> { + // Do we have to go recursively? + // https://sourcegraph.com/github.com/JetBrains/kotlin/-/blob/idea/src/org/jetbrains/kotlin/idea/actions/generate/utils.kt?L32:5 + val methods = + ownerDecl + .unsubstitutedMemberScope + .getContributedDescriptors() + .filterIsInstance() + val staticMethods = + ownerDecl + .staticScope + .getContributedDescriptors() + .filterIsInstance() + val ctors = ownerDecl.constructors.toList() + val allFuncs = + ArrayList(methods.size + ctors.size + staticMethods.size) + allFuncs.addAll(ctors) + allFuncs.addAll(methods) + allFuncs.addAll(staticMethods) + allFuncs + } + is TypeAliasDescriptor -> { + // We get the underlying class descriptor and restart the process recursively + getAllMethods(desc, TypeUtils.getClassDescriptor(ownerDecl.underlyingType)!!) + } + else -> + throw IllegalStateException( + "unexpected owner decl type '${ownerDecl.javaClass}':\n\t\tMethod: ${desc}\n\t\tParent: $ownerDecl") + } + + override fun iterator(): Iterator = globals.values.iterator() +} + +class LocalSymbolsCache : Iterable { + private val symbols = HashMap() + private var localsCounter = 0 + + val iterator: Iterable> + get() = symbols.asIterable() + + val size: Int + get() = symbols.size + + operator fun get(desc: DeclarationDescriptor): Symbol? = symbols[desc] + + operator fun plus(desc: DeclarationDescriptor): Symbol { + val result = Symbol.createLocal(localsCounter++) + symbols[desc] = result + return result + } + + override fun iterator(): Iterator = symbols.values.iterator() +} + +@ExperimentalContracts +class SymbolsCache(private val globals: GlobalSymbolsCache, private val locals: LocalSymbolsCache) { + operator fun get(descriptor: DeclarationDescriptor) = globals[descriptor, locals] +} diff --git a/semanticdb-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor b/semanticdb-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor new file mode 100644 index 00000000..ea687ae9 --- /dev/null +++ b/semanticdb-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor @@ -0,0 +1 @@ +com.sourcegraph.semanticdb_kotlinc.AnalyzerCommandLineProcessor \ No newline at end of file diff --git a/semanticdb-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar b/semanticdb-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar new file mode 100644 index 00000000..f7d02a4a --- /dev/null +++ b/semanticdb-kotlinc/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar @@ -0,0 +1 @@ +com.sourcegraph.semanticdb_kotlinc.AnalyzerRegistrar \ No newline at end of file diff --git a/semanticdb-kotlinc/src/snapshots/kotlin/com/sourcegraph/lsif_kotlin/Snapshot.kt b/semanticdb-kotlinc/src/snapshots/kotlin/com/sourcegraph/lsif_kotlin/Snapshot.kt new file mode 100644 index 00000000..43b92d3b --- /dev/null +++ b/semanticdb-kotlinc/src/snapshots/kotlin/com/sourcegraph/lsif_kotlin/Snapshot.kt @@ -0,0 +1,29 @@ +package com.sourcegraph.lsif_kotlin + +import com.sourcegraph.scip_java.ScipJava +import kotlin.io.path.Path + +fun main() { + val snapshotDir = Path(System.getProperty("snapshotDir")) + val sourceroot = Path(System.getProperty("sourceroot")) + val targetroot = Path(System.getProperty("targetroot")) + + ScipJava.main(arrayOf( + "index-semanticdb", + "--no-emit-inverse-relationships", + "--cwd", + sourceroot.toString(), + "--output", + targetroot.resolve("index.scip").toString(), + targetroot.toString() + )) + ScipJava.main(arrayOf( + "snapshot", + "--cwd", + sourceroot.toString(), + "--output", + snapshotDir.toString(), + targetroot.toString() + )) +} + diff --git a/semanticdb-kotlinc/src/test/kotlin/com/sourcegraph/semanticdb_kotlinc/test/AnalyzerTest.kt b/semanticdb-kotlinc/src/test/kotlin/com/sourcegraph/semanticdb_kotlinc/test/AnalyzerTest.kt new file mode 100644 index 00000000..b5ee422e --- /dev/null +++ b/semanticdb-kotlinc/src/test/kotlin/com/sourcegraph/semanticdb_kotlinc/test/AnalyzerTest.kt @@ -0,0 +1,613 @@ +package com.sourcegraph.semanticdb_kotlinc.test + +import com.sourcegraph.semanticdb_kotlinc.* +import com.sourcegraph.semanticdb_kotlinc.Semanticdb.Language.KOTLIN +import com.sourcegraph.semanticdb_kotlinc.Semanticdb.SymbolOccurrence.Role +import com.sourcegraph.semanticdb_kotlinc.Semanticdb.TextDocument +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.PluginOption +import com.tschuchort.compiletesting.SourceFile +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.fail +import io.kotest.assertions.withClue +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import java.io.File +import java.nio.file.Path +import kotlin.contracts.ExperimentalContracts +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 + +@OptIn(ExperimentalCompilerApi::class) +@ExperimentalContracts +class AnalyzerTest { + fun compileSemanticdb(path: Path, @Language("kotlin") code: String): TextDocument { + val buildPath = File(path.resolve("build").toString()).apply { mkdir() } + val source = SourceFile.testKt(code) + lateinit var document: TextDocument + + val result = + KotlinCompilation() + .apply { + sources = listOf(source) + componentRegistrars = listOf(AnalyzerRegistrar { document = it }) + verbose = false + pluginOptions = + listOf( + PluginOption("semanticdb-kotlinc", "sourceroot", path.toString()), + PluginOption("semanticdb-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 = + compileSemanticdb( + path, + """ + package sample + class Banana { + fun foo() { } + }""") + + val occurrences = + arrayOf( + SymbolOccurrence { + role = Role.REFERENCE + symbol = "sample/" + range { + startLine = 0 + startCharacter = 8 + endLine = 0 + endCharacter = 14 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "sample/Banana#" + range { + startLine = 1 + startCharacter = 6 + endLine = 1 + endCharacter = 12 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "sample/Banana#foo()." + range { + startLine = 2 + startCharacter = 8 + endLine = 2 + endCharacter = 11 + } + }) + assertSoftly(document.occurrencesList) { + withClue(this) { occurrences.forEach(::shouldContain) } + } + + val symbols = + arrayOf( + SymbolInformation { + symbol = "sample/Banana#" + language = KOTLIN + displayName = "Banana" + documentation = + Documentation { + format = Semanticdb.Documentation.Format.MARKDOWN + message = "```kotlin\npublic final class Banana\n```" + } + }, + SymbolInformation { + symbol = "sample/Banana#foo()." + language = KOTLIN + displayName = "foo" + documentation = + Documentation { + format = Semanticdb.Documentation.Format.MARKDOWN + message = "```kotlin\npublic final fun foo()\n```" + } + }) + assertSoftly(document.symbolsList) { withClue(this) { symbols.forEach(::shouldContain) } } + } + + @Test + fun `exception test`(@TempDir path: Path) { + val buildPath = File(path.resolve("build").toString()).apply { mkdir() } + val result = + KotlinCompilation() + .apply { + sources = listOf(SourceFile.testKt("")) + componentRegistrars = listOf(AnalyzerRegistrar { throw Exception("sample text") }) + verbose = false + pluginOptions = + listOf( + PluginOption("semanticdb-kotlinc", "sourceroot", path.toString()), + PluginOption("semanticdb-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) + componentRegistrars = listOf(AnalyzerRegistrar()) + verbose = false + pluginOptions = + listOf( + PluginOption("semanticdb-kotlinc", "sourceroot", path.toString()), + PluginOption("semanticdb-kotlinc", "targetroot", buildPath.toString())) + commandLineProcessors = listOf(AnalyzerCommandLineProcessor()) + workingDir = path.toFile() + } + .compile() + + result.exitCode shouldBe KotlinCompilation.ExitCode.OK + } + + @Test + fun documentation(@TempDir path: Path) { + val document = + compileSemanticdb( + 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/TestKt#docstrings().", "Example method docstring") + } + + private fun TextDocument.assertDocumentation(symbol: String, expectedDocumentation: String) { + val markdown = + this.symbolsList.find { it.symbol == symbol }?.documentation?.message + ?: fail("no documentation for symbol $symbol") + val obtainedDocumentation = markdown.split("----").last().trim() + assertEquals(expectedDocumentation, obtainedDocumentation) + } +} diff --git a/semanticdb-kotlinc/src/test/kotlin/com/sourcegraph/semanticdb_kotlinc/test/SemanticdbSymbolsTest.kt b/semanticdb-kotlinc/src/test/kotlin/com/sourcegraph/semanticdb_kotlinc/test/SemanticdbSymbolsTest.kt new file mode 100644 index 00000000..fcbdcffb --- /dev/null +++ b/semanticdb-kotlinc/src/test/kotlin/com/sourcegraph/semanticdb_kotlinc/test/SemanticdbSymbolsTest.kt @@ -0,0 +1,637 @@ +package com.sourcegraph.semanticdb_kotlinc.test + +import com.sourcegraph.semanticdb_kotlinc.* +import com.sourcegraph.semanticdb_kotlinc.Semanticdb.Documentation.Format +import com.sourcegraph.semanticdb_kotlinc.Semanticdb.Language +import com.sourcegraph.semanticdb_kotlinc.Semanticdb.SymbolOccurrence.Role +import com.sourcegraph.semanticdb_kotlinc.test.ExpectedSymbols.SemanticdbData +import com.sourcegraph.semanticdb_kotlinc.test.ExpectedSymbols.SymbolCacheData +import com.tschuchort.compiletesting.SourceFile +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import kotlin.contracts.ExperimentalContracts +import org.junit.jupiter.api.TestFactory + +@ExperimentalCompilerApi +@ExperimentalContracts +class SemanticdbSymbolsTest { + @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("TestKt#test().".symbol(), "TestKt#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/MapsKt#mapOf(+1).".symbol(), + "kotlin/io/ConsoleKt#println().".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()), + semanticdb = + SemanticdbData( + expectedOccurrences = + listOf( + SymbolOccurrence { + role = Role.DEFINITION + symbol = "TestKt#x." + range { + startLine = 0 + startCharacter = 4 + endLine = 0 + endCharacter = 5 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "TestKt#getX()." + range { + startLine = 0 + startCharacter = 4 + endLine = 0 + endCharacter = 5 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "TestKt#setX()." + range { + startLine = 0 + startCharacter = 4 + endLine = 0 + endCharacter = 5 + } + })), + ), + ExpectedSymbols( + "top level properties - explicit getter", + SourceFile.testKt( + """ + |var x: Int = 5 + | get() = field + 10 + |""".trimMargin()), + semanticdb = + SemanticdbData( + expectedOccurrences = + listOf( + SymbolOccurrence { + role = Role.DEFINITION + symbol = "TestKt#x." + range { + startLine = 0 + startCharacter = 4 + endLine = 0 + endCharacter = 5 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "TestKt#setX()." + range { + startLine = 0 + startCharacter = 4 + endLine = 0 + endCharacter = 5 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "TestKt#getX()." + range { + startLine = 1 + startCharacter = 4 + endLine = 1 + endCharacter = 7 + } + })), + ), + ExpectedSymbols( + "top level properties - explicit setter", + SourceFile.testKt( + """ + |var x: Int = 5 + | set(value) { field = value + 5 } + |""".trimMargin()), + semanticdb = + SemanticdbData( + expectedOccurrences = + listOf( + SymbolOccurrence { + role = Role.DEFINITION + symbol = "TestKt#x." + range { + startLine = 0 + startCharacter = 4 + endLine = 0 + endCharacter = 5 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "TestKt#getX()." + range { + startLine = 0 + startCharacter = 4 + endLine = 0 + endCharacter = 5 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "TestKt#setX()." + range { + startLine = 1 + startCharacter = 4 + endLine = 1 + endCharacter = 7 + } + })), + ), + ExpectedSymbols( + "top level properties - explicit getter & setter", + SourceFile.testKt( + """ + |var x: Int = 5 + | get() = field + 10 + | set(value) { field = value + 10 } + |""".trimMargin()), + semanticdb = + SemanticdbData( + expectedOccurrences = + listOf( + SymbolOccurrence { + role = Role.DEFINITION + symbol = "TestKt#x." + range { + startLine = 0 + startCharacter = 4 + endLine = 0 + endCharacter = 5 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "TestKt#getX()." + range { + startLine = 1 + startCharacter = 4 + endLine = 1 + endCharacter = 7 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "TestKt#setX()." + range { + startLine = 2 + startCharacter = 4 + endLine = 2 + endCharacter = 7 + } + })), + ), + ExpectedSymbols( + "class constructor properties", + SourceFile.testKt( + """ + |class Test(var sample: Int, text: String): Throwable(sample.toString()) { + | fun test() { + | println(sample) + | } + |} + |""".trimMargin()), + semanticdb = + SemanticdbData( + expectedOccurrences = + listOf( + SymbolOccurrence { + role = Role.DEFINITION + symbol = "Test#sample." + range { + startLine = 0 + startCharacter = 15 + endLine = 0 + endCharacter = 21 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "Test#getSample()." + range { + startLine = 0 + startCharacter = 15 + endLine = 0 + endCharacter = 21 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "Test#setSample()." + range { + startLine = 0 + startCharacter = 15 + endLine = 0 + endCharacter = 21 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "Test#``().(sample)" + range { + startLine = 0 + startCharacter = 15 + endLine = 0 + endCharacter = 21 + } + }, + SymbolOccurrence { + role = Role.REFERENCE + symbol = "Test#``().(sample)" + range { + startLine = 0 + startCharacter = 53 + endLine = 0 + endCharacter = 59 + } + }, + SymbolOccurrence { + role = Role.REFERENCE + symbol = "Test#sample." + range { + startLine = 2 + startCharacter = 16 + endLine = 2 + endCharacter = 22 + } + }, + SymbolOccurrence { + role = 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()), + semanticdb = + SemanticdbData( + expectedOccurrences = + listOf( + SymbolOccurrence { + role = Role.DEFINITION + symbol = "Banana#" + range { + startLine = 0 + startCharacter = 6 + endLine = 0 + endCharacter = 12 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "Banana#``()." + range { + startLine = 0 + startCharacter = 6 + endLine = 0 + endCharacter = 12 + } + }, + ))), + ExpectedSymbols( + "explicit primary constructor without keyword", + SourceFile.testKt( + """ + |class Banana(size: Int) + |""".trimMargin()), + semanticdb = + SemanticdbData( + expectedOccurrences = + listOf( + SymbolOccurrence { + role = Role.DEFINITION + symbol = "Banana#" + range { + startLine = 0 + startCharacter = 6 + endLine = 0 + endCharacter = 12 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "Banana#``()." + range { + startLine = 0 + startCharacter = 6 + endLine = 0 + endCharacter = 12 + } + }, + ))), + ExpectedSymbols( + "explicit primary constructor with keyword", + SourceFile.testKt( + """ + |class Banana constructor(size: Int) + |""".trimMargin()), + semanticdb = + SemanticdbData( + expectedOccurrences = + listOf( + SymbolOccurrence { + role = Role.DEFINITION + symbol = "Banana#" + range { + startLine = 0 + startCharacter = 6 + endLine = 0 + endCharacter = 12 + } + }, + SymbolOccurrence { + role = Role.DEFINITION + symbol = "Banana#``()." + range { + startLine = 0 + startCharacter = 13 + endLine = 0 + endCharacter = 24 + } + }, + )))) + .mapCheckExpectedSymbols() + + @TestFactory + fun `Single Abstract Method interface`() = + listOf( + ExpectedSymbols( + "basic java.lang.Runnable", + SourceFile.testKt( + """ + |val x = Runnable { }.run() + |""".trimMargin()), + semanticdb = + SemanticdbData( + expectedOccurrences = + listOf( + SymbolOccurrence { + role = Role.REFERENCE + symbol = "java/lang/Runnable#" + range { + startLine = 0 + startCharacter = 8 + endLine = 0 + endCharacter = 16 + } + }, + SymbolOccurrence { + role = 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()), + semanticdb = + SemanticdbData( + expectedSymbols = + listOf( + SymbolInformation { + symbol = "TestKt#x." + displayName = "x" + language = Language.KOTLIN + documentation { + message = + "```kotlin\npublic val x: kotlin.String\n```\n\n----\n\n\nhello world\n test content\n" + format = Format.MARKDOWN + } + }, + SymbolInformation { + symbol = "TestKt#getX()." + displayName = "x" + language = Language.KOTLIN + documentation { + message = + "```kotlin\npublic val x: kotlin.String\n```\n\n----\n\n\nhello world\n test content\n" + format = Format.MARKDOWN + } + })))) + .mapCheckExpectedSymbols() +} diff --git a/semanticdb-kotlinc/src/test/kotlin/com/sourcegraph/semanticdb_kotlinc/test/Utils.kt b/semanticdb-kotlinc/src/test/kotlin/com/sourcegraph/semanticdb_kotlinc/test/Utils.kt new file mode 100644 index 00000000..34f22467 --- /dev/null +++ b/semanticdb-kotlinc/src/test/kotlin/com/sourcegraph/semanticdb_kotlinc/test/Utils.kt @@ -0,0 +1,155 @@ +package com.sourcegraph.semanticdb_kotlinc.test + +import com.sourcegraph.semanticdb_kotlinc.* +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 kotlin.contracts.ExperimentalContracts +import org.intellij.lang.annotations.Language +import org.jetbrains.kotlin.analyzer.AnalysisResult +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension +import org.junit.jupiter.api.Assumptions.assumeFalse +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.DynamicTest.dynamicTest + +data class ExpectedSymbols( + val testName: String, + val source: SourceFile, + val symbolsCacheData: SymbolCacheData? = null, + val semanticdb: SemanticdbData? = null +) { + data class SemanticdbData( + 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 +@ExperimentalContracts +fun List.mapCheckExpectedSymbols(): List = + this.flatMap { (testName, source, symbolsData, semanticdbData) -> + val globals = GlobalSymbolsCache(testing = true) + val locals = LocalSymbolsCache() + lateinit var document: Semanticdb.TextDocument + 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 - semanticdb") { + semanticdbData?.apply { + println( + "checking semanticdb: ${expectedOccurrences?.size ?: 0} occurrences and ${expectedSymbols?.size ?: 0} symbols") + checkContainsExpectedSemanticdb(document, expectedOccurrences, expectedSymbols) + } + ?: assumeFalse(true) + }) + } + +@ExperimentalContracts +fun checkContainsExpectedSymbols( + globals: GlobalSymbolsCache, + locals: LocalSymbolsCache, + expectedGlobals: List?, + localsCount: Int? = null +) { + assertSoftly(globals) { expectedGlobals?.let { this.shouldContainInOrder(it) } } + localsCount?.also { locals.size shouldBe it } +} + +@ExperimentalContracts +fun checkContainsExpectedSemanticdb( + document: Semanticdb.TextDocument, + expectedOccurrences: List?, + expectedSymbols: List? +) { + assertSoftly(document.occurrencesList) { + expectedOccurrences?.let { this.shouldContainInOrder(it) } + } + assertSoftly(document.symbolsList) { expectedSymbols?.let { this.shouldContainInOrder(it) } } +} + +@OptIn(ExperimentalCompilerApi::class) +@ExperimentalContracts +private fun configureTestCompiler( + source: SourceFile, + globals: GlobalSymbolsCache, + locals: LocalSymbolsCache, + hook: (Semanticdb.TextDocument) -> Unit = {} +): KotlinCompilation { + val compilation = + KotlinCompilation().apply { + sources = listOf(source) + inheritClassPath = true + verbose = false + } + + val analyzer = semanticdbVisitorAnalyzer(globals, locals, compilation.workingDir.toPath(), hook) + compilation.apply { componentRegistrars = listOf(analyzer) } + return compilation +} + +@OptIn(ExperimentalCompilerApi::class) +@ExperimentalContracts +fun semanticdbVisitorAnalyzer( + globals: GlobalSymbolsCache, + locals: LocalSymbolsCache, + sourceroot: Path, + hook: (Semanticdb.TextDocument) -> Unit = {} +): ComponentRegistrar { + return object : ComponentRegistrar { + override fun registerProjectComponents( + project: MockProject, + configuration: CompilerConfiguration + ) { + AnalysisHandlerExtension.registerExtension( + project, + object : AnalysisHandlerExtension { + override fun analysisCompleted( + project: Project, + module: ModuleDescriptor, + bindingTrace: BindingTrace, + files: Collection + ): AnalysisResult? { + val resolver = + DescriptorResolver(bindingTrace).also { globals.resolver = it } + val lineMap = LineMap(project, files.first()) + hook( + SemanticdbVisitor( + sourceroot, resolver, files.first(), lineMap, globals, locals) + .build()) + return super.analysisCompleted(project, module, bindingTrace, files) + } + }) + } + } +}