Skip to content

Commit

Permalink
Add detekt compiler plugin to main project (#5492)
Browse files Browse the repository at this point in the history
* Add DetektKotlinCompilerPlugin to main project

* Allow both Gradle plugins to be used in the same project

* Restore flag on extension to allow enabling or disabling compiler plugin

* Enable detekt compiler plugin on Kotlin tasks by default

This only has an effect when io.github.detekt.gradle.compiler-plugin is
applied to the project. It has no effect on io.gitlab.arturbosch.detekt.

* Use workaround to check for expected compiler plugin version

* Copy plugin-build source into main project

detekt/detekt-compiler-plugin@7f3594d

* Integrate detekt-compiler-plugin into build

* Fix style issues

* Explain purpose of DEFAULT_COMPILER_PLUGIN_ENABLED

* Use kotlinVersion-detektVersion scheme for compiler plugin versioning

* Reuse constants where possible

* Remove unused `detektPluginVersion`

Co-authored-by: Chao Zhang <chao.zhang@instacart.com>
  • Loading branch information
3flex and chao2zhang committed Dec 4, 2022
1 parent 8aa1c82 commit 0abd43d
Show file tree
Hide file tree
Showing 24 changed files with 913 additions and 2 deletions.
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) {
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()) }
}

0 comments on commit 0abd43d

Please sign in to comment.