diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 93d930c20..a028c2915 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,9 +2,9 @@ clikt = "5.1.0" gradle-api = "8.11.1" junit-jupiter = "5.11.4" -kctfork = "0.7.1" +kctfork = "0.13.0" kotest = "6.2.1" -kotlin = "2.2.0" +kotlin = "2.4.0" kotlinx-serialization = "1.11.0" lombok = "1.18.46" maven-plugin-annotations = "3.15.2" diff --git a/scip-kotlinc/build.gradle.kts b/scip-kotlinc/build.gradle.kts index 699d24dd9..aad935acf 100644 --- a/scip-kotlinc/build.gradle.kts +++ b/scip-kotlinc/build.gradle.kts @@ -24,10 +24,6 @@ dependencies { testImplementation(libs.kctfork.core) } -tasks.withType().configureEach { - compilerOptions.freeCompilerArgs.add("-Xcontext-parameters") -} - tasks.named("test") { maxHeapSize = "2g" } diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt index 37ece4b57..d3d0259ce 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCheckers.kt @@ -11,19 +11,23 @@ import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext import org.jetbrains.kotlin.fir.analysis.checkers.declaration.* import org.jetbrains.kotlin.fir.analysis.checkers.expression.ExpressionCheckers import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirQualifiedAccessExpressionChecker +import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirResolvedQualifierChecker import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirTypeOperatorCallChecker -import org.jetbrains.kotlin.fir.analysis.checkers.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.declarations.utils.isCompanion import org.jetbrains.kotlin.fir.expressions.FirQualifiedAccessExpression +import org.jetbrains.kotlin.fir.expressions.FirResolvedQualifier import org.jetbrains.kotlin.fir.expressions.FirTypeOperatorCall import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference import org.jetbrains.kotlin.fir.resolve.calls.FirSyntheticFunctionSymbol +import org.jetbrains.kotlin.fir.resolve.getContainingClassSymbol import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider import org.jetbrains.kotlin.fir.resolve.toClassLikeSymbol import org.jetbrains.kotlin.fir.symbols.impl.FirAnonymousObjectSymbol import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol +import org.jetbrains.kotlin.fir.types.FirTypeRef import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName @@ -36,6 +40,15 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio element.treeStructure .findChildByType(element.lighterASTNode, KtTokens.IDENTIFIER) ?.toKtLightSourceElement(element.treeStructure) ?: element + + context(context: CheckerContext) + private fun ScipVisitor.emitTypeRef(typeRef: FirTypeRef) { + val klass = typeRef.toClassLikeSymbol(context.session) + val source = typeRef.source + if (klass != null && source != null && source.kind !is KtFakeSourceElementKind) { + visitClassReference(klass, getIdentifier(source)) + } + } } override val declarationCheckers: DeclarationCheckers @@ -48,8 +61,12 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio Set = setOf(SemanticQualifiedAccessExpressionChecker()) + override val resolvedQualifierCheckers: + Set = + setOf(SemanticResolvedQualifierChecker()) + override val typeOperatorCallCheckers: - Set = + Set = setOf(SemanticClassReferenceExpressionChecker()) } @@ -71,6 +88,8 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio override val typeAliasCheckers: Set = setOf(SemanticTypeAliasChecker()) override val propertyAccessorCheckers: Set = setOf(SemanticPropertyAccessorChecker()) + override val enumEntryCheckers: Set = + setOf(SemanticEnumEntryChecker()) } private class SemanticFileChecker(private val sourceroot: Path) : @@ -134,7 +153,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio if (names != null) { eachFqNameElement(fqName, source.treeStructure, names) { fqName, name -> - visitor?.visitPackage(fqName, name, context) + visitor?.visitPackage(fqName, name) } } } @@ -161,13 +180,13 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio ) if (klass != null) { - visitor?.visitClassReference(klass, name, context) + visitor?.visitClassReference(klass, name) } else if (callables.isNotEmpty()) { for (callable in callables) { - visitor?.visitCallableReference(callable, name, context) + visitor?.visitCallableReference(callable, name) } } else { - visitor?.visitPackage(fqName, name, context) + visitor?.visitPackage(fqName, name) } } } @@ -179,7 +198,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirClassLikeDeclaration) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] val objectKeyword = if (declaration is FirAnonymousObject) { @@ -189,20 +208,29 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio } else { null } + val identifierSource = getIdentifier(source) + // For unnamed companion objects, getIdentifier() falls back to source (no IDENTIFIER + // token). Use the 'companion' keyword as the range instead. The COMPANION_KEYWORD is + // inside a MODIFIER_LIST child, so we use findDescendantByType instead of + // findChildByType. + val companionKeyword = + if (identifierSource === source && declaration is FirRegularClass && declaration.isCompanion) { + source + .treeStructure + .findDescendantByType(source.lighterASTNode, KtTokens.COMPANION_KEYWORD) + ?.toKtLightSourceElement(source.treeStructure) + } else { + null + } visitor?.visitClassOrObject( declaration, - objectKeyword ?: getIdentifier(source), - context, - enclosingSource = source, + objectKeyword ?: companionKeyword ?: identifierSource, + enclosingSource = source ) if (declaration is FirClass) { for (superType in declaration.superTypeRefs) { - val superSymbol = superType.toClassLikeSymbol(context.session) - val superSource = superType.source - if (superSymbol != null && superSource != null) { - visitor?.visitClassReference(superSymbol, superSource, context) - } + visitor?.emitTypeRef(superType) } } } @@ -212,7 +240,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirConstructor) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] if (declaration.isPrimary) { @@ -238,14 +266,12 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio visitor?.visitPrimaryConstructor( declaration, constructorKeyboard ?: objectKeyword ?: getIdentifier(klassSource), - context, enclosingSource = source, ) } else { visitor?.visitSecondaryConstructor( declaration, getIdentifier(source), - context, enclosingSource = source, ) } @@ -254,24 +280,17 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio private class SemanticSimpleFunctionChecker : FirSimpleFunctionChecker(MppCheckerKind.Common) { context(context: CheckerContext, reporter: DiagnosticReporter) - override fun check(declaration: FirSimpleFunction) { + override fun check(declaration: FirNamedFunction) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.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) - } + visitor?.emitTypeRef(declaration.returnTypeRef) + declaration.receiverParameter?.typeRef?.let { visitor?.emitTypeRef(it) } } } @@ -280,9 +299,9 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirAnonymousFunction) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] - visitor?.visitNamedFunction(declaration, source, context, enclosingSource = source) + visitor?.visitNamedFunction(declaration, source, enclosingSource = source) } } @@ -290,22 +309,15 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirProperty) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitProperty( declaration, getIdentifier(source), - context, enclosingSource = source, ) - - 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) - } + visitor?.emitTypeRef(declaration.returnTypeRef) + declaration.receiverParameter?.typeRef?.let { visitor?.emitTypeRef(it) } } } @@ -313,22 +325,14 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirValueParameter) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitParameter( declaration, getIdentifier(source), - context, enclosingSource = source, ) - - 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) - } + visitor?.emitTypeRef(declaration.returnTypeRef) } } @@ -336,12 +340,11 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirTypeParameter) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitTypeParameter( declaration, getIdentifier(source), - context, enclosingSource = source, ) } @@ -351,12 +354,11 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirTypeAlias) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitTypeAlias( declaration, getIdentifier(source), - context, enclosingSource = source, ) } @@ -367,7 +369,7 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(declaration: FirPropertyAccessor) { val source = declaration.source ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] val identifierSource = if (declaration.isGetter) { @@ -385,12 +387,38 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio visitor?.visitPropertyAccessor( declaration, identifierSource, - context, enclosingSource = source, ) } } + private class SemanticEnumEntryChecker : FirEnumEntryChecker(MppCheckerKind.Common) { + context(context: CheckerContext, reporter: DiagnosticReporter) + override fun check(declaration: FirEnumEntry) { + val source = declaration.source ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return + val visitor = visitors[ktFile] + visitor?.visitEnumEntry( + declaration, + getIdentifier(source), + enclosingSource = source, + ) + } + } + + private class SemanticResolvedQualifierChecker : + FirResolvedQualifierChecker(MppCheckerKind.Common) { + context(context: CheckerContext, reporter: DiagnosticReporter) + override fun check(expression: FirResolvedQualifier) { + val symbol = expression.symbol ?: return + val source = expression.source ?: return + if (source.kind is KtFakeSourceElementKind) return + val ktFile = context.containingFileSymbol?.sourceFile ?: return + val visitor = visitors[ktFile] + visitor?.visitClassReference(symbol, getIdentifier(source)) + } + } + private class SemanticQualifiedAccessExpressionChecker : FirQualifiedAccessExpressionChecker(MppCheckerKind.Common) { context(context: CheckerContext, reporter: DiagnosticReporter) @@ -401,13 +429,10 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio return } - val ktFile = context.containingFile?.sourceFile ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] - visitor?.visitSimpleNameExpression( - calleeReference, - getIdentifier(calleeReference.source ?: source), - context, - ) + val identifierSource = getIdentifier(calleeReference.source ?: source) + visitor?.visitSimpleNameExpression(calleeReference, identifierSource) val resolvedSymbol = calleeReference.resolvedSymbol if ( @@ -415,12 +440,11 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio resolvedSymbol is FirSyntheticFunctionSymbol ) { val referencedKlass = - resolvedSymbol.resolvedReturnType.toClassLikeSymbol(context.session) + resolvedSymbol.resolvedReturnType.toClassLikeSymbol() if (referencedKlass != null) { visitor?.visitClassReference( referencedKlass, - getIdentifier(calleeReference.source ?: source), - context, + identifierSource, ) } } @@ -431,15 +455,13 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio resolvedSymbol.getterSymbol?.let { visitor?.visitCallableReference( it, - getIdentifier(calleeReference.source ?: source), - context, + identifierSource, ) } resolvedSymbol.setterSymbol?.let { visitor?.visitCallableReference( it, - getIdentifier(calleeReference.source ?: source), - context, + identifierSource, ) } } @@ -452,15 +474,13 @@ open class AnalyzerCheckers(session: FirSession) : FirAdditionalCheckersExtensio override fun check(expression: FirTypeOperatorCall) { val typeRef = expression.conversionTypeRef val source = typeRef.source ?: return - val classSymbol = - expression.conversionTypeRef.toClassLikeSymbol(context.session) ?: return - val ktFile = context.containingFile?.sourceFile ?: return + val classSymbol = typeRef.toClassLikeSymbol(context.session) ?: return + val ktFile = context.containingFileSymbol?.sourceFile ?: return val visitor = visitors[ktFile] visitor?.visitClassReference( classSymbol, - getIdentifier(expression.conversionTypeRef.source ?: source), - context, + getIdentifier(source), ) } } diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCommandLineProcessor.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCommandLineProcessor.kt index 45bb987b2..601511099 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCommandLineProcessor.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerCommandLineProcessor.kt @@ -15,9 +15,11 @@ val KEY_SOURCES = CompilerConfigurationKey(VAL_SOURCES) const val VAL_TARGET = "targetroot" val KEY_TARGET = CompilerConfigurationKey(VAL_TARGET) +const val PLUGIN_ID = "scip-kotlinc" + @OptIn(ExperimentalCompilerApi::class) class AnalyzerCommandLineProcessor : CommandLineProcessor { - override val pluginId: String = "scip-kotlinc" + override val pluginId: String = PLUGIN_ID override val pluginOptions: Collection = listOf( CliOption( diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt index 289a0cf4e..5833d8883 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/AnalyzerRegistrar.kt @@ -19,6 +19,7 @@ class AnalyzerRegistrar(private val callback: (Document) -> Unit = {}) : Compile FirExtensionRegistrarAdapter.registerExtension(AnalyzerFirExtensionRegistrar(options)) IrGenerationExtension.registerExtension( PostAnalysisExtension( + configuration = configuration, sourceRoot = options.sourceroot, targetRoot = options.targetroot, callback = callback, @@ -26,6 +27,8 @@ class AnalyzerRegistrar(private val callback: (Document) -> Unit = {}) : Compile ) } + override val pluginId = PLUGIN_ID + override val supportsK2: Boolean get() = true } diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/LineMap.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/LineMap.kt index c2da05fdf..afd51bd0a 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/LineMap.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/LineMap.kt @@ -10,22 +10,22 @@ class LineMap(private val file: FirFile) { private fun offsetToLineAndCol(offset: Int): Pair? = file.sourceFileLinesMapping?.getLineAndColumnByOffset(offset) - /** Returns the non-0-based line number for a given offset */ + /** Returns the 1-based line number for a given offset (subtract 1 for protobuf). */ fun lineNumberForOffset(offset: Int): Int = file.sourceFileLinesMapping?.getLineByOffset(offset)?.let { it + 1 } ?: 0 - /** Returns the non-0-based column number for a given offset */ + /** Returns the 0-based column number for a given offset. */ fun columnForOffset(offset: Int): Int = offsetToLineAndCol(offset)?.second ?: 0 - /** Returns the non-0-based start character */ + /** Returns the 0-based start character. */ fun startCharacter(element: KtSourceElement): Int = offsetToLineAndCol(element.startOffset)?.second ?: 0 - /** Returns the non-0-based end character */ + /** Returns the 0-based end character. */ fun endCharacter(element: KtSourceElement): Int = startCharacter(element) + nameForOffset(element).length - /** Returns the non-0-based line number */ + /** Returns the 1-based line number (subtract 1 for protobuf). */ fun lineNumber(element: KtSourceElement): Int = file.sourceFileLinesMapping?.getLineByOffset(element.startOffset)?.let { it + 1 } ?: 0 diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt index 54ea38fa6..fb01151a5 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/PostAnalysisExtension.kt @@ -25,6 +25,7 @@ import org.scip_code.scip_java.shared.ScipShardWriter * outside the source root are skipped with a stderr warning. */ class PostAnalysisExtension( + private val configuration: CompilerConfiguration, private val sourceRoot: Path, private val targetRoot: Path, private val callback: (Document) -> Unit, @@ -45,6 +46,7 @@ class PostAnalysisExtension( } catch (e: Exception) { handleException(e) } + AnalyzerCheckers.visitors.clear() } private fun scipShardPathForFile(file: KtSourceFile): Path? { @@ -60,11 +62,10 @@ class PostAnalysisExtension( } private val messageCollector = - CompilerConfiguration() - .get( - CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, - PrintingMessageCollector(System.err, MessageRenderer.PLAIN_FULL_PATHS, false), - ) + configuration.get( + CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, + PrintingMessageCollector(System.err, MessageRenderer.PLAIN_FULL_PATHS, false) + ) private fun handleException(e: Exception) { val writer = @@ -73,7 +74,7 @@ class PostAnalysisExtension( val buf = StringBuffer() override fun close() = - messageCollector.report(CompilerMessageSeverity.EXCEPTION, buf.toString()) + messageCollector.report(CompilerMessageSeverity.WARNING, buf.toString()) override fun flush() = Unit diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt index f767dda2b..6aabbb8f4 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipTextDocumentBuilder.kt @@ -5,10 +5,13 @@ import java.nio.file.Paths import org.jetbrains.kotlin.KtSourceElement import org.jetbrains.kotlin.KtSourceFile import org.jetbrains.kotlin.fir.FirElement +import org.jetbrains.kotlin.fir.FirPackageDirective import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext import org.jetbrains.kotlin.fir.analysis.checkers.directOverriddenSymbolsSafe import org.jetbrains.kotlin.fir.analysis.checkers.toClassLikeSymbol import org.jetbrains.kotlin.fir.analysis.getChild +import org.jetbrains.kotlin.fir.declarations.* +import org.jetbrains.kotlin.fir.declarations.utils.isInterface import org.jetbrains.kotlin.fir.renderer.* import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol import org.jetbrains.kotlin.fir.symbols.SymbolInternals @@ -19,6 +22,7 @@ import org.jetbrains.kotlin.text import org.scip_code.scip.Document import org.scip_code.scip.Occurrence import org.scip_code.scip.SymbolInformation +import org.scip_code.scip.SymbolInformation.Kind import org.scip_code.scip.SymbolRole import org.scip_code.scip.relationship import org.scip_code.scip.signature @@ -39,26 +43,26 @@ class ScipTextDocumentBuilder( fun build(): Document = documentBuilder.build("kotlin", relativePath(), fileText) + context(context: CheckerContext) fun emitScipData( firBasedSymbol: FirBasedSymbol<*>?, symbol: Symbol, element: KtSourceElement, isDefinition: Boolean, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { documentBuilder.addOccurrence(occurrence(symbol, element, isDefinition, enclosingSource)) if (isDefinition) { - documentBuilder.addSymbol(symbolInformation(firBasedSymbol, symbol, element, context)) + documentBuilder.addSymbol(symbolInformation(firBasedSymbol, symbol, element)) } } @OptIn(SymbolInternals::class) + context(context: CheckerContext) private fun symbolInformation( firBasedSymbol: FirBasedSymbol<*>?, symbol: Symbol, element: KtSourceElement, - context: CheckerContext, ): SymbolInformation { val supers = when (firBasedSymbol) { @@ -68,7 +72,7 @@ class ScipTextDocumentBuilder( .mapNotNull { it.toClassLikeSymbol(firBasedSymbol.moduleData.session) } .flatMap { cache[it] } is FirFunctionSymbol<*> -> - firBasedSymbol.directOverriddenSymbolsSafe(context).flatMap { cache[it] } + firBasedSymbol.directOverriddenSymbolsSafe().flatMap { cache[it] } else -> emptyList() } return symbolInformation { @@ -84,6 +88,8 @@ class ScipTextDocumentBuilder( } docComment(firBasedSymbol.fir)?.let { documentation += it } } + this.kind = scipKind(firBasedSymbol?.fir) + this.enclosingSymbol = context.containingDeclarations.lastOrNull()?.let {cache[it].last().toString() } ?: "" for (parent in supers) { relationships += relationship { this.symbol = parent.toString() @@ -146,7 +152,7 @@ class ScipTextDocumentBuilder( bodyRenderer = null, propertyAccessorRenderer = null, callArgumentsRenderer = FirCallNoArgumentsRenderer(), - modifierRenderer = FirAllModifierRenderer(), + modifierRenderer = FirAllModifierRenderer(FirModifierRenderer.StaticPolicy.Default), callableSignatureRenderer = FirCallableSignatureRendererForReadability(), declarationRenderer = FirDeclarationRenderer("local "), ) @@ -159,6 +165,23 @@ class ScipTextDocumentBuilder( return stripKdoc(kdoc).ifEmpty { null } } + private fun scipKind(element: FirElement?): Kind = + when (element) { + is FirClass if element.isInterface -> Kind.Interface + is FirTypeAlias -> Kind.TypeAlias + is FirClassLikeDeclaration -> Kind.Class + is FirConstructor -> Kind.Constructor + is FirTypeParameter -> Kind.TypeParameter + is FirValueParameter -> Kind.Parameter + is FirField -> Kind.Field + is FirProperty -> Kind.Property + is FirEnumEntry -> Kind.EnumMember + is FirVariable -> Kind.Variable + is FirCallableDeclaration -> Kind.Method + is FirPackageDirective -> Kind.Package + else -> Kind.UNRECOGNIZED + } + /** Strips the `/**`, leading `*`s, and `*/` from a kdoc block, returning just the body text. */ private fun stripKdoc(kdoc: String): String { if (kdoc.isEmpty()) return kdoc @@ -184,14 +207,15 @@ class ScipTextDocumentBuilder( } companion object { - @OptIn(SymbolInternals::class) + @OptIn(SymbolInternals::class, RenderingInternals::class) private fun displayName(firBasedSymbol: FirBasedSymbol<*>): String = when (firBasedSymbol) { - is FirClassSymbol -> firBasedSymbol.classId.shortClassName.asString() + is FirClassLikeSymbol -> firBasedSymbol.classId.shortClassName.asString() is FirPropertyAccessorSymbol -> firBasedSymbol.fir.propertySymbol.name.asString() is FirFunctionSymbol -> firBasedSymbol.callableId.callableName.asString() - is FirPropertySymbol -> firBasedSymbol.callableId.callableName.asString() + is FirPropertySymbol -> firBasedSymbol.callableIdForRendering.callableName.asString() is FirVariableSymbol -> firBasedSymbol.name.asString() + is FirTypeParameterSymbol -> firBasedSymbol.name.asString() else -> firBasedSymbol.toString() } } diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt index 80b9866e5..5525e607c 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/ScipVisitor.kt @@ -33,10 +33,10 @@ class ScipVisitor( fun build(): Document = documentBuilder.build() + context(context: CheckerContext) private fun Sequence?.emitAll( element: KtSourceElement, isDefinition: Boolean, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ): List? = this?.onEach { (firBasedSymbol, symbol) -> @@ -45,7 +45,6 @@ class ScipVisitor( symbol, element, isDefinition, - context, enclosingSource, ) } @@ -55,132 +54,144 @@ class ScipVisitor( private fun Sequence.with(firBasedSymbol: FirBasedSymbol<*>?) = this.map { SymbolDescriptorPair(firBasedSymbol, it) } - fun visitPackage(pkg: FqName, element: KtSourceElement, context: CheckerContext) { - cache[pkg].with(null).emitAll(element, isDefinition = false, context) + context(context: CheckerContext) + fun visitPackage(pkg: FqName, element: KtSourceElement) { + cache[pkg].with(null).emitAll(element, isDefinition = false) } + context(context: CheckerContext) fun visitClassReference( firClassSymbol: FirClassLikeSymbol<*>, element: KtSourceElement, - context: CheckerContext, ) { - cache[firClassSymbol].with(firClassSymbol).emitAll(element, isDefinition = false, context) + cache[firClassSymbol].with(firClassSymbol).emitAll(element, isDefinition = false) } + context(context: CheckerContext) fun visitCallableReference( firClassSymbol: FirCallableSymbol<*>, element: KtSourceElement, - context: CheckerContext, ) { - cache[firClassSymbol].with(firClassSymbol).emitAll(element, isDefinition = false, context) + cache[firClassSymbol].with(firClassSymbol).emitAll(element, isDefinition = false) } + context(context: CheckerContext) fun visitClassOrObject( firClass: FirClassLikeDeclaration, element: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firClass.symbol] .with(firClass.symbol) - .emitAll(element, isDefinition = true, context, enclosingSource) + .emitAll(element, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitPrimaryConstructor( firConstructor: FirConstructor, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firConstructor.symbol] .with(firConstructor.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitSecondaryConstructor( firConstructor: FirConstructor, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firConstructor.symbol] .with(firConstructor.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitNamedFunction( firFunction: FirFunction, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firFunction.symbol] .with(firFunction.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitProperty( firProperty: FirProperty, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firProperty.symbol] .with(firProperty.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitParameter( firParameter: FirValueParameter, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firParameter.symbol] .with(firParameter.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitTypeParameter( firTypeParameter: FirTypeParameter, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firTypeParameter.symbol] .with(firTypeParameter.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitTypeAlias( firTypeAlias: FirTypeAlias, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firTypeAlias.symbol] .with(firTypeAlias.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) fun visitPropertyAccessor( firPropertyAccessor: FirPropertyAccessor, source: KtSourceElement, - context: CheckerContext, enclosingSource: KtSourceElement? = null, ) { cache[firPropertyAccessor.symbol] .with(firPropertyAccessor.symbol) - .emitAll(source, isDefinition = true, context, enclosingSource) + .emitAll(source, isDefinition = true, enclosingSource) } + context(context: CheckerContext) + fun visitEnumEntry( + firEnumEntry: FirEnumEntry, + source: KtSourceElement, + enclosingSource: KtSourceElement? = null, + ) { + cache[firEnumEntry.symbol] + .with(firEnumEntry.symbol) + .emitAll(source, isDefinition = true, enclosingSource) + } + + context(context: CheckerContext) fun visitSimpleNameExpression( firResolvedNamedReference: FirResolvedNamedReference, source: KtSourceElement, - context: CheckerContext, ) { cache[firResolvedNamedReference.resolvedSymbol] .with(firResolvedNamedReference.resolvedSymbol) - .emitAll(source, isDefinition = false, context) + .emitAll(source, isDefinition = false) } } diff --git a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt index 57f5b1b69..c6162af21 100644 --- a/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt +++ b/scip-kotlinc/src/main/kotlin/org/scip_code/scip_java/kotlinc/SymbolsCache.kt @@ -1,20 +1,21 @@ 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.analysis.checkers.declaration.isLocalDeclaredInBlock import org.jetbrains.kotlin.fir.declarations.DirectDeclarationsAccess import org.jetbrains.kotlin.fir.declarations.FirClass import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin import org.jetbrains.kotlin.fir.declarations.utils.memberDeclarationNameOrNull +import org.jetbrains.kotlin.fir.types.classId +import org.jetbrains.kotlin.fir.types.coneType import org.jetbrains.kotlin.fir.packageFqName import org.jetbrains.kotlin.fir.resolve.getContainingDeclaration +import org.jetbrains.kotlin.fir.resolve.getContainingSymbol 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 @@ -80,7 +81,7 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { private fun uncachedSymbol(symbol: FirBasedSymbol<*>?, locals: LocalSymbolsCache): Symbol { if (symbol == null || symbol is FirAnonymousFunctionSymbol) return Symbol.NONE - if (symbol.fir.isLocalMember) return locals + symbol + if (symbol.fir.isLocalDeclaredInBlock) return locals + symbol val owner = getParentSymbol(symbol, locals) @@ -115,10 +116,32 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { return getSymbol(symbol.containingDeclarationSymbol, locals) is FirValueParameterSymbol -> return getSymbol(symbol.containingDeclarationSymbol, locals) + is FirPropertyAccessorSymbol -> return getSymbol(symbol.propertySymbol, locals) is FirCallableSymbol -> { val session = symbol.fir.moduleData.session - return symbol.getContainingSymbol(session)?.let { getSymbol(it, locals) } - ?: getSymbol(symbol.packageFqName()) + val containingSymbol = symbol.getContainingSymbol(session) + // For top-level extension functions/properties (containingSymbol is file or null), + // use the receiver type as a synthetic parent within the package + // (e.g. sample/String#foo().). + if (containingSymbol == null || containingSymbol is FirFileSymbol) { + val receiverClassId = + symbol.fir + .receiverParameter + ?.typeRef + ?.coneType + ?.classId + if (receiverClassId != null) { + val packageSymbol = getSymbol(symbol.packageFqName()) + return Symbol.createGlobal( + packageSymbol, + ScipSymbolDescriptor( + Kind.TYPE, receiverClassId.shortClassName.asString() + ) + ) + } + } + containingSymbol?.let { return getSymbol(it, locals) } + return getSymbol(symbol.packageFqName()) } is FirClassLikeSymbol -> { val session = symbol.fir.moduleData.session @@ -142,15 +165,9 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { symbol is FirClassLikeSymbol -> ScipSymbolDescriptor(Kind.TYPE, symbol.classId.shortClassName.asString()) symbol is FirPropertyAccessorSymbol && symbol.isSetter -> - ScipSymbolDescriptor( - Kind.METHOD, - "set" + symbol.propertySymbol.fir.name.toString().capitalizeAsciiOnly(), - ) + ScipSymbolDescriptor(Kind.METHOD, "set") symbol is FirPropertyAccessorSymbol && symbol.isGetter -> - ScipSymbolDescriptor( - Kind.METHOD, - "get" + symbol.propertySymbol.fir.name.toString().capitalizeAsciiOnly(), - ) + ScipSymbolDescriptor(Kind.METHOD, "get") symbol is FirConstructorSymbol -> ScipSymbolDescriptor(Kind.METHOD, "", methodDisambiguator(symbol)) symbol is FirFunctionSymbol -> @@ -164,6 +181,7 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { symbol is FirValueParameterSymbol -> ScipSymbolDescriptor(Kind.PARAMETER, symbol.name.toString()) symbol is FirVariableSymbol -> ScipSymbolDescriptor(Kind.TERM, symbol.name.toString()) + symbol is FirFileSymbol -> ScipSymbolDescriptor.NONE else -> { err.println("unknown symbol kind ${symbol.javaClass.simpleName}") ScipSymbolDescriptor.NONE @@ -178,13 +196,49 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { 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, - ) + containingSymbol.fir.declarations.map { it.symbol } + is FirFileSymbol, null -> { + // For top-level extension functions, siblings are the receiver class members + // (if in the same package) followed by other extension functions on the same + // receiver type in this package. This ensures consistent disambiguation + // when both a class member and an extension share the same parent namespace + // (e.g. sample/MyClass#foo(). vs sample/MyClass#foo(+1).). + val receiverClassId = + symbol.fir + .receiverParameter + ?.typeRef + ?.coneType + ?.classId + if (receiverClassId != null) { + val receiverClass = + session.symbolProvider.getClassLikeSymbolByClassId(receiverClassId) + as? FirClassSymbol<*> + val classMembers = + if (receiverClass?.packageFqName() == symbol.packageFqName()) { + receiverClass.fir.declarations.map { it.symbol } + } else { + emptyList() + } + val extensionFns = + session.symbolProvider + .getTopLevelCallableSymbols(symbol.packageFqName(), symbol.name) + .filter { + it is FirFunctionSymbol<*> && + it.fir + .receiverParameter + ?.typeRef + ?.coneType + ?.classId == receiverClassId + } + classMembers + extensionFns + } else if (containingSymbol is FirFileSymbol) { + containingSymbol.fir.declarations.map { it.symbol } + } else { + session.symbolProvider.getTopLevelCallableSymbols( + symbol.packageFqName(), symbol.name + ) + } + } else -> return "()" } @@ -201,7 +255,11 @@ class GlobalSymbolsCache(testing: Boolean = false) : Iterable { } } - if (count == 0 || !found) return "()" + if (!found) { + err.println("methodDisambiguator: ${symbol.callableId} not found in sibling list") + return "()" + } + if (count == 0) return "()" return "(+${count})" } diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt index 36e94ad56..09dd17009 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/AnalyzerTest.kt @@ -7,6 +7,7 @@ import io.kotest.assertions.fail import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain import java.io.File import java.nio.file.Path import kotlin.test.Test @@ -15,6 +16,8 @@ import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.junit.jupiter.api.io.TempDir import org.scip_code.scip.Document +import org.scip_code.scip.SymbolInformation.Kind +import org.scip_code.scip.symbolInformation import org.scip_code.scip_java.kotlinc.* @OptIn(ExperimentalCompilerApi::class) @@ -107,11 +110,15 @@ class AnalyzerTest { arrayOf( scipSymbol { symbol = "sample/Banana#" + kind = Kind.Class + enclosingSymbol = "sample/" displayName = "Banana" signatureText = "public final class Banana : Any" }, scipSymbol { symbol = "sample/Banana#foo()." + kind = Kind.Method + enclosingSymbol = "sample/Banana#" displayName = "foo" signatureText = "public final fun foo(): Unit" }, @@ -279,21 +286,29 @@ class AnalyzerTest { arrayOf( scipSymbol { symbol = "sample/foo()." + kind = Kind.Method + enclosingSymbol = "sample/" displayName = "foo" signatureText = "public final fun foo(): Unit" }, scipSymbol { symbol = "local 0" + kind = Kind.Class + enclosingSymbol = "sample/foo()." displayName = "LocalClass" signatureText = "local final class LocalClass : Any" }, scipSymbol { symbol = "local 1" + kind = Kind.Constructor + enclosingSymbol = "local 0" displayName = "LocalClass" signatureText = "public constructor(): LocalClass" }, scipSymbol { symbol = "local 2" + kind = Kind.Method + enclosingSymbol = "local 0" displayName = "localClassMethod" signatureText = "public final fun localClassMethod(): Unit" }, @@ -301,6 +316,371 @@ class AnalyzerTest { document.symbolsList.shouldContainAll(*symbols) } + @Test + fun `lambda parameters`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + fun use() { + val f = { n: Int -> n * 2 } + } + """ + ) + + val occurrences = + arrayOf( + // val f is a local variable — gets local0 via isLocalMember + scipOccurrence { + role = DEFINITION + symbol = "local 0" + range { startLine = 3; startCharacter = 8; endLine = 3; endCharacter = 9 } + enclosingRange { + startLine = 3 + startCharacter = 4 + endLine = 3 + endCharacter = 31 + } + }, + // n is a lambda parameter — gets local1 via the owner == Symbol.NONE path + // (the containing FirAnonymousFunction is skipped, yielding Symbol.NONE as owner) + scipOccurrence { + role = DEFINITION + symbol = "local 1" + range { startLine = 3; startCharacter = 14; endLine = 3; endCharacter = 15 } + enclosingRange { + startLine = 3 + startCharacter = 14 + endLine = 3 + endCharacter = 20 + } + }, + // explicit type annotation on n emits a class reference + scipOccurrence { + role = REFERENCE + symbol = "kotlin/Int#" + range { startLine = 3; startCharacter = 17; endLine = 3; endCharacter = 20 } + }, + // reference to n in the lambda body uses the same local symbol + scipOccurrence { + role = REFERENCE + symbol = "local 1" + range { startLine = 3; startCharacter = 24; endLine = 3; endCharacter = 25 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/use()." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "use" + signatureText = "public final fun use(): Unit" + }, + scipSymbol { + symbol = "local 0" + kind = Kind.Property + enclosingSymbol = "sample/use()." + displayName = "f" + signatureText = "local val f: (Int) -> Int" + }, + scipSymbol { + symbol = "local 1" + kind = Kind.Parameter + displayName = "n" + signatureText = "n: Int" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `local functions`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + fun outer() { + fun inner() {} + fun innerWithReturnType(): Int = 42 + inner() + } + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/outer()." + range { startLine = 2; startCharacter = 4; endLine = 2; endCharacter = 9 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 6 + endCharacter = 1 + } + }, + // inner() — local named function gets a local symbol + scipOccurrence { + role = DEFINITION + symbol = "local 0" + range { startLine = 3; startCharacter = 8; endLine = 3; endCharacter = 13 } + enclosingRange { + startLine = 3 + startCharacter = 4 + endLine = 3 + endCharacter = 18 + } + }, + // innerWithReturnType() — local named function with explicit return type + scipOccurrence { + role = DEFINITION + symbol = "local 1" + range { startLine = 4; startCharacter = 8; endLine = 4; endCharacter = 27 } + enclosingRange { + startLine = 4 + startCharacter = 4 + endLine = 4 + endCharacter = 39 + } + }, + // Int return-type reference + scipOccurrence { + role = REFERENCE + symbol = "kotlin/Int#" + range { startLine = 4; startCharacter = 31; endLine = 4; endCharacter = 34 } + }, + // call site inner() references the same local symbol + scipOccurrence { + role = REFERENCE + symbol = "local 0" + range { startLine = 5; startCharacter = 4; endLine = 5; endCharacter = 9 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "local 0" + kind = Kind.Method + enclosingSymbol = "sample/outer()." + displayName = "inner" + signatureText = "local final fun inner(): Unit" + }, + scipSymbol { + symbol = "local 1" + kind = Kind.Method + enclosingSymbol = "sample/outer()." + displayName = "innerWithReturnType" + signatureText = "local final fun innerWithReturnType(): Int" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `user-defined class as return type`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + class MyClass + + fun bar(): MyClass = MyClass() + """ + ) + + val occurrences = + arrayOf ( + scipOccurrence { + role = DEFINITION + symbol = "sample/bar()." + range { startLine = 4; startCharacter = 4; endLine = 4; endCharacter = 7 } + enclosingRange { + startLine = 4 + startCharacter = 0 + endLine = 4 + endCharacter = 30 + } + }, + // MyClass in the return type position generates a class reference + scipOccurrence { + role = REFERENCE + symbol = "sample/MyClass#" + range { startLine = 4; startCharacter = 11; endLine = 4; endCharacter = 18 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/bar()." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "bar" + signatureText = "public final fun bar(): MyClass" + }) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `typealias`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + typealias MyAlias = Int + val x: MyAlias = 42 + """ + ) + + val occurrences = + arrayOf ( + scipOccurrence { + role = DEFINITION + symbol = "sample/MyAlias#" + range { startLine = 2; startCharacter = 10; endLine = 2; endCharacter = 17 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 23 + } + }, + // Note: val x: MyAlias does not emit a REFERENCE for sample/MyAlias# because the + // property checker resolves the type alias to its expansion (Int). + scipOccurrence { + role = DEFINITION + symbol = "sample/x." + range { startLine = 3; startCharacter = 4; endLine = 3; endCharacter = 5 } + enclosingRange { + startLine = 3 + startCharacter = 0 + endLine = 3 + endCharacter = 19 + } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/MyAlias#" + kind = Kind.TypeAlias + enclosingSymbol = "sample/" + displayName = "MyAlias" + signatureText = "public final typealias MyAlias = Int\n" + }) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `type parameters`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + fun identity(x: T): T = x + """ + ) + + val occurrences = + arrayOf ( + scipOccurrence { + role = DEFINITION + symbol = "sample/identity()." + range { startLine = 2; startCharacter = 8; endLine = 2; endCharacter = 16 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 29 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/identity().[T]" + range { startLine = 2; startCharacter = 5; endLine = 2; endCharacter = 6 } + enclosingRange { + startLine = 2 + startCharacter = 5 + endLine = 2 + endCharacter = 6 + } + }, + // Note: T in type-annotation positions (x: T and return type : T) does not produce + // REFERENCE occurrences. The checkers use toClassLikeSymbol() to detect type + // references, which returns null for type parameters, so those usages are not + // currently tracked. + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/identity().[T]" + kind = Kind.TypeParameter + enclosingSymbol = "sample/identity()." + displayName = "T" + signatureText = "T" + }) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `generic class declaration`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + class Box(val value: T) { + fun unwrap(): T = value + } + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Box#" + range { startLine = 2; startCharacter = 6; endLine = 2; endCharacter = 9 } + enclosingRange { startLine = 2; endLine = 4; endCharacter = 1 } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Box#[T]" + range { startLine = 2; startCharacter = 10; endLine = 2; endCharacter = 11 } + enclosingRange { startLine = 2; startCharacter = 10; endLine = 2; endCharacter = 11 } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Box#unwrap()." + range { startLine = 3; startCharacter = 8; endLine = 3; endCharacter = 14 } + enclosingRange { startLine = 3; startCharacter = 4; endLine = 3; endCharacter = 27 } + } + ) + document.occurrencesList.shouldContainAll(*occurrences) + } + @Test fun overrides(@TempDir path: Path) { val document = @@ -410,22 +790,30 @@ class AnalyzerTest { arrayOf( scipSymbol { symbol = "sample/Interface#" + kind = Kind.Interface + enclosingSymbol = "sample/" displayName = "Interface" signatureText = "public abstract interface Interface : Any" }, scipSymbol { symbol = "sample/Interface#foo()." + kind = Kind.Method + enclosingSymbol = "sample/Interface#" displayName = "foo" signatureText = "public abstract fun foo(): Unit\n" }, scipSymbol { symbol = "sample/Class#" + kind = Kind.Class + enclosingSymbol = "sample/" displayName = "Class" signatureText = "public final class Class : Interface" addOverriddenSymbols("sample/Interface#") }, scipSymbol { symbol = "sample/Class#foo()." + kind = Kind.Method + enclosingSymbol = "sample/Class#" displayName = "foo" signatureText = "public open override fun foo(): Unit" addOverriddenSymbols("sample/Interface#foo().") @@ -502,7 +890,7 @@ class AnalyzerTest { }, scipOccurrence { role = DEFINITION - symbol = "sample/``#" + symbol = "local 1" range { startLine = 7 startCharacter = 12 @@ -518,7 +906,7 @@ class AnalyzerTest { }, scipOccurrence { role = DEFINITION - symbol = "sample/``#``()." + symbol = "local 2" range { startLine = 7 startCharacter = 12 @@ -544,7 +932,7 @@ class AnalyzerTest { }, scipOccurrence { role = DEFINITION - symbol = "sample/``#foo()." + symbol = "local 3" range { startLine = 8 startCharacter = 21 @@ -560,7 +948,7 @@ class AnalyzerTest { }, scipOccurrence { role = DEFINITION - symbol = "sample/``#" + symbol = "local 5" range { startLine = 10 startCharacter = 12 @@ -576,7 +964,7 @@ class AnalyzerTest { }, scipOccurrence { role = DEFINITION - symbol = "sample/``#``()." + symbol = "local 6" range { startLine = 10 startCharacter = 12 @@ -602,7 +990,7 @@ class AnalyzerTest { }, scipOccurrence { role = DEFINITION - symbol = "sample/``#foo()." + symbol = "local 7" range { startLine = 11 startCharacter = 21 @@ -623,29 +1011,39 @@ class AnalyzerTest { arrayOf( scipSymbol { symbol = "sample/Interface#" + kind = Kind.Interface + enclosingSymbol = "sample/" displayName = "Interface" signatureText = "public abstract interface Interface : Any" }, scipSymbol { - symbol = "sample/``#" + symbol = "local 1" + kind = Kind.Class + enclosingSymbol = "local 0" displayName = "" signatureText = "object : Interface" addOverriddenSymbols("sample/Interface#") }, scipSymbol { - symbol = "sample/``#foo()." + symbol = "local 3" + kind = Kind.Method + enclosingSymbol = "local 1" displayName = "foo" signatureText = "public open override fun foo(): Unit" addOverriddenSymbols("sample/Interface#foo().") }, scipSymbol { - symbol = "sample/``#" + symbol = "local 5" + kind = Kind.Class + enclosingSymbol = "local 4" displayName = "" signatureText = "object : Interface" addOverriddenSymbols("sample/Interface#") }, scipSymbol { - symbol = "sample/``#foo()." + symbol = "local 7" + kind = Kind.Method + enclosingSymbol = "local 5" displayName = "foo" signatureText = "public open override fun foo(): Unit" addOverriddenSymbols("sample/Interface#foo().") @@ -737,6 +1135,13 @@ class AnalyzerTest { else -> x as Float } } + + class Wrapper + fun classify(x: Any) { + if (x is Wrapper) {} + val s = x as? String + val w = x as Wrapper + } """, ) @@ -762,8 +1167,38 @@ class AnalyzerTest { endCharacter = 26 } }, - ) - document.occurrencesList.shouldContainAll(*occurrences) + scipOccurrence { + role = REFERENCE + symbol = "sample/Wrapper#" + range { + startLine = 11 + startCharacter = 13 + endLine = 11 + endCharacter = 20 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "kotlin/String#" + range { + startLine = 12 + startCharacter = 18 + endLine = 12 + endCharacter = 24 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Wrapper#" + range { + startLine = 13 + startCharacter = 17 + endLine = 13 + endCharacter = 24 + } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) } @Test @@ -776,6 +1211,7 @@ class AnalyzerTest { compilerPluginRegistrars = listOf(AnalyzerRegistrar { throw Exception("sample text") }) verbose = false + messageOutputStream = java.io.OutputStream.nullOutputStream() pluginOptions = listOf( PluginOption("scip-kotlinc", "sourceroot", path.toString()), @@ -787,6 +1223,8 @@ class AnalyzerTest { .compile() result.exitCode shouldBe KotlinCompilation.ExitCode.OK + result.messages shouldContain "Exception in scip-kotlin compiler plugin:" + result.messages shouldContain "sample text" } @Test @@ -1300,6 +1738,8 @@ class AnalyzerTest { arrayOf( scipSymbol { symbol = "hello/sample/Apple#" + kind = Kind.Class + enclosingSymbol = "hello/sample/" displayName = "Apple" signatureText = "public final class Apple : Any" } @@ -1385,11 +1825,15 @@ class AnalyzerTest { arrayOf( scipSymbol { symbol = "sample/Banana#" + kind = Kind.Class + enclosingSymbol = "sample/" displayName = "Banana" signatureText = "public final class Banana : Any" }, scipSymbol { symbol = "sample/Banana#foo()." + kind = Kind.Method + enclosingSymbol = "sample/Banana#" displayName = "foo" signatureText = "public final fun foo(): Unit" }, @@ -1414,7 +1858,7 @@ class AnalyzerTest { * Example method docstring * **/ - inline fun docstrings(msg: String): Int { return msg.length } + fun docstrings(msg: String): Int { return msg.length } """ .trimIndent(), ) @@ -1422,10 +1866,828 @@ class AnalyzerTest { document.assertDocumentation("sample/docstrings().", "Example method docstring") } + @Test + fun `extension receiver type reference`(@TempDir path: Path) { + // String is from the kotlin package; our extension in sample gets + // symbol sample/String#foo(). — distinct from kotlin/String#foo(). + // This means extensions on cross-package types never collide with + // the receiver class's own methods in the symbol table. + val document = + compileScip( + path, + """ + package sample + + fun String.foo(): Int = 42 + fun use(s: String) = s.foo() + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/String#foo()." + range { startLine = 2; startCharacter = 11; endLine = 2; endCharacter = 14 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 26 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "kotlin/String#" + range { startLine = 2; startCharacter = 4; endLine = 2; endCharacter = 10 } + }, + scipOccurrence { + role = REFERENCE + symbol = "kotlin/Int#" + range { startLine = 2; startCharacter = 18; endLine = 2; endCharacter = 21 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/String#foo()." + range { startLine = 3; startCharacter = 23; endLine = 3; endCharacter = 26 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/String#foo()." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "foo" + signatureText = "public final fun String.foo(): Int" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `extension property receiver type reference`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + val Int.asString: String get() = this.toString() + fun use() = 42.asString + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Int#asString." + range { startLine = 2; startCharacter = 8; endLine = 2; endCharacter = 16 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 48 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "kotlin/Int#" + range { startLine = 2; startCharacter = 4; endLine = 2; endCharacter = 7 } + }, + scipOccurrence { + role = REFERENCE + symbol = "kotlin/String#" + range { startLine = 2; startCharacter = 18; endLine = 2; endCharacter = 24 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Int#asString." + range { startLine = 3; startCharacter = 15; endLine = 3; endCharacter = 23 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/Int#asString." + kind = Kind.Property + enclosingSymbol = "sample/" + displayName = "asString" + signatureText = "public final val Int.asString: String" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `extension overload disambiguator`(@TempDir path: Path) { + // When a class already has a member named foo() and an extension also + // adds foo(), the extension is counted after the member in the combined + // sibling list (class members + same-package extensions on the same + // receiver type), so the extension gets the (+1) disambiguator and the + // two produce distinct symbols. + val document = + compileScip( + path, + """ + package sample + + class MyClass { + fun foo() {} + } + fun MyClass.foo(x: Int) {} + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/MyClass#foo()." + range { startLine = 3; startCharacter = 8; endLine = 3; endCharacter = 11 } + enclosingRange { + startLine = 3 + startCharacter = 4 + endLine = 3 + endCharacter = 16 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/MyClass#foo(+1)." + range { startLine = 5; startCharacter = 12; endLine = 5; endCharacter = 15 } + enclosingRange { + startLine = 5 + startCharacter = 0 + endLine = 5 + endCharacter = 26 + } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + } + + @Test + fun `enum entry definitions`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + enum class Color { RED, GREEN, BLUE } + + fun useEnum(): Color = Color.RED + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Color#" + range { startLine = 2; startCharacter = 11; endLine = 2; endCharacter = 16 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 37 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Color#RED." + range { startLine = 2; startCharacter = 19; endLine = 2; endCharacter = 22 } + enclosingRange { + startLine = 2 + startCharacter = 19 + endLine = 2 + endCharacter = 23 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Color#GREEN." + range { startLine = 2; startCharacter = 24; endLine = 2; endCharacter = 29 } + enclosingRange { + startLine = 2 + startCharacter = 24 + endLine = 2 + endCharacter = 30 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Color#BLUE." + range { startLine = 2; startCharacter = 31; endLine = 2; endCharacter = 35 } + enclosingRange { + startLine = 2 + startCharacter = 31 + endLine = 2 + endCharacter = 35 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Color#" + range { startLine = 4; startCharacter = 23; endLine = 4; endCharacter = 28 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Color#RED." + range { startLine = 4; startCharacter = 29; endLine = 4; endCharacter = 32 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/Color#" + kind = Kind.Class + enclosingSymbol = "sample/" + displayName = "Color" + addOverriddenSymbols("kotlin/Enum#") + signatureText = "public final enum class Color : Enum" + }, + scipSymbol { + symbol = "sample/Color#RED." + kind = Kind.EnumMember + enclosingSymbol = "sample/Color#" + displayName = "RED" + signatureText = "public final static enum entry RED: Color" + }, + scipSymbol { + symbol = "sample/Color#GREEN." + kind = Kind.EnumMember + enclosingSymbol = "sample/Color#" + displayName = "GREEN" + signatureText = "public final static enum entry GREEN: Color" + }, + scipSymbol { + symbol = "sample/Color#BLUE." + kind = Kind.EnumMember + enclosingSymbol = "sample/Color#" + displayName = "BLUE" + signatureText = "public final static enum entry BLUE: Color" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `enum entry with body`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + enum class Op { + PLUS { + override fun apply(a: Int, b: Int) = a + b + }; + + abstract fun apply(a: Int, b: Int): Int + } + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Op#PLUS." + range { startLine = 3; startCharacter = 4; endLine = 3; endCharacter = 8 } + // Body-having enum entry: enclosing_range covers PLUS through the + // trailing `};` that terminates the entry list. + enclosingRange { startLine = 3; startCharacter = 4; endLine = 5; endCharacter = 6 } + }, + // An enum entry with a body is modeled as a synthetic anonymous subclass, + // so its overridden member is a local symbol (local2), not a global + // sample/Op#PLUS.apply(). It still gets an enclosing_range spanning the + // function declaration. + scipOccurrence { + role = DEFINITION + symbol = "local 2" + range { startLine = 4; startCharacter = 21; endLine = 4; endCharacter = 26 } + enclosingRange { startLine = 4; startCharacter = 8; endLine = 4; endCharacter = 50 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + // The entry body becomes an anonymous class enclosed by the entry... + scipSymbol { + symbol = "local 0" + kind = Kind.Class + enclosingSymbol = "sample/Op#PLUS." + displayName = "" + addOverriddenSymbols("sample/Op#") + signatureText = "object : Op" + }, + // ...and the override is a method of that anonymous class. + scipSymbol { + symbol = "local 2" + kind = Kind.Method + enclosingSymbol = "local 0" + displayName = "apply" + addOverriddenSymbols("sample/Op#apply().") + signatureText = "public open override fun apply(a: Int, b: Int): Int" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `named object declarations`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + object MySingleton { + fun hello(): String = "hi" + } + fun use() = MySingleton.hello() + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/MySingleton#" + range { startLine = 2; startCharacter = 7; endLine = 2; endCharacter = 18 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 4 + endCharacter = 1 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/MySingleton#hello()." + range { startLine = 3; startCharacter = 8; endLine = 3; endCharacter = 13 } + enclosingRange { + startLine = 3 + startCharacter = 4 + endLine = 3 + endCharacter = 30 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/MySingleton#" + range { startLine = 5; startCharacter = 12; endLine = 5; endCharacter = 23 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/MySingleton#hello()." + range { startLine = 5; startCharacter = 24; endLine = 5; endCharacter = 29 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/MySingleton#" + kind = Kind.Class + enclosingSymbol = "sample/" + displayName = "MySingleton" + signatureText = "public final object MySingleton : Any" + }, + scipSymbol { + symbol = "sample/MySingleton#hello()." + kind = Kind.Method + enclosingSymbol = "sample/MySingleton#" + displayName = "hello" + signatureText = "public final fun hello(): String" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `companion object`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + class Foo { + companion object Factory { + fun create(): Foo = Foo() + } + } + fun use() = Foo.Factory.create() + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Foo#" + range { startLine = 2; startCharacter = 6; endLine = 2; endCharacter = 9 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 6 + endCharacter = 1 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Foo#Factory#" + range { startLine = 3; startCharacter = 21; endLine = 3; endCharacter = 28 } + enclosingRange { + startLine = 3 + startCharacter = 4 + endLine = 5 + endCharacter = 5 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Foo#Factory#create()." + range { startLine = 4; startCharacter = 12; endLine = 4; endCharacter = 18 } + enclosingRange { + startLine = 4 + startCharacter = 8 + endLine = 4 + endCharacter = 33 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Foo#" + range { startLine = 7; startCharacter = 12; endLine = 7; endCharacter = 15 } + }, + // Foo.Factory is a FirResolvedQualifier spanning the full qualifier expression + scipOccurrence { + role = REFERENCE + symbol = "sample/Foo#Factory#" + range { startLine = 7; startCharacter = 12; endLine = 7; endCharacter = 23 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Foo#Factory#create()." + range { startLine = 7; startCharacter = 24; endLine = 7; endCharacter = 30 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/Foo#" + kind = Kind.Class + enclosingSymbol = "sample/" + displayName = "Foo" + signatureText = "public final class Foo : Any" + }, + scipSymbol { + symbol = "sample/Foo#Factory#" + kind = Kind.Class + enclosingSymbol = "sample/Foo#" + displayName = "Factory" + signatureText = "public final companion object Factory : Any" + }, + scipSymbol { + symbol = "sample/Foo#Factory#create()." + kind = Kind.Method + enclosingSymbol = "sample/Foo#Factory#" + displayName = "create" + signatureText = "public final fun create(): Foo" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `unnamed companion object`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + class Bar { + companion object { + fun instance(): Bar = Bar() + } + } + fun use() = Bar.instance() + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Bar#" + range { startLine = 2; startCharacter = 6; endLine = 2; endCharacter = 9 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 6 + endCharacter = 1 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Bar#Companion#instance()." + range { startLine = 4; startCharacter = 12; endLine = 4; endCharacter = 20 } + enclosingRange { + startLine = 4 + startCharacter = 8 + endLine = 4 + endCharacter = 35 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Bar#" + range { startLine = 7; startCharacter = 12; endLine = 7; endCharacter = 15 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Bar#Companion#instance()." + range { startLine = 7; startCharacter = 16; endLine = 7; endCharacter = 24 } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/Bar#Companion#" + range { startLine = 3; startCharacter = 4; endLine = 3; endCharacter = 13 } + enclosingRange { + startLine = 3 + startCharacter = 4 + endLine = 5 + endCharacter = 5 + } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/Bar#" + kind = Kind.Class + enclosingSymbol = "sample/" + displayName = "Bar" + signatureText = "public final class Bar : Any" + }, + scipSymbol { + symbol = "sample/Bar#Companion#" + kind = Kind.Class + enclosingSymbol = "sample/Bar#" + displayName = "Companion" + signatureText = "public final companion object Companion : Any" + }, + scipSymbol { + symbol = "sample/Bar#Companion#instance()." + kind = Kind.Method + enclosingSymbol = "sample/Bar#Companion#" + displayName = "instance" + signatureText = "public final fun instance(): Bar" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `string template references`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + fun greet(name: String) = "Hello, ${'$'}name!" + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/greet()." + range { startLine = 2; startCharacter = 4; endLine = 2; endCharacter = 9 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 41 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/greet().(name)" + range { startLine = 2; startCharacter = 10; endLine = 2; endCharacter = 14 } + enclosingRange { + startLine = 2 + startCharacter = 10 + endLine = 2 + endCharacter = 22 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/greet().(name)" + range { startLine = 2; startCharacter = 35; endLine = 2; endCharacter = 39 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/greet()." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "greet" + signatureText = "public final fun greet(name: String): String" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `multiple supertype references`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + interface Named + interface Speakable + class Person : Named, Speakable + fun use(p: Person): Named = p + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/Person#" + range { startLine = 4; startCharacter = 6; endLine = 4; endCharacter = 12 } + enclosingRange { + startLine = 4 + startCharacter = 0 + endLine = 4 + endCharacter = 31 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Named#" + range { startLine = 4; startCharacter = 15; endLine = 4; endCharacter = 20 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Speakable#" + range { startLine = 4; startCharacter = 22; endLine = 4; endCharacter = 31 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/Named#" + range { startLine = 5; startCharacter = 20; endLine = 5; endCharacter = 25 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/Person#" + kind = Kind.Class + enclosingSymbol = "sample/" + displayName = "Person" + addOverriddenSymbols("sample/Named#") + addOverriddenSymbols("sample/Speakable#") + signatureText = "public final class Person : Named, Speakable" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + + @Test + fun `three-way overload disambiguator`(@TempDir path: Path) { + val document = + compileScip( + path, + """ + package sample + + fun add(x: Int, y: Int): Int = x + y + fun add(x: Double, y: Double): Double = x + y + fun add(x: String, y: String): String = x + y + fun use() { + add(1, 2) + add(1.0, 2.0) + add("a", "b") + } + """ + ) + + val occurrences = + arrayOf( + scipOccurrence { + role = DEFINITION + symbol = "sample/add()." + range { startLine = 2; startCharacter = 4; endLine = 2; endCharacter = 7 } + enclosingRange { + startLine = 2 + startCharacter = 0 + endLine = 2 + endCharacter = 36 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/add(+1)." + range { startLine = 3; startCharacter = 4; endLine = 3; endCharacter = 7 } + enclosingRange { + startLine = 3 + startCharacter = 0 + endLine = 3 + endCharacter = 45 + } + }, + scipOccurrence { + role = DEFINITION + symbol = "sample/add(+2)." + range { startLine = 4; startCharacter = 4; endLine = 4; endCharacter = 7 } + enclosingRange { + startLine = 4 + startCharacter = 0 + endLine = 4 + endCharacter = 45 + } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/add()." + range { startLine = 6; startCharacter = 4; endLine = 6; endCharacter = 7 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/add(+1)." + range { startLine = 7; startCharacter = 4; endLine = 7; endCharacter = 7 } + }, + scipOccurrence { + role = REFERENCE + symbol = "sample/add(+2)." + range { startLine = 8; startCharacter = 4; endLine = 8; endCharacter = 7 } + }, + ) + document.occurrencesList.shouldContainAll(*occurrences) + + val symbols = + arrayOf( + scipSymbol { + symbol = "sample/add()." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "add" + signatureText = "public final fun add(x: Int, y: Int): Int" + }, + scipSymbol { + symbol = "sample/add(+1)." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "add" + signatureText = "public final fun add(x: Double, y: Double): Double" + }, + scipSymbol { + symbol = "sample/add(+2)." + kind = Kind.Method + enclosingSymbol = "sample/" + displayName = "add" + signatureText = "public final fun add(x: String, y: String): String" + }, + ) + document.symbolsList.shouldContainAll(*symbols) + } + private fun Document.assertDocumentation(symbol: String, expectedDocumentation: String) { val info = this.symbolsList.find { it.symbol == symbol } - ?: fail("no SymbolInformation for symbol $symbol") + ?: fail("no scipSymbol 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 index 1e8ee95a7..fe8bdebc7 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipBuilders.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipBuilders.kt @@ -76,6 +76,8 @@ class ScipOccurrenceBuilder { @ScipBuilderDsl class ScipSymbolInformationBuilder { var symbol: String = "" + var kind: SymbolInformation.Kind = SymbolInformation.Kind.UnspecifiedKind + var enclosingSymbol: String = "" var displayName: String = "" var signatureText: String? = null private val docs = mutableListOf() @@ -95,6 +97,8 @@ class ScipSymbolInformationBuilder { internal fun build(): SymbolInformation = symbolInformation { symbol = this@ScipSymbolInformationBuilder.symbol + kind = this@ScipSymbolInformationBuilder.kind + enclosingSymbol = this@ScipSymbolInformationBuilder.enclosingSymbol if (this@ScipSymbolInformationBuilder.displayName.isNotEmpty()) { displayName = this@ScipSymbolInformationBuilder.displayName } diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt index 048d33cee..0bc8349bf 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/ScipSymbolsTest.kt @@ -3,6 +3,7 @@ package org.scip_code.scip_java.kotlinc.test import com.tschuchort.compiletesting.SourceFile import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.junit.jupiter.api.TestFactory +import org.scip_code.scip.SymbolInformation.Kind import org.scip_code.scip_java.kotlinc.* import org.scip_code.scip_java.kotlinc.test.ExpectedSymbols.ScipData import org.scip_code.scip_java.kotlinc.test.ExpectedSymbols.SymbolCacheData @@ -275,7 +276,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "getX()." + symbol = "x.get()." range { startLine = 0 startCharacter = 4 @@ -286,7 +287,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "setX()." + symbol = "x.set()." range { startLine = 0 startCharacter = 4 @@ -327,7 +328,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "setX()." + symbol = "x.set()." range { startLine = 0 startCharacter = 4 @@ -341,7 +342,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "getX()." + symbol = "x.get()." range { startLine = 1 startCharacter = 4 @@ -387,7 +388,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "getX()." + symbol = "x.get()." range { startLine = 0 startCharacter = 4 @@ -401,7 +402,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "setX()." + symbol = "x.set()." range { startLine = 1 startCharacter = 4 @@ -448,7 +449,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "getX()." + symbol = "x.get()." range { startLine = 1 startCharacter = 4 @@ -464,7 +465,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "setX()." + symbol = "x.set()." range { startLine = 2 startCharacter = 4 @@ -527,7 +528,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "Test#getSample()." + symbol = "Test#sample.get()." range { startLine = 0 startCharacter = 15 @@ -541,7 +542,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = DEFINITION - symbol = "Test#setSample()." + symbol = "Test#sample.set()." range { startLine = 0 startCharacter = 15 @@ -575,7 +576,7 @@ class ScipSymbolsTest { }, scipOccurrence { role = REFERENCE - symbol = "Test#getSample()." + symbol = "Test#sample.get()." range { startLine = 2 startCharacter = 16 @@ -775,12 +776,16 @@ class ScipSymbolsTest { listOf( scipSymbol { symbol = "x." + kind = Kind.Property + enclosingSymbol = "_root_/" displayName = "x" signatureText = "public final val x: String" documentation("hello world\n test content") }, scipSymbol { - symbol = "getX()." + symbol = "x.get()." + kind = Kind.Method + enclosingSymbol = "x." displayName = "x" signatureText = "public get(): String" documentation("hello world\n test content") diff --git a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt index 4576e9893..5622fe594 100644 --- a/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt +++ b/scip-kotlinc/src/test/kotlin/org/scip_code/scip_java/kotlinc/test/Utils.kt @@ -193,6 +193,7 @@ fun scipVisitorAnalyzer( ) IrGenerationExtension.registerExtension( PostAnalysisExtension( + configuration = configuration, sourceRoot = sourceroot, targetRoot = Paths.get(""), callback = hook, @@ -200,6 +201,8 @@ fun scipVisitorAnalyzer( ) } + override val pluginId = PLUGIN_ID + override val supportsK2: Boolean get() = true }