Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add detekt compiler plugin to main project #5492

Merged
merged 12 commits into from Dec 4, 2022
Merged
117 changes: 117 additions & 0 deletions detekt-compiler-plugin/build.gradle.kts
@@ -0,0 +1,117 @@
import de.undercouch.gradle.tasks.download.Download
import de.undercouch.gradle.tasks.download.Verify
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.ByteArrayOutputStream

val kotlinVersion: String = libs.versions.kotlin.get()
val detektVersion: String = Versions.DETEKT

val kotlinCompilerChecksum: String by project

group = "io.github.detekt"
version = "$kotlinVersion-$detektVersion"

val detektPublication = "DetektPublication"

plugins {
id("module")
alias(libs.plugins.gradleVersions)
alias(libs.plugins.shadow)
alias(libs.plugins.download)
}

repositories {
mavenCentral()
mavenLocal()
}

dependencies {
compileOnly(kotlin("stdlib"))
compileOnly(kotlin("compiler-embeddable"))

implementation(projects.detektApi)
implementation(projects.detektTooling)
runtimeOnly(projects.detektCore)
runtimeOnly(projects.detektRules)

testImplementation(libs.assertj)
testImplementation(libs.kotlinCompileTesting)
}

val javaComponent = components["java"] as AdhocComponentWithVariants
javaComponent.withVariantsFromConfiguration(configurations["shadowRuntimeElements"]) {
skip()
}

tasks.shadowJar.configure {
relocate("org.jetbrains.kotlin.com.intellij", "com.intellij")
mergeServiceFiles()
dependencies {
include(dependency("io.gitlab.arturbosch.detekt:.*"))
include(dependency("io.github.detekt:.*"))
include(dependency("org.yaml:snakeyaml"))
include(dependency("io.github.davidburstrom.contester:contester-breakpoint"))
}
}

val verifyKotlinCompilerDownload by tasks.registering(Verify::class) {
3flex marked this conversation as resolved.
Show resolved Hide resolved
src(file("$rootDir/build/kotlinc/kotlin-compiler-$kotlinVersion.zip"))
algorithm("SHA-256")
checksum(kotlinCompilerChecksum)
outputs.upToDateWhen { true }
}

val downloadKotlinCompiler by tasks.registering(Download::class) {
src("https://github.com/JetBrains/kotlin/releases/download/v$kotlinVersion/kotlin-compiler-$kotlinVersion.zip")
dest(file("$rootDir/build/kotlinc/kotlin-compiler-$kotlinVersion.zip"))
overwrite(false)
finalizedBy(verifyKotlinCompilerDownload)
}

val unzipKotlinCompiler by tasks.registering(Copy::class) {
dependsOn(downloadKotlinCompiler)
from(zipTree(downloadKotlinCompiler.get().dest))
into(file("$rootDir/build/kotlinc/$kotlinVersion"))
}

val testPluginKotlinc by tasks.registering(RunTestExecutable::class) {
dependsOn(unzipKotlinCompiler, tasks.shadowJar)

args(
listOf(
"$rootDir/src/test/resources/hello.kt",
"-Xplugin=${tasks.shadowJar.get().archiveFile.get().asFile.absolutePath}",
"-P",
)
)

val baseExecutablePath = "${unzipKotlinCompiler.get().destinationDir}/kotlinc/bin/kotlinc"
val pluginParameters = "plugin:detekt-compiler-plugin:debug=true"

if (org.apache.tools.ant.taskdefs.condition.Os.isFamily("windows")) {
executable(file("$baseExecutablePath.bat"))
args("\"$pluginParameters\"")
} else {
executable(file(baseExecutablePath))
args(pluginParameters)
}

errorOutput = ByteArrayOutputStream()
// dummy path - required for RunTestExecutable task but doesn't do anything
outputDir = file("$buildDir/tmp/kotlinc")

doLast {
if (!errorOutput.toString().contains("warning: magicNumber:")) {
throw GradleException(
"kotlinc $kotlinVersion run with compiler plugin did not find MagicNumber issue as expected"
)
}
(this as RunTestExecutable).executionResult.get().assertNormalExitValue()
}
}

tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs = listOf(
"-opt-in=kotlin.RequiresOptIn"
)
}
6 changes: 6 additions & 0 deletions detekt-compiler-plugin/gradle.properties
@@ -0,0 +1,6 @@
kotlinCompilerChecksum=8412b31b808755f0c0d336dbb8c8443fa239bf32ddb3cdb81b305b25f0ad279e

