From 2cdf33cb4cbb07c8c37b861777339e429fe27c59 Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Sun, 2 Jun 2024 18:06:24 +0200 Subject: [PATCH] Use FlexInspect for inspections rather than KSP (#941) --- .../commonMain/kotlin/entity/DiscordUser.kt | 2 - inspections/BuilderDslMarker.inspection.kts | 62 +++++++++++++++ .../OptionalWithoutDefault.inspection.kts | 78 +++++++++++++++++++ .../BuilderDslMarkerInspectionProcessor.kt | 36 --------- .../OptionalDefaultInspectionProcessor.kt | 52 ------------- ...ols.ksp.processing.SymbolProcessorProvider | 2 - qodana.yaml | 13 ++++ 7 files changed, 153 insertions(+), 92 deletions(-) create mode 100644 inspections/BuilderDslMarker.inspection.kts create mode 100644 inspections/OptionalWithoutDefault.inspection.kts delete mode 100644 ksp-processors/src/main/kotlin/inspection/BuilderDslMarkerInspectionProcessor.kt delete mode 100644 ksp-processors/src/main/kotlin/inspection/OptionalDefaultInspectionProcessor.kt create mode 100644 qodana.yaml diff --git a/common/src/commonMain/kotlin/entity/DiscordUser.kt b/common/src/commonMain/kotlin/entity/DiscordUser.kt index a891325d4ee2..429a98e8748c 100644 --- a/common/src/commonMain/kotlin/entity/DiscordUser.kt +++ b/common/src/commonMain/kotlin/entity/DiscordUser.kt @@ -51,8 +51,6 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract /** * A representation of the [Discord User structure](https://discord.com/developers/docs/resources/user). diff --git a/inspections/BuilderDslMarker.inspection.kts b/inspections/BuilderDslMarker.inspection.kts new file mode 100644 index 000000000000..14b04427e219 --- /dev/null +++ b/inspections/BuilderDslMarker.inspection.kts @@ -0,0 +1,62 @@ +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.project.Project +import org.intellij.lang.annotations.Language +import org.jetbrains.kotlin.idea.util.addAnnotation +import org.jetbrains.kotlin.idea.util.findAnnotation +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.psi.KtAnnotated +import org.jetbrains.kotlin.psi.KtClass + +@Language("HTML") +val htmlDescription = """ + + + This reports subtypes of dev.kord.rest.builder.RequestBuilder which are not annotated with + @KordDsl + + +""".trimIndent() + + +val kordDslId = ClassId.fromString("dev/kord/common/annotation/KordDsl") +val requestBuilderId = ClassId.fromString("dev/kord/rest/builder/RequestBuilder") + +class AddKordDsl : LocalQuickFix { + override fun getFamilyName(): String = "Add @KordDsl annotation" + + override fun applyFix(project: Project, problem: ProblemDescriptor) { + problem.psiElement.parentsOfType().first().addAnnotation(kordDslId, searchForExistingEntry = false) + } +} + +val builderWithoutDslMarkerInspection = localInspection { psiFile, inspection -> + + psiFile.descendantsOfType().forEach { + analyze(it) { + val requestBuilderClass = getClassOrObjectSymbolByClassId(requestBuilderId) ?: return@analyze + if (it.getClassOrObjectSymbol()?.isSubClassOf(requestBuilderClass) == true + && it.findAnnotation(kordDslId) == null + && it.hasModifier(KtTokens.PUBLIC_KEYWORD) + ) { + inspection.registerProblem( + it.nameIdentifier, + "This class should be annotated with @KordDsl", + AddKordDsl() + ) + } + } + } +} + + +listOf( + InspectionKts( + id = "BuilderDslMarker", + localTool = builderWithoutDslMarkerInspection, + name = "Reports builder's without DSL annotations", + htmlDescription = htmlDescription, + level = HighlightDisplayLevel.ERROR, + ) +) \ No newline at end of file diff --git a/inspections/OptionalWithoutDefault.inspection.kts b/inspections/OptionalWithoutDefault.inspection.kts new file mode 100644 index 000000000000..3f7c53c0f236 --- /dev/null +++ b/inspections/OptionalWithoutDefault.inspection.kts @@ -0,0 +1,78 @@ +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.project.Project +import org.intellij.lang.annotations.Language +import org.jetbrains.kotlin.idea.base.psi.setDefaultValue +import org.jetbrains.kotlin.idea.util.findAnnotation +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.psi.* + +private val optionalTypes = + listOf("Optional", "OptionalBoolean", "OptionalInt", "OptionalLong", "OptionalSnowflake") + .map { "dev.kord.common.entity.optional.$it" } + .toSet() + +@Language("HTML") +val htmlDescription = """ + + + This inspection reports misusage of 'Optional' classes, when using them without a default value + + Supported classes: ${optionalTypes.map { "${it.substringAfterLast('.')}" }} + + +""".trimIndent() + +private class AddDefaultQuickfix(private val optionalClassName: String) : LocalQuickFix { + override fun getFamilyName(): String = "Add '${optionalClassName}.Missing' as a default value" + + override fun applyFix(project: Project, problem: ProblemDescriptor) { + val factory = KtPsiFactory(project) + val optionalType = buildString { + append(optionalClassName) + append(".Missing") + if (optionalClassName == "Optional") { + append("()") + } + } + val initializer = factory.createExpression(optionalType) + + val parameter = problem.psiElement as KtParameter + parameter.setDefaultValue(initializer) + } +} + +val optionalWithoutDefaultInspection = localInspection { psiFile, inspection -> + val serializable = ClassId.fromString("kotlinx/serialization/Serializable") + + psiFile + .descendantsOfType() + .filter { it.findAnnotation(serializable, withResolve = true) != null } + .flatMap(KtClass::allConstructors) + .flatMap(KtConstructor<*>::getValueParameters) + .filterNot(KtParameter::hasDefaultValue) + .filter { it.isOptionalTypeParameter() } + .forEach { + analyze(it) { + inspection.registerProblem( + it, + "This parameter should have a default value", + AddDefaultQuickfix(it.getReturnKtType().expandedClassSymbol!!.name!!.asString()) + ) + } + } +} + +fun KtParameter.isOptionalTypeParameter() = analyze(this) { + getReturnKtType().fullyExpandedType.expandedClassSymbol?.getFQN() in optionalTypes +} + +listOf( + InspectionKts( + id = "OptionalWithoutDefault", + localTool = optionalWithoutDefaultInspection, + name = "Optional without default inspection", + htmlDescription = htmlDescription, + level = HighlightDisplayLevel.NON_SWITCHABLE_ERROR, + ) +) \ No newline at end of file diff --git a/ksp-processors/src/main/kotlin/inspection/BuilderDslMarkerInspectionProcessor.kt b/ksp-processors/src/main/kotlin/inspection/BuilderDslMarkerInspectionProcessor.kt deleted file mode 100644 index 168bf8f42d6c..000000000000 --- a/ksp-processors/src/main/kotlin/inspection/BuilderDslMarkerInspectionProcessor.kt +++ /dev/null @@ -1,36 +0,0 @@ -package dev.kord.ksp.inspection - -import com.google.devtools.ksp.getAllSuperTypes -import com.google.devtools.ksp.processing.* -import com.google.devtools.ksp.symbol.KSAnnotated -import com.google.devtools.ksp.symbol.Modifier -import dev.kord.ksp.getNewClasses -import dev.kord.ksp.isOfType - -class BuilderDslMarkerInspectionProcessorProvider : SymbolProcessorProvider { - override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = - BuilderDslMarkerInspectionProcessor(environment.logger) -} - -private class BuilderDslMarkerInspectionProcessor(private val logger: KSPLogger) : SymbolProcessor { - override fun process(resolver: Resolver): List { - resolver.getNewClasses() - // some internal implementations are not annotated with @KordDsl on purpose - .filterNot { Modifier.INTERNAL in it.modifiers } - .filter { - it.annotations.none { annotation -> - annotation.isOfType("dev.kord.common.annotation.KordDsl") - } - } - .filter { - it.getAllSuperTypes().any { type -> - type.declaration.qualifiedName?.asString() == "dev.kord.rest.builder.RequestBuilder" - } - } - .forEach { - logger.error("Found builder without @KordDsl", symbol = it) - } - - return emptyList() // we never have to defer any symbols - } -} diff --git a/ksp-processors/src/main/kotlin/inspection/OptionalDefaultInspectionProcessor.kt b/ksp-processors/src/main/kotlin/inspection/OptionalDefaultInspectionProcessor.kt deleted file mode 100644 index 993fbbe965d3..000000000000 --- a/ksp-processors/src/main/kotlin/inspection/OptionalDefaultInspectionProcessor.kt +++ /dev/null @@ -1,52 +0,0 @@ -package dev.kord.ksp.inspection - -import com.google.devtools.ksp.findActualType -import com.google.devtools.ksp.processing.* -import com.google.devtools.ksp.symbol.* -import dev.kord.ksp.getSymbolsWithAnnotation -import dev.kord.ksp.isClassifierReference -import kotlinx.serialization.Serializable - -/** [SymbolProcessorProvider] for [OptionalDefaultInspectionProcessor]. */ -class OptionalDefaultInspectionProcessorProvider : SymbolProcessorProvider { - override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = - OptionalDefaultInspectionProcessor(environment.logger) -} - -private val OPTIONAL_TYPES = - listOf("Optional", "OptionalBoolean", "OptionalInt", "OptionalLong", "OptionalSnowflake") - .map { "dev.kord.common.entity.optional.$it" } - .toSet() - -/** - * [SymbolProcessor] that verifies that every primary constructor parameter with `Optional` type of a [Serializable] - * class has a default value. - */ -private class OptionalDefaultInspectionProcessor(private val logger: KSPLogger) : SymbolProcessor { - override fun process(resolver: Resolver): List { - resolver.getSymbolsWithAnnotation() - .filterIsInstance() - .forEach { it.verifySerializableClassPrimaryConstructor() } - - return emptyList() // we never have to defer any symbols - } - - private fun KSClassDeclaration.verifySerializableClassPrimaryConstructor() { - primaryConstructor?.parameters?.forEach { parameter -> - if (parameter.hasDefault) return@forEach - - val type = parameter.type - if (type.element?.isClassifierReference == false) return@forEach - - val clazz = when (val declaration = type.resolve().declaration) { - is KSTypeParameter -> return@forEach - is KSClassDeclaration -> declaration - is KSTypeAlias -> declaration.findActualType() - else -> error("Unexpected KSDeclaration: $declaration") - } - if (clazz.qualifiedName?.asString() in OPTIONAL_TYPES) { - logger.error("Missing default for parameter ${parameter.name?.asString()}.", symbol = parameter) - } - } - } -} diff --git a/ksp-processors/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/ksp-processors/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider index 34373dcd977e..92148a6b0adb 100644 --- a/ksp-processors/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider +++ b/ksp-processors/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -1,3 +1 @@ dev.kord.ksp.generation.GenerationProcessorProvider -dev.kord.ksp.inspection.BuilderDslMarkerInspectionProcessorProvider -dev.kord.ksp.inspection.OptionalDefaultInspectionProcessorProvider diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 000000000000..bbad4ac4cb2b --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,13 @@ +version: "1.0" + +profile: + name: qodana.recommended + +exclude: + - name: All + paths: + - voice/src/main/java/com/iwebpp/crypto/TweetNaclFast.java + +projectJDK: "8" + +linter: jetbrains/qodana-jvm