kotlin.code.style=official
systemProp.sonar.host.url=http://localhost:9000
systemProp.detektVersion=detektVersion
systemProp.file.encoding=UTF-8
@@ -0,0 +1,42 @@
package io.github.detekt.compiler.plugin

import io.github.detekt.compiler.plugin.internal.DetektService
import io.github.detekt.compiler.plugin.internal.info
import io.github.detekt.tooling.api.spec.ProcessingSpec
import org.jetbrains.kotlin.analyzer.AnalysisResult
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.com.intellij.openapi.project.Project
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 java.nio.file.FileSystems
import java.nio.file.Path
import java.nio.file.Paths

class DetektAnalysisExtension(
private val log: MessageCollector,
private val spec: ProcessingSpec,
private val rootPath: Path,
private val excludes: Collection<String>
) : AnalysisHandlerExtension {

override fun analysisCompleted(
project: Project,
module: ModuleDescriptor,
bindingTrace: BindingTrace,
files: Collection<KtFile>
): AnalysisResult? {
if (spec.loggingSpec.debug) {
log.info("$spec")
}
val matchers = excludes.map { FileSystems.getDefault().getPathMatcher("glob:$it") }
val (includedFiles, excludedFiles) = files.partition { file ->
matchers.none { it.matches(rootPath.relativize(Paths.get(file.virtualFilePath))) }
}
log.info("Running detekt on module '${module.name.asString()}'")
excludedFiles.forEach { log.info("File excluded by filter: ${it.virtualFilePath}") }
DetektService(log, spec).analyze(includedFiles, bindingTrace.bindingContext)
return null
}
}
@@ -0,0 +1,132 @@
package io.github.detekt.compiler.plugin

import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption
import org.jetbrains.kotlin.compiler.plugin.CliOption
import org.jetbrains.kotlin.compiler.plugin.CliOptionProcessingException
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
import org.jetbrains.kotlin.config.CompilerConfiguration
import java.io.ByteArrayInputStream
import java.io.ObjectInputStream
import java.nio.file.Paths
import java.util.Base64

class DetektCommandLineProcessor : CommandLineProcessor {

override val pluginId: String = "detekt-compiler-plugin"

@Suppress("StringLiteralDuplication")
override val pluginOptions: Collection<AbstractCliOption> = listOf(
CliOption(
Options.config,
"<path|paths>",
"Comma separated paths to detekt config files.",
false
),
CliOption(
Options.configDigest,
"<digest>",
"A digest calculated from the content of the config files. Used for Gradle incremental task invalidation.",
false
),
CliOption(
Options.baseline,
"<path>",
"Path to a detekt baseline file.",
false
),
CliOption(
Options.debug,
"<true|false>",
"Print debug messages.",
false
),
CliOption(
Options.isEnabled,
"<true|false>",
"Should detekt run?",
false
),
CliOption(
Options.useDefaultConfig,
"<true|false>",
"Use the default detekt config as baseline.",
false
),
CliOption(
Options.allRules,
"<true|false>",
"Turns on all the rules.",
false
),
CliOption(
Options.disableDefaultRuleSets,
"<true|false>",
"Disables all default detekt rulesets and will only run detekt with custom rules " +
"defined in plugins passed in with `detektPlugins` configuration.",
false
),
CliOption(
Options.parallel,
"<true|false>",
"Enables parallel compilation and analysis of source files.",
false
),
CliOption(
Options.rootPath,
"<path>",
"Root path used to relativize paths when using exclude patterns.",
false
),
CliOption(
Options.excludes,
"<base64-encoded globs>",
"A base64-encoded list of the globs used to exclude paths from scanning.",
false
),
CliOption(
Options.report,
"<report-id:path>",
"Generates a report for given 'report-id' and stores it on given 'path'. " +
"Available 'report-id' values: 'txt', 'xml', 'html'.",
false,
allowMultipleOccurrences = true
)
)

override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) {
when (option.optionName) {
Options.baseline -> configuration.put(Keys.BASELINE, Paths.get(value))
Options.config -> configuration.put(Keys.CONFIG, value.split(",;").map { Paths.get(it) })
Options.configDigest -> configuration.put(Keys.CONFIG_DIGEST, value)
Options.debug -> configuration.put(Keys.DEBUG, value.toBoolean())
Options.isEnabled -> configuration.put(Keys.IS_ENABLED, value.toBoolean())
Options.useDefaultConfig -> configuration.put(Keys.USE_DEFAULT_CONFIG, value.toBoolean())
Options.allRules -> configuration.put(Keys.ALL_RULES, value.toBoolean())
Options.disableDefaultRuleSets -> configuration.put(Keys.DISABLE_DEFAULT_RULE_SETS, value.toBoolean())
Options.parallel -> configuration.put(Keys.PARALLEL, value.toBoolean())
Options.rootPath -> configuration.put(Keys.ROOT_PATH, Paths.get(value))
Options.excludes -> configuration.put(Keys.EXCLUDES, value.decodeToGlobSet())
Options.report -> configuration.put(
Keys.REPORTS,
value.substringBefore(':'),
Paths.get(value.substringAfter(':')),
)
else -> throw CliOptionProcessingException("Unknown option: ${option.optionName}")
}
}
}

private fun String.decodeToGlobSet(): List<String> {
val b = Base64.getDecoder().decode(this)
val bi = ByteArrayInputStream(b)

return ObjectInputStream(bi).use { inputStream ->
val globs = mutableListOf<String>()

repeat(inputStream.readInt()) {
globs.add(inputStream.readUTF())
}

globs
}
}
@@ -0,0 +1,34 @@
package io.github.detekt.compiler.plugin

import io.github.detekt.compiler.plugin.internal.toSpec
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.com.intellij.mock.MockProject
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension
import java.nio.file.Paths

class DetektComponentRegistrar : ComponentRegistrar {

override fun registerProjectComponents(
project: MockProject,
configuration: CompilerConfiguration
) {
if (configuration.get(Keys.IS_ENABLED) == false) {
return
}

val messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)

AnalysisHandlerExtension.registerExtension(
project,
DetektAnalysisExtension(
messageCollector,
configuration.toSpec(messageCollector),
configuration.get(Keys.ROOT_PATH, Paths.get(System.getProperty("user.dir"))),
configuration.getList(Keys.EXCLUDES)
)
)
}
}
@@ -0,0 +1,37 @@
package io.github.detekt.compiler.plugin

import org.jetbrains.kotlin.config.CompilerConfigurationKey
import java.nio.file.Path

object Options {

@Suppress("NonBooleanPropertyPrefixedWithIs")
const val isEnabled: String = "isEnabled"
const val debug: String = "debug"
const val config = "config"
const val configDigest: String = "configDigest"
const val baseline: String = "baseline"
const val useDefaultConfig: String = "useDefaultConfig"
const val allRules: String = "allRules"
const val disableDefaultRuleSets: String = "disableDefaultRuleSets"
const val parallel: String = "parallel"
const val rootPath = "rootDir"
const val excludes = "excludes"
const val report = "report"
}

object Keys {

val DEBUG = CompilerConfigurationKey.create<Boolean>(Options.debug)
val IS_ENABLED = CompilerConfigurationKey.create<Boolean>(Options.isEnabled)
val CONFIG = CompilerConfigurationKey.create<List<Path>>(Options.config)
val CONFIG_DIGEST = CompilerConfigurationKey.create<String>(Options.configDigest)
val BASELINE = CompilerConfigurationKey.create<Path>(Options.baseline)
val USE_DEFAULT_CONFIG = CompilerConfigurationKey.create<Boolean>(Options.useDefaultConfig)
val ALL_RULES = CompilerConfigurationKey.create<Boolean>(Options.allRules)
val DISABLE_DEFAULT_RULE_SETS = CompilerConfigurationKey.create<Boolean>(Options.disableDefaultRuleSets)
val PARALLEL = CompilerConfigurationKey.create<Boolean>(Options.parallel)
val ROOT_PATH = CompilerConfigurationKey.create<Path>(Options.rootPath)
val EXCLUDES = CompilerConfigurationKey.create<List<String>>(Options.excludes)
val REPORTS = CompilerConfigurationKey.create<Map<String, Path>>(Options.report)
}
@@ -0,0 +1,11 @@
package io.github.detekt.compiler.plugin.internal

internal class AppendableAdapter(val logging: (String) -> Unit) : Appendable {

override fun append(csq: CharSequence): Appendable = also { logging(csq.toString()) }

override fun append(csq: CharSequence, start: Int, end: Int): Appendable =
also { logging(csq.substring(start, end)) }

override fun append(c: Char): Appendable = also { logging(c.toString()) }
}