diff --git a/.github/workflows/diktat_snapshot.yml b/.github/workflows/diktat_snapshot.yml index 6ab4b7e62f..f1c541fa03 100644 --- a/.github/workflows/diktat_snapshot.yml +++ b/.github/workflows/diktat_snapshot.yml @@ -48,6 +48,8 @@ jobs: arguments: | :diktat-common:publishToMavenLocal :diktat-rules:publishToMavenLocal + :diktat-runner:diktat-runner-api:publishToMavenLocal + :diktat-runner:diktat-runner-ktlint-engine:publishToMavenLocal :diktat-gradle-plugin:publishToMavenLocal :generateLibsForDiktatSnapshot -x detekt diff --git a/build.gradle.kts b/build.gradle.kts index f4c6c56c5c..106d4180a5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,11 +32,13 @@ tasks.create("generateLibsForDiktatSnapshot") { val dir = rootProject.buildDir.resolve("diktat-snapshot") val dependencies = setOf( - projects.diktatCommon, - projects.diktatRules, - projects.diktatGradlePlugin, + rootProject.project(":diktat-common"), + rootProject.project(":diktat-rules"), + rootProject.project(":diktat-runner:diktat-runner-api"), + rootProject.project(":diktat-runner:diktat-runner-ktlint-engine"), + rootProject.project(":diktat-gradle-plugin"), ) - mustRunAfter(dependencies.map { ":${it.name}:publishToMavenLocal" }) + mustRunAfter(dependencies.map { "${it.path}:publishToMavenLocal" }) val libsFile = rootProject.file("gradle/libs.versions.toml") inputs.file(libsFile) @@ -72,19 +74,19 @@ tasks.create("generateLibsForDiktatSnapshot") { } /** - * @param projectDependency + * @param project * @return resolved path to directory according to maven coordinate */ -fun File.pathToMavenArtifact(projectDependency: ProjectDependency): File = projectDependency.group.toString() +fun File.pathToMavenArtifact(project: Project): File = project.group.toString() .split(".") .fold(this) { dirToArtifact, newPart -> dirToArtifact.resolve(newPart) } - .resolve(projectDependency.name) - .resolve(projectDependency.version.toString()) + .resolve(project.name) + .resolve(project.version.toString()) /** * @return generated pom.xml for project dependency */ -fun ProjectDependency.pomFile(): File = rootProject.file("$name/build/publications/") +fun Project.pomFile(): File = buildDir.resolve("publications") .let { publicationsDir -> publicationsDir.resolve("pluginMaven") .takeIf { it.exists() } @@ -93,16 +95,16 @@ fun ProjectDependency.pomFile(): File = rootProject.file("$name/build/publicatio .resolve("pom-default.xml") /** - * @return file name of pom.xml for project dependency + * @return file name of pom.xml for project */ -fun ProjectDependency.pomFileName(): String = "$name-$version.pom" +fun Project.pomFileName(): String = "$name-$version.pom" /** * @return generated artifact for project dependency */ -fun ProjectDependency.artifactFile(): File = rootProject.file("$name/build/libs/$name-$version.jar") +fun Project.artifactFile(): File = buildDir.resolve("libs/${artifactFileName()}") /** * @return file name of artifact for project dependency */ -fun ProjectDependency.artifactFileName(): String = "$name-$version.jar" +fun Project.artifactFileName(): String = "$name-$version.jar" diff --git a/diktat-common/src/main/kotlin/org/cqfn/diktat/common/ktlint/KtLintUtils.kt b/diktat-common/src/main/kotlin/org/cqfn/diktat/common/ktlint/KtLintUtils.kt deleted file mode 100644 index 4e45c72d02..0000000000 --- a/diktat-common/src/main/kotlin/org/cqfn/diktat/common/ktlint/KtLintUtils.kt +++ /dev/null @@ -1,22 +0,0 @@ -@file:Suppress("HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE") - -package org.cqfn.diktat.common.ktlint - -private val ktlintRuleSetIds: List by lazy { - listOf( - "standard", - "experimental", - "test", - "custom" - ) -} - -/** - * Contains the necessary value of the `--disabled_rules` _KtLint_ argument. - */ -val ktlintDisabledRulesArgument: String by lazy { - ktlintRuleSetIds.joinToString( - prefix = "--disabled_rules=", - separator = "," - ) -} diff --git a/diktat-gradle-plugin/build.gradle.kts b/diktat-gradle-plugin/build.gradle.kts index 2fc6a6626e..0d70ded181 100644 --- a/diktat-gradle-plugin/build.gradle.kts +++ b/diktat-gradle-plugin/build.gradle.kts @@ -13,17 +13,16 @@ plugins { dependencies { implementation(kotlin("gradle-plugin-api")) + implementation(projects.diktatRunner.diktatRunnerKtlintEngine) + implementation(libs.ktlint.core) + implementation(libs.ktlint.reporter.plain) + implementation(libs.ktlint.reporter.sarif) + implementation(libs.ktlint.reporter.json) + implementation(libs.ktlint.reporter.html) + implementation(libs.ktlint.reporter.baseline) + // merge sarif reports implementation(libs.sarif4k.jvm) - - api(projects.diktatCommon) { - exclude("org.jetbrains.kotlin", "kotlin-compiler-embeddable") - exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8") - exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7") - exclude("org.jetbrains.kotlin", "kotlin-stdlib") - exclude("org.jetbrains.kotlin", "kotlin-stdlib-common") - exclude("org.slf4j", "slf4j-log4j12") - } - + implementation(libs.kotlinx.serialization.json) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) } diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePlugin.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePlugin.kt index e444c458b6..7980e01e92 100644 --- a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePlugin.kt +++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePlugin.kt @@ -1,12 +1,10 @@ package org.cqfn.diktat.plugin.gradle +import org.cqfn.diktat.plugin.gradle.tasks.DiktatCheckTask.Companion.registerDiktatCheckTask +import org.cqfn.diktat.plugin.gradle.tasks.DiktatFixTask.Companion.registerDiktatFixTask import org.cqfn.diktat.plugin.gradle.tasks.configureMergeReportsTask -import generated.DIKTAT_VERSION -import generated.KTLINT_VERSION import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.artifacts.ExternalModuleDependency -import org.gradle.api.attributes.Bundling import org.gradle.api.tasks.util.PatternSet /** @@ -28,38 +26,8 @@ class DiktatGradlePlugin : Plugin { diktatConfigFile = project.rootProject.file("diktat-analysis.yml") } - // Configuration that will be used as classpath for JavaExec task. - val diktatConfiguration = project.configurations.create(DIKTAT_CONFIGURATION) { configuration -> - configuration.isVisible = false - configuration.dependencies.add(project.dependencies.create("com.pinterest:ktlint:$KTLINT_VERSION", closureOf { - /* - * Prevent the discovery of standard rules by excluding them as - * dependencies. - */ - val ktlintRuleSets = sequenceOf( - "standard", - "experimental", - "test", - "template" - ) - ktlintRuleSets.forEach { ktlintRuleSet -> - exclude( - mutableMapOf( - "group" to "com.pinterest.ktlint", - "module" to "ktlint-ruleset-$ktlintRuleSet" - ) - ) - } - - attributes { - it.attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling::class.java, Bundling.EXTERNAL)) - } - })) - configuration.dependencies.add(project.dependencies.create("org.cqfn.diktat:diktat-rules:$DIKTAT_VERSION")) - } - - project.registerDiktatCheckTask(diktatExtension, diktatConfiguration, patternSet) - project.registerDiktatFixTask(diktatExtension, diktatConfiguration, patternSet) + project.registerDiktatCheckTask(diktatExtension, patternSet) + project.registerDiktatFixTask(diktatExtension, patternSet) project.configureMergeReportsTask(diktatExtension) } diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskBase.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskBase.kt index 94f1690088..aca13fb5b8 100644 --- a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskBase.kt +++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskBase.kt @@ -1,250 +1,9 @@ package org.cqfn.diktat.plugin.gradle -import org.cqfn.diktat.common.config.rules.DIKTAT_CONF_PROPERTY -import org.cqfn.diktat.common.ktlint.ktlintDisabledRulesArgument -import org.cqfn.diktat.plugin.gradle.DiktatGradlePlugin.Companion.DIKTAT_CHECK_TASK -import org.cqfn.diktat.plugin.gradle.DiktatGradlePlugin.Companion.DIKTAT_FIX_TASK -import org.cqfn.diktat.plugin.gradle.DiktatGradlePlugin.Companion.MIN_JVM_REQUIRES_ADD_OPENS - -import generated.DIKTAT_VERSION -import generated.KTLINT_VERSION -import org.gradle.api.JavaVersion -import org.gradle.api.Project -import org.gradle.api.artifacts.Configuration -import org.gradle.api.file.FileCollection -import org.gradle.api.provider.Property -import org.gradle.api.tasks.IgnoreEmptyDirectories -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.Internal -import org.gradle.api.tasks.JavaExec -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.SkipWhenEmpty -import org.gradle.api.tasks.TaskAction -import org.gradle.api.tasks.TaskProvider -import org.gradle.api.tasks.VerificationTask -import org.gradle.api.tasks.util.PatternFilterable -import org.gradle.api.tasks.util.PatternSet -import org.gradle.util.GradleVersion - -import java.io.File -import javax.inject.Inject - -/** - * A base diktat task for gradle <6.8, which wraps [JavaExec]. - * - * Note: class being `open` is required for gradle to create a task. - */ -open class DiktatJavaExecTaskBase @Inject constructor( - private val gradleVersionString: String, - diktatExtension: DiktatExtension, - diktatConfiguration: Configuration, - private val inputs: PatternFilterable, - additionalFlags: Iterable = emptyList() -) : JavaExec(), VerificationTask { - /** - * A backing [Property] for [getIgnoreFailures] and [setIgnoreFailures] - */ - @get:Internal - internal val ignoreFailuresProp: Property = project.objects.property(Boolean::class.javaObjectType) - - /** - * Whether diktat should be executed via JavaExec or not. - */ - @get:Internal - internal var shouldRun = true - - /** - * Files that will be analyzed by diktat - */ - @get:IgnoreEmptyDirectories - @get:SkipWhenEmpty - @get:PathSensitive(PathSensitivity.RELATIVE) - @get:InputFiles - val actualInputs: FileCollection by lazy { - if (inputs.includes.isEmpty() && inputs.excludes.isEmpty()) { - inputs.include("src/**/*.kt") - } - project.objects.fileCollection().from( - project.fileTree("${project.projectDir}").apply { - exclude("${project.buildDir}") - } - .matching(inputs) - ) - } - - init { - group = "verification" - if (isMainClassPropertySupported(gradleVersionString)) { - // `main` is deprecated and replaced with `mainClass` since gradle 6.4 - mainClass.set("com.pinterest.ktlint.Main") - } else { - @Suppress("Deprecation") - main = "com.pinterest.ktlint.Main" - } - - classpath = diktatConfiguration - project.logger.debug("Setting diktatCheck classpath to ${diktatConfiguration.dependencies.toSet()}") - if (diktatExtension.debug) { - project.logger.lifecycle("Running diktat $DIKTAT_VERSION with ktlint $KTLINT_VERSION") - } - ignoreFailures = diktatExtension.ignoreFailures - isIgnoreExitValue = ignoreFailures // ignore returned value of JavaExec started process if lint errors shouldn't fail the build - systemProperty(DIKTAT_CONF_PROPERTY, resolveConfigFile(diktatExtension.diktatConfigFile).also { - project.logger.info("Setting system property for diktat config to $it") - }) - args = additionalFlags.toMutableList().apply { - /* - * Disable the standard rules via the command line. - * - * Classpath exclusion (see `DiktatGradlePlugin`) is enough, but - * this is better left enabled as a safety net. - */ - add(ktlintDisabledRulesArgument) - - if (diktatExtension.debug) { - add("--debug") - } - diktatExtension.baseline?.let { - add("--baseline=${diktatExtension.baseline}") - } - actualInputs.also { - if (it.isEmpty) { - /* - If ktlint receives empty patterns, it implicitly uses **/*.kt, **/*.kts instead. - This can lead to diktat analyzing gradle buildscripts and so on. We want to prevent it. - */ - project.logger.warn("Inputs for $name do not exist, will not run diktat") - shouldRun = false - } - } - .files - .also { files -> - project.logger.info("Analyzing ${files.size} files with diktat in project ${project.name}") - project.logger.debug("Analyzing $files") - } - .forEach { - addInput(it) - } - - add(reporterFlag(diktatExtension)) - } - project.logger.debug("Setting JavaExec args to $args") - } - - /** - * Function to execute diKTat - */ - @TaskAction - override fun exec() { - fixForNewJpms() - if (shouldRun) { - super.exec() - } else { - project.logger.info("Skipping diktat execution") - } - } - - /** - * @param ignoreFailures whether failure in this plugin should be ignored by a build - */ - override fun setIgnoreFailures(ignoreFailures: Boolean) = ignoreFailuresProp.set(ignoreFailures) - - /** - * @return whether failure in this plugin should be ignored by a build - */ - @Suppress("FUNCTION_BOOLEAN_PREFIX") - override fun getIgnoreFailures(): Boolean = ignoreFailuresProp.getOrElse(false) - - @Suppress("AVOID_NULL_CHECKS") - private fun reporterFlag(diktatExtension: DiktatExtension): String = buildString { - val reporterFlag = project.createReporterFlag(diktatExtension) - append(reporterFlag) - if (isSarifReporterActive(reporterFlag)) { - // need to set user.home specially for ktlint, so it will be able to put a relative path URI in SARIF - systemProperty("user.home", project.rootDir.toString()) - } - - val outputFile = project.getOutputFile(diktatExtension) - if (outputFile != null) { - outputs.file(outputFile) - val outFlag = ",output=$outputFile" - append(outFlag) - } - } - - @Suppress("MagicNumber") - private fun isMainClassPropertySupported(gradleVersionString: String) = - GradleVersion.version(gradleVersionString) >= GradleVersion.version("6.4") - - private fun MutableList.addInput(file: File) { - add(file.toRelativeString(project.projectDir)) - } - - private fun resolveConfigFile(file: File): String { - if (file.toPath().startsWith(project.rootDir.toPath())) { - // In gradle, project.files() returns File relative to project.projectDir. - // There is no need to resolve file further if it has been passed via gradle files API. - return file.absolutePath - } - - // otherwise, e.g. if file is passed as java.io.File with relative path, we try to find it - return generateSequence(project.projectDir) { it.parentFile } - .map { it.resolve(file) } - .run { - firstOrNull { it.exists() } ?: first() - } - .absolutePath - } - - private fun fixForNewJpms() { - val javaVersion = getJavaExecJvmVersion() - project.logger.debug("For diktat execution jvm version $javaVersion will be used") - if (javaVersion.majorVersion.toInt() >= MIN_JVM_REQUIRES_ADD_OPENS) { - // https://github.com/saveourtool/diktat/issues/1182#issuecomment-1023099713 - project.logger.debug("Adding `--add-opens` flag for JVM version >=$MIN_JVM_REQUIRES_ADD_OPENS compatibility") - jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") - } - } - - private fun getJavaExecJvmVersion(): JavaVersion = if (GradleVersion.version(gradleVersionString) >= GradleVersion.version("6.7") && - javaLauncher.isPresent - ) { - // Java Launchers are available since 6.7, but may not always be configured - javaLauncher.map { it.metadata.jvmVersion }.map(JavaVersion::toVersion).get() - } else { - // `javaVersion` property is available since 5.2 and is simply derived from path to JVM executable, - // which may be explicitly set or be the same as current Gradle JVM. - javaVersion - } -} - -/** - * @param diktatExtension [DiktatExtension] with some values for task configuration - * @param diktatConfiguration dependencies of diktat run - * @param patternSet [PatternSet] to discover files for diktat check - * @return a [TaskProvider] - */ -fun Project.registerDiktatCheckTask(diktatExtension: DiktatExtension, - diktatConfiguration: Configuration, - patternSet: PatternSet -): TaskProvider = - tasks.register( - DIKTAT_CHECK_TASK, DiktatJavaExecTaskBase::class.java, gradle.gradleVersion, - diktatExtension, diktatConfiguration, patternSet - ) +import org.gradle.api.Task /** - * @param diktatExtension [DiktatExtension] with some values for task configuration - * @param diktatConfiguration dependencies of diktat run - * @param patternSet [PatternSet] to discover files for diktat fix - * @return a [TaskProvider] + * An interface with old name for base class for backward compatibility in plugin configuration */ -fun Project.registerDiktatFixTask(diktatExtension: DiktatExtension, - diktatConfiguration: Configuration, - patternSet: PatternSet -): TaskProvider = - tasks.register( - DIKTAT_FIX_TASK, DiktatJavaExecTaskBase::class.java, gradle.gradleVersion, - diktatExtension, diktatConfiguration, patternSet, listOf("-F ") - ) +@Deprecated("will be removed in 2.x") +interface DiktatJavaExecTaskBase : Task diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/Utils.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/Utils.kt index 9181043b48..ab84448c43 100644 --- a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/Utils.kt +++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/Utils.kt @@ -47,28 +47,28 @@ fun Any.closureOf(action: T.() -> Unit): Closure = * @param diktatExtension extension of type [DiktatExtension] * @return CLI flag as string */ -fun Project.createReporterFlag(diktatExtension: DiktatExtension): String { +fun Project.getReporterType(diktatExtension: DiktatExtension): String { val name = diktatExtension.reporter.trim() val validReporters = listOf("sarif", "plain", "json", "html") - val reporterFlag = when { + val reporterType = when { diktatExtension.githubActions -> { if (diktatExtension.reporter.isNotEmpty()) { logger.warn("`diktat.githubActions` is set to true, so custom reporter [$name] will be ignored and SARIF reporter will be used") } - "--reporter=sarif" + "sarif" } name.isEmpty() -> { logger.info("Reporter name was not set. Using 'plain' reporter") - "--reporter=plain" + "plain" } name !in validReporters -> { logger.warn("Reporter name is invalid (provided value: [$name]). Falling back to 'plain' reporter") - "--reporter=plain" + "plain" } - else -> "--reporter=$name" + else -> name } - return reporterFlag + return reporterType } /** diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/DiktatCheckTask.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/DiktatCheckTask.kt new file mode 100644 index 0000000000..9e33195639 --- /dev/null +++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/DiktatCheckTask.kt @@ -0,0 +1,38 @@ +package org.cqfn.diktat.plugin.gradle.tasks + +import org.cqfn.diktat.DiktatProcessCommand +import org.cqfn.diktat.plugin.gradle.DiktatExtension +import org.cqfn.diktat.plugin.gradle.DiktatGradlePlugin +import org.gradle.api.Project +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.util.PatternFilterable +import org.gradle.api.tasks.util.PatternSet +import javax.inject.Inject + +/** + * A task to check source code by diktat + */ +abstract class DiktatCheckTask @Inject constructor( + extension: DiktatExtension, + inputs: PatternFilterable +) : DiktatTaskBase(extension, inputs) { + override fun doRun(diktatCommand: DiktatProcessCommand, formattedContentConsumer: (String) -> Unit) { + diktatCommand.check() + } + + companion object { + /** + * @param diktatExtension [DiktatExtension] with some values for task configuration + * @param patternSet [PatternSet] to discover files for diktat check + * @return a [TaskProvider] + */ + fun Project.registerDiktatCheckTask( + diktatExtension: DiktatExtension, + patternSet: PatternSet + ): TaskProvider = + tasks.register( + DiktatGradlePlugin.DIKTAT_CHECK_TASK, DiktatCheckTask::class.java, + diktatExtension, patternSet + ) + } +} diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/DiktatFixTask.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/DiktatFixTask.kt new file mode 100644 index 0000000000..c705fb22fe --- /dev/null +++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/DiktatFixTask.kt @@ -0,0 +1,39 @@ +package org.cqfn.diktat.plugin.gradle.tasks + +import org.cqfn.diktat.DiktatProcessCommand +import org.cqfn.diktat.plugin.gradle.DiktatExtension +import org.cqfn.diktat.plugin.gradle.DiktatGradlePlugin +import org.gradle.api.Project +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.util.PatternFilterable +import org.gradle.api.tasks.util.PatternSet +import javax.inject.Inject + +/** + * A task to check source code by diktat + */ +abstract class DiktatFixTask @Inject constructor( + extension: DiktatExtension, + inputs: PatternFilterable +) : DiktatTaskBase(extension, inputs) { + override fun doRun(diktatCommand: DiktatProcessCommand, formattedContentConsumer: (String) -> Unit) { + val formattedText = diktatCommand.fix() + formattedContentConsumer(formattedText) + } + + companion object { + /** + * @param diktatExtension [DiktatExtension] with some values for task configuration + * @param patternSet [PatternSet] to discover files for diktat fix + * @return a [TaskProvider] + */ + fun Project.registerDiktatFixTask( + diktatExtension: DiktatExtension, + patternSet: PatternSet + ): TaskProvider = + tasks.register( + DiktatGradlePlugin.DIKTAT_FIX_TASK, DiktatFixTask::class.java, + diktatExtension, patternSet + ) + } +} diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/DiktatTaskBase.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/DiktatTaskBase.kt new file mode 100644 index 0000000000..2a1328bcd7 --- /dev/null +++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/DiktatTaskBase.kt @@ -0,0 +1,232 @@ +@file:Suppress( + "Deprecation" +) + +package org.cqfn.diktat.plugin.gradle.tasks + +import org.cqfn.diktat.DiktatProcessCommand +import org.cqfn.diktat.DiktatProcessor +import org.cqfn.diktat.api.DiktatCallback +import org.cqfn.diktat.api.DiktatLogLevel +import org.cqfn.diktat.ktlint.LintErrorReporter +import org.cqfn.diktat.ktlint.unwrap +import org.cqfn.diktat.plugin.gradle.DiktatExtension +import org.cqfn.diktat.plugin.gradle.DiktatJavaExecTaskBase +import org.cqfn.diktat.plugin.gradle.getOutputFile +import org.cqfn.diktat.plugin.gradle.getReporterType +import org.cqfn.diktat.plugin.gradle.isSarifReporterActive + +import com.pinterest.ktlint.core.Reporter +import com.pinterest.ktlint.core.internal.CurrentBaseline +import com.pinterest.ktlint.core.internal.containsLintError +import com.pinterest.ktlint.core.internal.loadBaseline +import com.pinterest.ktlint.reporter.baseline.BaselineReporter +import com.pinterest.ktlint.reporter.html.HtmlReporter +import com.pinterest.ktlint.reporter.json.JsonReporter +import com.pinterest.ktlint.reporter.plain.PlainReporter +import com.pinterest.ktlint.reporter.sarif.SarifReporter +import generated.DIKTAT_VERSION +import generated.KTLINT_VERSION +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.IgnoreEmptyDirectories +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.VerificationTask +import org.gradle.api.tasks.util.PatternFilterable + +import java.io.File +import java.io.FileOutputStream +import java.io.PrintStream + +/** + * A base task to run `diktat` + * @property extension + */ +@Suppress("WRONG_NEWLINES") +abstract class DiktatTaskBase( + @get:Internal internal val extension: DiktatExtension, + private val inputs: PatternFilterable +) : DefaultTask(), VerificationTask, DiktatJavaExecTaskBase { + /** + * Files that will be analyzed by diktat + */ + @get:IgnoreEmptyDirectories + @get:SkipWhenEmpty + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputFiles + val actualInputs: FileCollection by lazy { + if (inputs.includes.isEmpty() && inputs.excludes.isEmpty()) { + inputs.include("src/**/*.kt") + } + project.objects.fileCollection().from( + project.fileTree("${project.projectDir}").apply { + exclude("${project.buildDir}") + } + .matching(inputs) + ) + } + + /** + * Whether diktat should be executed + */ + @get:Internal + internal val shouldRun: Boolean by lazy { + !actualInputs.isEmpty + } + + /** + * [DiktatProcessor] created from [extension] + */ + @get:Internal + internal val diktatProcessor: DiktatProcessor by lazy { + DiktatProcessor.builder() + .diktatRuleSetProvider(extension.diktatConfigFile.toPath()) + .logLevel( + if (extension.debug) { + DiktatLogLevel.DEBUG + } else { + DiktatLogLevel.INFO + } + ) + .build() + } + + /** + * A baseline loaded from provided file or empty + */ + @get:Internal + internal val baseline: CurrentBaseline by lazy { + extension.baseline?.let { loadBaseline(it) } + ?: CurrentBaseline(emptyMap(), false) + } + + /** + * A reporter created based on configuration + */ + @get:Internal + internal val reporter: Reporter by lazy { + resolveReporter(baseline) + } + + init { + ignoreFailures = extension.ignoreFailures + } + + /** + * Function to execute diKTat + * + * @throws GradleException + */ + @TaskAction + fun run() { + if (extension.debug) { + project.logger.lifecycle("Running diktat $DIKTAT_VERSION with ktlint $KTLINT_VERSION") + } + if (!shouldRun) { + /* + If ktlint receives empty patterns, it implicitly uses **/*.kt, **/*.kts instead. + This can lead to diktat analyzing gradle buildscripts and so on. We want to prevent it. + */ + project.logger.warn("Inputs for $name do not exist, will not run diktat") + project.logger.info("Skipping diktat execution") + } else { + reporter.beforeAll() + val lintErrorReporter = LintErrorReporter() + actualInputs.files + .also { files -> + project.logger.info("Analyzing ${files.size} files with diktat in project ${project.name}") + project.logger.debug("Analyzing $files") + } + .forEach { file -> + processFile( + file = file, + diktatProcessor = diktatProcessor, + reporter = Reporter.from(reporter, lintErrorReporter) + ) + } + reporter.afterAll() + if (lintErrorReporter.isNotEmpty() && !ignoreFailures) { + throw GradleException("There are ${lintErrorReporter.errorCount()} lint errors") + } + } + } + + private fun processFile( + file: File, + diktatProcessor: DiktatProcessor, + reporter: Reporter + ) { + project.logger.debug("Checking file $file") + reporter.before(file.absolutePath) + val baselineErrors = baseline.baselineRules?.get( + file.relativeTo(project.projectDir).invariantSeparatorsPath + ) ?: emptyList() + val diktatCallback = DiktatCallback { error, isCorrected -> + val ktLintError = error.unwrap() + if (!baselineErrors.containsLintError(ktLintError)) { + reporter.onLintError(file.absolutePath, ktLintError, isCorrected) + } + } + val command = DiktatProcessCommand.builder() + .processor(diktatProcessor) + .file(file.toPath()) + .callback(diktatCallback) + .build() + doRun(command) { formattedText -> + val fileName = file.absolutePath + val fileContent = file.readText(Charsets.UTF_8) + if (fileContent != formattedText) { + project.logger.info("Original and formatted content differ, writing to $fileName...") + file.writeText(formattedText, Charsets.UTF_8) + } + } + reporter.after(file.absolutePath) + } + + /** + * An abstract method which should be overridden by fix and check tasks + * + * @param diktatCommand + * @param formattedContentConsumer + */ + protected abstract fun doRun(diktatCommand: DiktatProcessCommand, formattedContentConsumer: (String) -> Unit) + + private fun resolveReporter(baselineResults: CurrentBaseline): Reporter { + val reporterType = project.getReporterType(extension) + if (isSarifReporterActive(reporterType)) { + // need to set user.home specially for ktlint, so it will be able to put a relative path URI in SARIF + System.setProperty("user.home", project.rootDir.toString()) + } + val output = project.getOutputFile(extension)?.outputStream()?.let { PrintStream(it) } ?: System.`out` + val actualReporter = if (extension.githubActions) { + SarifReporter(output) + } else { + when (reporterType) { + "sarif" -> SarifReporter(output) + "plain" -> PlainReporter(output) + "json" -> JsonReporter(output) + "html" -> HtmlReporter(output) + else -> { + project.logger.warn("Reporter name $reporterType was not specified or is invalid. Falling to 'plain' reporter") + PlainReporter(output) + } + } + } + + return if (baselineResults.baselineGenerationNeeded) { + val baseline = requireNotNull(extension.baseline) { + "baseline is not provided, but baselineGenerationNeeded is true" + } + val baselineReporter = BaselineReporter(PrintStream(FileOutputStream(baseline, true))) + return Reporter.from(actualReporter, baselineReporter) + } else { + actualReporter + } + } +} diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/SarifReportMergeTask.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/SarifReportMergeTask.kt index 36718bd318..eafaf09f84 100644 --- a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/SarifReportMergeTask.kt +++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/SarifReportMergeTask.kt @@ -2,10 +2,10 @@ package org.cqfn.diktat.plugin.gradle.tasks import org.cqfn.diktat.plugin.gradle.DiktatExtension import org.cqfn.diktat.plugin.gradle.DiktatGradlePlugin.Companion.MERGE_SARIF_REPORTS_TASK_NAME -import org.cqfn.diktat.plugin.gradle.DiktatJavaExecTaskBase -import org.cqfn.diktat.plugin.gradle.createReporterFlag import org.cqfn.diktat.plugin.gradle.getOutputFile +import org.cqfn.diktat.plugin.gradle.getReporterType import org.cqfn.diktat.plugin.gradle.isSarifReporterActive + import io.github.detekt.sarif4k.SarifSchema210 import org.gradle.api.DefaultTask import org.gradle.api.Project @@ -18,6 +18,7 @@ import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskExecutionException import org.gradle.api.tasks.VerificationTask + import kotlinx.serialization.SerializationException import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString @@ -91,12 +92,12 @@ internal fun Project.configureMergeReportsTask(diktatExtension: DiktatExtension) } } rootProject.tasks.withType(SarifReportMergeTask::class.java).configureEach { reportMergeTask -> - if (isSarifReporterActive(createReporterFlag(diktatExtension))) { + if (isSarifReporterActive(getReporterType(diktatExtension))) { getOutputFile(diktatExtension)?.let { reportMergeTask.input.from(it) } - reportMergeTask.shouldRunAfter(tasks.withType(DiktatJavaExecTaskBase::class.java)) + reportMergeTask.shouldRunAfter(tasks.withType(DiktatTaskBase::class.java)) } } - tasks.withType(DiktatJavaExecTaskBase::class.java).configureEach { diktatJavaExecTaskBase -> + tasks.withType(DiktatTaskBase::class.java).configureEach { diktatJavaExecTaskBase -> diktatJavaExecTaskBase.finalizedBy(rootProject.tasks.withType(SarifReportMergeTask::class.java)) } } diff --git a/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePluginTest.kt b/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePluginTest.kt index 376111138d..08b92ebf7b 100644 --- a/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePluginTest.kt +++ b/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePluginTest.kt @@ -1,5 +1,6 @@ package org.cqfn.diktat.plugin.gradle +import org.cqfn.diktat.plugin.gradle.tasks.DiktatCheckTask import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.junit.jupiter.api.Assertions @@ -29,7 +30,7 @@ class DiktatGradlePluginTest { fun `check default extension properties`() { val diktatExtension = project.extensions.getByName("diktat") as DiktatExtension val actualInputs = project.tasks - .named("diktatCheck", DiktatJavaExecTaskBase::class.java) + .named("diktatCheck", DiktatCheckTask::class.java) .get() .actualInputs Assertions.assertFalse(diktatExtension.debug) @@ -42,7 +43,7 @@ class DiktatGradlePluginTest { val diktatExtension = project.extensions.getByName("diktat") as DiktatExtension Assertions.assertEquals("", diktatExtension.reporter) - val reporterFlag = project.createReporterFlag(diktatExtension) - Assertions.assertEquals("--reporter=plain", reporterFlag) + val reporterFlag = project.getReporterType(diktatExtension) + Assertions.assertEquals("plain", reporterFlag) } } diff --git a/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskTest.kt b/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskTest.kt index 00a4f08bbf..a363adff6e 100644 --- a/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskTest.kt +++ b/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskTest.kt @@ -1,7 +1,9 @@ package org.cqfn.diktat.plugin.gradle -import org.cqfn.diktat.common.config.rules.DIKTAT_CONF_PROPERTY -import org.cqfn.diktat.common.ktlint.ktlintDisabledRulesArgument +import org.cqfn.diktat.plugin.gradle.tasks.DiktatCheckTask +import com.pinterest.ktlint.reporter.json.JsonReporter +import com.pinterest.ktlint.reporter.plain.PlainReporter +import com.pinterest.ktlint.reporter.sarif.SarifReporter import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder @@ -33,28 +35,22 @@ class DiktatJavaExecTaskTest { @Test fun `check command line for various inputs`() { - assertCommandLineEquals( + assertFiles( listOf( - null, - ktlintDisabledRulesArgument, - combinePathParts("src", "main", "kotlin", "Test.kt"), - "--reporter=plain" + combinePathParts("src", "main", "kotlin", "Test.kt") ) ) { inputs { include("src/**/*.kt") } } + + val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + assert(task.reporter is PlainReporter) } @Test fun `check command line in debug mode`() { - assertCommandLineEquals( - listOf( - null, - ktlintDisabledRulesArgument, - "--debug", - combinePathParts("src", "main", "kotlin", "Test.kt"), - "--reporter=plain" - ) + assertFiles( + listOf(combinePathParts("src", "main", "kotlin", "Test.kt")) ) { inputs { include("src/**/*.kt") } debug = true @@ -65,19 +61,17 @@ class DiktatJavaExecTaskTest { fun `check command line with excludes`() { project.file("src/main/kotlin/generated").mkdirs() project.file("src/main/kotlin/generated/Generated.kt").createNewFile() - assertCommandLineEquals( - listOf( - null, - ktlintDisabledRulesArgument, - combinePathParts("src", "main", "kotlin", "Test.kt"), - "--reporter=plain" - ) + assertFiles( + listOf(combinePathParts("src", "main", "kotlin", "Test.kt")) ) { inputs { include("src/**/*.kt") exclude("src/main/kotlin/generated") } } + + val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + assert(task.reporter is PlainReporter) } @Test @@ -93,7 +87,7 @@ class DiktatJavaExecTaskTest { val task = project.registerDiktatTask { inputs { exclude("*") } } - Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml").absolutePath, task.systemProperties[DIKTAT_CONF_PROPERTY]) + Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml"), task.extension.diktatConfigFile) } @Test @@ -102,96 +96,74 @@ class DiktatJavaExecTaskTest { inputs { exclude("*") } diktatConfigFile = project.file("../diktat-analysis.yml") } - Assertions.assertEquals(File(project.projectDir.parentFile, "diktat-analysis.yml").absolutePath, task.systemProperties[DIKTAT_CONF_PROPERTY]) + Assertions.assertEquals(File(project.projectDir.parentFile, "diktat-analysis.yml"), task.extension.diktatConfigFile) } @Test fun `check command line has reporter type and output`() { - assertCommandLineEquals( - listOf( - null, - ktlintDisabledRulesArgument, - "--reporter=json,output=${project.projectDir.resolve("some.txt")}" - ) - ) { + assertFiles(emptyList()) { inputs { exclude("*") } diktatConfigFile = project.file("../diktat-analysis.yml") reporter = "json" output = "some.txt" } + val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + assert(task.reporter is JsonReporter) } @Test fun `check command line has reporter type without output`() { - assertCommandLineEquals( - listOf( - null, - ktlintDisabledRulesArgument, - "--reporter=json" - ) - ) { + assertFiles(emptyList()) { inputs { exclude("*") } diktatConfigFile = project.file("../diktat-analysis.yml") reporter = "json" } + val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + assert(task.reporter is JsonReporter) } @Test fun `check command line in githubActions mode`() { val path = project.file("${project.buildDir}/reports/diktat/diktat.sarif") - assertCommandLineEquals( - listOf( - null, - ktlintDisabledRulesArgument, - "--reporter=sarif,output=$path" - ) - ) { + assertFiles(emptyList()) { inputs { exclude("*") } diktatConfigFile = project.file("../diktat-analysis.yml") githubActions = true } - val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatJavaExecTaskBase + val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + assert(task.reporter is SarifReporter) Assertions.assertEquals( project.rootDir.toString(), - task.systemProperties["user.home"] + System.getProperty("user.home") ) } @Test fun `githubActions mode should have higher precedence over explicit reporter`() { val path = project.file("${project.buildDir}/reports/diktat/diktat.sarif") - assertCommandLineEquals( - listOf( - null, - ktlintDisabledRulesArgument, - "--reporter=sarif,output=$path" - ) - ) { + assertFiles(emptyList()) { inputs { exclude("*") } diktatConfigFile = project.file("../diktat-analysis.yml") githubActions = true reporter = "json" output = "report.json" } + val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + assert(task.reporter is SarifReporter) } @Test fun `should set system property with SARIF reporter`() { - assertCommandLineEquals( - listOf( - null, - ktlintDisabledRulesArgument, - "--reporter=sarif" - ) - ) { + assertFiles(emptyList()) { inputs { exclude("*") } diktatConfigFile = project.file("../diktat-analysis.yml") reporter = "sarif" } - val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatJavaExecTaskBase + val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + assert(task.reporter is SarifReporter) Assertions.assertEquals( project.rootDir.toString(), - task.systemProperties["user.home"] + System.getProperty("user.home") ) } @@ -204,10 +176,10 @@ class DiktatJavaExecTaskTest { inputs { exclude("*") } } } - val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatJavaExecTaskBase - val subprojectTask = subproject.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatJavaExecTaskBase - Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml").absolutePath, task.systemProperties[DIKTAT_CONF_PROPERTY]) - Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml").absolutePath, subprojectTask.systemProperties[DIKTAT_CONF_PROPERTY]) + val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + val subprojectTask = subproject.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml"), task.extension.diktatConfigFile) + Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml"), subprojectTask.extension.diktatConfigFile) } @Test @@ -220,10 +192,10 @@ class DiktatJavaExecTaskTest { diktatConfigFile = it.rootProject.file("diktat-analysis.yml") } } - val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatJavaExecTaskBase - val subprojectTask = subproject.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatJavaExecTaskBase - Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml").absolutePath, task.systemProperties[DIKTAT_CONF_PROPERTY]) - Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml").absolutePath, subprojectTask.systemProperties[DIKTAT_CONF_PROPERTY]) + val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + val subprojectTask = subproject.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml"), task.extension.diktatConfigFile) + Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml"), subprojectTask.extension.diktatConfigFile) } @Test @@ -237,21 +209,29 @@ class DiktatJavaExecTaskTest { diktatConfigFile = it.file("diktat-analysis.yml") } } - val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatJavaExecTaskBase - val subprojectTask = subproject.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatJavaExecTaskBase - Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml").absolutePath, task.systemProperties[DIKTAT_CONF_PROPERTY]) - Assertions.assertEquals(File(subproject.projectDir, "diktat-analysis.yml").absolutePath, subprojectTask.systemProperties[DIKTAT_CONF_PROPERTY]) + val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + val subprojectTask = subproject.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask + Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml"), task.extension.diktatConfigFile) + Assertions.assertEquals(File(subproject.projectDir, "diktat-analysis.yml"), subprojectTask.extension.diktatConfigFile) } - private fun Project.registerDiktatTask(extensionConfiguration: DiktatExtension.() -> Unit): DiktatJavaExecTaskBase { + private fun Project.registerDiktatTask(extensionConfiguration: DiktatExtension.() -> Unit): DiktatCheckTask { pluginManager.apply(DiktatGradlePlugin::class.java) extensions.configure("diktat", extensionConfiguration) - return tasks.getByName(DIKTAT_CHECK_TASK) as DiktatJavaExecTaskBase + return tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask } - private fun assertCommandLineEquals(expected: List, extensionConfiguration: DiktatExtension.() -> Unit) { + private fun assertFiles(expected: List, extensionConfiguration: DiktatExtension.() -> Unit) { val task = project.registerDiktatTask(extensionConfiguration) - Assertions.assertIterableEquals(expected, task.commandLine) + val files = task.actualInputs.files + Assertions.assertEquals(expected.size, files.size) + Assertions.assertTrue { + expected.zip(files) + .map { (expectedStr, file) -> + file.path.endsWith(expectedStr) + } + .all { it } + } } private fun setupMultiProject() { diff --git a/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/ReporterSelectionTest.kt b/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/ReporterSelectionTest.kt index 472480f89f..b0f65c0ec0 100644 --- a/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/ReporterSelectionTest.kt +++ b/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/ReporterSelectionTest.kt @@ -27,8 +27,8 @@ class ReporterSelectionTest { } Assertions.assertEquals( - "--reporter=plain", - project.createReporterFlag(diktatExtension) + "plain", + project.getReporterType(diktatExtension) ) } } diff --git a/diktat-maven-plugin/build.gradle.kts b/diktat-maven-plugin/build.gradle.kts index ba822027cb..1e0034d8ef 100644 --- a/diktat-maven-plugin/build.gradle.kts +++ b/diktat-maven-plugin/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(libs.kotlin.stdlib.jdk8) implementation(projects.diktatRules) + implementation(projects.diktatRunner.diktatRunnerKtlintEngine) implementation(libs.ktlint.core) implementation(libs.ktlint.reporter.plain) implementation(libs.ktlint.reporter.sarif) diff --git a/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatBaseMojo.kt b/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatBaseMojo.kt index 5188811923..7eedfa4cce 100644 --- a/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatBaseMojo.kt +++ b/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatBaseMojo.kt @@ -4,15 +4,16 @@ package org.cqfn.diktat.plugin.maven -import org.cqfn.diktat.ktlint.KtLintRuleSetProviderWrapper.Companion.toKtLint -import org.cqfn.diktat.ruleset.rules.DiktatRuleSetProvider +import org.cqfn.diktat.DiktatProcessCommand +import org.cqfn.diktat.DiktatProcessor +import org.cqfn.diktat.api.DiktatLogLevel +import org.cqfn.diktat.ktlint.LintErrorReporter +import org.cqfn.diktat.ktlint.unwrap import org.cqfn.diktat.ruleset.utils.isKotlinCodeOrScript -import com.pinterest.ktlint.core.KtLint import com.pinterest.ktlint.core.LintError import com.pinterest.ktlint.core.Reporter import com.pinterest.ktlint.core.RuleExecutionException -import com.pinterest.ktlint.core.RuleSet import com.pinterest.ktlint.core.internal.CurrentBaseline import com.pinterest.ktlint.core.internal.containsLintError import com.pinterest.ktlint.core.internal.loadBaseline @@ -32,6 +33,10 @@ import org.apache.maven.project.MavenProject import java.io.File import java.io.FileOutputStream import java.io.PrintStream +import java.nio.file.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.readText +import kotlin.io.path.writeText /** * Base [Mojo] for checking and fixing code using diktat @@ -69,7 +74,6 @@ abstract class DiktatBaseMojo : AbstractMojo() { */ @Parameter(property = "diktat.baseline") var baseline: File? = null - private lateinit var reporterImpl: Reporter /** * Path to diktat yml config file. Can be either absolute or relative to project's root directory. @@ -99,9 +103,10 @@ abstract class DiktatBaseMojo : AbstractMojo() { private lateinit var mavenSession: MavenSession /** - * @param params instance of [KtLint.ExperimentalParams] used in analysis + * @param command instance of [DiktatProcessCommand] used in analysis + * @param formattedContentConsumer consumer for formatted content of the file */ - abstract fun runAction(params: KtLint.ExperimentalParams) + abstract fun runAction(command: DiktatProcessCommand, formattedContentConsumer: (String) -> Unit) /** * Perform code check using diktat ruleset @@ -118,24 +123,29 @@ abstract class DiktatBaseMojo : AbstractMojo() { if (excludes.isNotEmpty()) " and excluding $excludes" else "" ) - val ruleSets by lazy { - listOf(DiktatRuleSetProvider(configFile).toKtLint().get()) + val diktatProcessor by lazy { + DiktatProcessor.builder() + .diktatRuleSetProvider(configFile) + .logLevel( + if (debug) DiktatLogLevel.DEBUG else DiktatLogLevel.INFO + ) + .build() } val baselineResults = baseline?.let { loadBaseline(it.absolutePath) } ?: CurrentBaseline(emptyMap(), false) - reporterImpl = resolveReporter(baselineResults) + val reporterImpl = resolveReporter(baselineResults) reporterImpl.beforeAll() - val lintErrors: MutableList = mutableListOf() + val lintErrorReporter = LintErrorReporter() inputs .map(::File) .forEach { - checkDirectory(it, lintErrors, baselineResults.baselineRules ?: emptyMap(), ruleSets) + diktatProcessor.checkDirectory(it, Reporter.from(reporterImpl, lintErrorReporter), baselineResults.baselineRules ?: emptyMap()) } reporterImpl.afterAll() - if (lintErrors.isNotEmpty()) { - throw MojoFailureException("There are ${lintErrors.size} lint errors") + if (lintErrorReporter.isNotEmpty()) { + throw MojoFailureException("There are ${lintErrorReporter.errorCount()} lint errors") } } @@ -199,11 +209,10 @@ abstract class DiktatBaseMojo : AbstractMojo() { * @throws MojoExecutionException if [RuleExecutionException] has been thrown by ktlint */ @Suppress("TYPE_ALIAS") - private fun checkDirectory( + private fun DiktatProcessor.checkDirectory( directory: File, - lintErrors: MutableList, + reporter: Reporter, baselineRules: Map>, - ruleSets: Iterable ) { val (excludedDirs, excludedFiles) = excludes.map(::File).partition { it.isDirectory } directory @@ -216,17 +225,16 @@ abstract class DiktatBaseMojo : AbstractMojo() { .forEach { file -> log.debug("Checking file $file") try { - reporterImpl.before(file.absolutePath) + reporter.before(file.absolutePath) checkFile( - file, - lintErrors, + file.toPath(), + reporter, baselineRules.getOrDefault( file.relativeTo(mavenProject.basedir.parentFile).invariantSeparatorsPath, emptyList() ), - ruleSets ) - reporterImpl.after(file.absolutePath) + reporter.after(file.absolutePath) } catch (e: RuleExecutionException) { log.error("Unhandled exception during rule execution: ", e) throw MojoExecutionException("Unhandled exception during rule execution", e) @@ -234,26 +242,28 @@ abstract class DiktatBaseMojo : AbstractMojo() { } } - private fun checkFile(file: File, - lintErrors: MutableList, - baselineErrors: List, - ruleSets: Iterable + private fun DiktatProcessor.checkFile( + file: Path, + reporter: Reporter, + baselineErrors: List, ) { - val text = file.readText() - val params = - KtLint.ExperimentalParams( - fileName = file.absolutePath, - text = text, - ruleSets = ruleSets, - script = file.extension.equals("kts", ignoreCase = true), - cb = { lintError, isCorrected -> - if (!baselineErrors.containsLintError(lintError)) { - reporterImpl.onLintError(file.absolutePath, lintError, isCorrected) - lintErrors.add(lintError) - } - }, - debug = debug - ) - runAction(params) + val command = DiktatProcessCommand.builder() + .processor(this) + .file(file) + .callback { error, isCorrected -> + val ktLintError = error.unwrap() + if (!baselineErrors.containsLintError(ktLintError)) { + reporter.onLintError(file.absolutePathString(), ktLintError, isCorrected) + } + } + .build() + runAction(command) { formattedText -> + val fileName = file.absolutePathString() + val fileContent = file.readText(Charsets.UTF_8) + if (fileContent != formattedText) { + log.info("Original and formatted content differ, writing to $fileName...") + file.writeText(formattedText, Charsets.UTF_8) + } + } } } diff --git a/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatMojo.kt b/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatMojo.kt index 1ff3f810c0..9934bf3350 100644 --- a/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatMojo.kt +++ b/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatMojo.kt @@ -4,25 +4,19 @@ package org.cqfn.diktat.plugin.maven +import org.cqfn.diktat.DiktatProcessCommand import org.cqfn.diktat.ruleset.rules.DiktatRuleSetProvider -import org.cqfn.diktat.ruleset.utils.ignoreCorrectedErrors -import com.pinterest.ktlint.core.KtLint import org.apache.maven.plugins.annotations.Mojo -import java.io.File - /** * Main [Mojo] that call [DiktatRuleSetProvider]'s rules on [inputs] files */ @Mojo(name = "check") @Suppress("unused") class DiktatCheckMojo : DiktatBaseMojo() { - /** - * @param params instance of [KtLint.ExperimentalParams] used in analysis - */ - override fun runAction(params: KtLint.ExperimentalParams) { - KtLint.lint(params) + override fun runAction(command: DiktatProcessCommand, formattedContentConsumer: (String) -> Unit) { + command.check() } } @@ -33,16 +27,8 @@ class DiktatCheckMojo : DiktatBaseMojo() { @Mojo(name = "fix") @Suppress("unused") class DiktatFixMojo : DiktatBaseMojo() { - /** - * @param params instance of [KtLint.Params] used in analysis - */ - override fun runAction(params: KtLint.ExperimentalParams) { - val fileName = params.fileName - val fileContent = File(fileName).readText(charset("UTF-8")) - val formattedText = KtLint.format(params.ignoreCorrectedErrors()) - if (fileContent != formattedText) { - log.info("Original and formatted content differ, writing to $fileName...") - File(fileName).writeText(formattedText, charset("UTF-8")) - } + override fun runAction(command: DiktatProcessCommand, formattedContentConsumer: (String) -> Unit) { + val formattedText = command.fix() + formattedContentConsumer(formattedText) } } diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt index cfe6ff4172..c241895a94 100644 --- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt @@ -103,7 +103,7 @@ import java.io.File * inspections and rules are stored. */ @Suppress("ForbiddenComment") -class DiktatRuleSetProvider(private var diktatConfigFile: String = DIKTAT_ANALYSIS_CONF) { +class DiktatRuleSetProvider(private val diktatConfigFile: String = DIKTAT_ANALYSIS_CONF) { private val possibleConfigs: Sequence = sequence { yield(resolveDefaultConfig()) yield(resolveConfigFileFromJarLocation()) @@ -114,7 +114,7 @@ class DiktatRuleSetProvider(private var diktatConfigFile: String = DIKTAT_ANALYS " (it can be placed to the run directory or the default file from resources will be used)") val configPath = possibleConfigs .firstOrNull { it != null && File(it).exists() } - diktatConfigFile = configPath + val resultedDiktatConfigFile = configPath ?: run { val possibleConfigsList = possibleConfigs.toList() log.warn( @@ -130,7 +130,7 @@ class DiktatRuleSetProvider(private var diktatConfigFile: String = DIKTAT_ANALYS } RulesConfigReader(javaClass.classLoader) - .readResource(diktatConfigFile) + .readResource(resultedDiktatConfigFile) ?.onEach(::validate) ?: emptyList() } diff --git a/diktat-ruleset/build.gradle.kts b/diktat-ruleset/build.gradle.kts index e5f043e33a..43fb22d3aa 100644 --- a/diktat-ruleset/build.gradle.kts +++ b/diktat-ruleset/build.gradle.kts @@ -1,3 +1,4 @@ +import org.cqfn.diktat.buildutils.configurePom import com.github.jengelman.gradle.plugins.shadow.ShadowExtension import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar @@ -48,14 +49,20 @@ tasks { } } -// it creates a publication for shadowJar publishing { publications { + // it creates a publication for shadowJar create("shadow") { // https://github.com/johnrengelman/shadow/issues/417#issuecomment-830668442 project.extensions.configure { component(this@create) } + this.artifactId = "diktat" + this.pom { + configurePom(project) + // need to override name + name.set("diktat") + } } } } diff --git a/diktat-runner/diktat-runner-api/build.gradle.kts b/diktat-runner/diktat-runner-api/build.gradle.kts new file mode 100644 index 0000000000..eaad652c9a --- /dev/null +++ b/diktat-runner/diktat-runner-api/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("org.cqfn.diktat.buildutils.kotlin-jvm-configuration") + id("org.cqfn.diktat.buildutils.code-quality-convention") + id("org.cqfn.diktat.buildutils.publishing-signing-default-configuration") +} + +project.description = "This module builds diktat-runner-api" + +dependencies { + implementation(projects.diktatRules) +} diff --git a/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/AbstractDiktatProcessCommand.kt b/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/AbstractDiktatProcessCommand.kt new file mode 100644 index 0000000000..98b6c05338 --- /dev/null +++ b/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/AbstractDiktatProcessCommand.kt @@ -0,0 +1,112 @@ +package org.cqfn.diktat + +import org.cqfn.diktat.api.DiktatCallback +import org.cqfn.diktat.ruleset.utils.isKotlinScript +import java.nio.file.Path +import kotlin.io.path.readText + +/** + * An abstract implementation for command to run `diktat` + * + * @property processor + * @property file + * @property fileContent + * @property isScript + * @property callback + */ +abstract class AbstractDiktatProcessCommand( + protected val processor: DiktatProcessor, + protected val file: Path, + protected val fileContent: String, + protected val isScript: Boolean, + protected val callback: DiktatCallback, +) { + /** + * Run `diktat fix` using parameters from current command + * + * @return result of `diktat fix` + */ + abstract fun fix(): String + + /** + * Run `diktat check` using parameters from current command + */ + abstract fun check() + + /** + * Builder for [AbstractDiktatProcessCommand] + */ + abstract class Builder { + private var processor: DiktatProcessor? = null + private var file: Path? = null + private var fileContent: String? = null + private var isScript: Boolean? = null + private var callback: DiktatCallback? = null + + /** + * @param processor + * @return updated builder + */ + fun processor(processor: DiktatProcessor) = apply { this.processor = processor } + + /** + * @param file + * @return updated builder + */ + fun file(file: Path) = apply { this.file = file } + + /** + * @param fileContent + * @return updated builder + */ + fun fileContent(fileContent: String) = apply { this.fileContent = fileContent } + + /** + * @param isScript + * @return updated builder + */ + fun isScript(isScript: Boolean) = apply { this.isScript = isScript } + + /** + * @param callback + * @return updated builder + */ + fun callback(callback: DiktatCallback) = apply { this.callback = callback } + + /** + * @return built [C] + */ + fun build(): C { + val resolvedFile = requireNotNull(file) { + "file is required" + } + return doBuild( + processor = requireNotNull(processor) { + "processor is required" + }, + file = resolvedFile, + fileContent = fileContent ?: resolvedFile.readText(Charsets.UTF_8), + isScript = isScript ?: resolvedFile.isKotlinScript(), + callback = requireNotNull(callback) { + "callback is required" + }, + ) + } + + /** + * @param processor + * @param file + * @param fileContent + * @param isScript + * @param callback + * @return [C] is built using values from builder + */ + abstract fun doBuild( + processor: DiktatProcessor, + file: Path, + fileContent: String, + isScript: Boolean, + callback: DiktatCallback, + ): C + } +} diff --git a/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/DiktatProcessor.kt b/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/DiktatProcessor.kt new file mode 100644 index 0000000000..69475f996f --- /dev/null +++ b/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/DiktatProcessor.kt @@ -0,0 +1,68 @@ +package org.cqfn.diktat + +import org.cqfn.diktat.api.DiktatLogLevel +import org.cqfn.diktat.ruleset.rules.DiktatRuleSetProvider +import java.nio.file.Path +import kotlin.io.path.absolutePathString + +/** + * Processor to run `diktat` + * + * @property diktatRuleSetProvider + * @property logLevel + */ +@Suppress("USE_DATA_CLASS") // to hide `copy` method +class DiktatProcessor private constructor( + val diktatRuleSetProvider: DiktatRuleSetProvider, + val logLevel: DiktatLogLevel, +) { + /** + * Builder for [DiktatProcessCommand] + * + * @property diktatRuleSetProvider + * @property logLevel + */ + class Builder internal constructor( + private var diktatRuleSetProvider: DiktatRuleSetProvider? = null, + private var logLevel: DiktatLogLevel = DiktatLogLevel.INFO, + ) { + /** + * @param diktatRuleSetProvider + * @return updated builder + */ + fun diktatRuleSetProvider(diktatRuleSetProvider: DiktatRuleSetProvider) = apply { this.diktatRuleSetProvider = diktatRuleSetProvider } + + /** + * @param configFile a config file to load [DiktatRuleSetProvider] + * @return updated builder + */ + fun diktatRuleSetProvider(configFile: String) = diktatRuleSetProvider(DiktatRuleSetProvider(configFile)) + + /** + * @param configFile a config file to load [DiktatRuleSetProvider] + * @return updated builder + */ + fun diktatRuleSetProvider(configFile: Path) = diktatRuleSetProvider(configFile.absolutePathString()) + + /** + * @param logLevel + * @return updated builder + */ + fun logLevel(logLevel: DiktatLogLevel) = apply { this.logLevel = logLevel } + + /** + * @return built [DiktatProcessCommand] + */ + fun build(): DiktatProcessor = DiktatProcessor( + diktatRuleSetProvider = diktatRuleSetProvider ?: DiktatRuleSetProvider(), + logLevel = logLevel, + ) + } + + companion object { + /** + * @return a builder for [DiktatProcessor] + */ + fun builder(): Builder = Builder() + } +} diff --git a/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/api/DiktatCallback.kt b/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/api/DiktatCallback.kt new file mode 100644 index 0000000000..de6ea06de7 --- /dev/null +++ b/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/api/DiktatCallback.kt @@ -0,0 +1,15 @@ +package org.cqfn.diktat.api + +/** + * Callback for diktat process + */ +@FunctionalInterface +fun interface DiktatCallback : Function2 { + /** + * Performs this callback on the given [error] taking into account [isCorrected] flag. + * + * @param error the error found by diktat + * @param isCorrected true if the error fixed by diktat + */ + override fun invoke(error: DiktatError, isCorrected: Boolean) +} diff --git a/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/api/DiktatError.kt b/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/api/DiktatError.kt new file mode 100644 index 0000000000..3d4aac4758 --- /dev/null +++ b/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/api/DiktatError.kt @@ -0,0 +1,31 @@ +package org.cqfn.diktat.api + +/** + * Error found by `diktat` + */ +interface DiktatError { + /** + * @return line number (one-based) + */ + fun getLine(): Int + + /** + * @return column number (one-based) + */ + fun getCol(): Int + + /** + * @return rule id + */ + fun getRuleId(): String + + /** + * @return error message + */ + fun getDetail(): String + + /** + * @return true if the found error can be fixed + */ + fun canBeAutoCorrected(): Boolean +} diff --git a/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/api/DiktatLogLevel.kt b/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/api/DiktatLogLevel.kt new file mode 100644 index 0000000000..6e6e18641d --- /dev/null +++ b/diktat-runner/diktat-runner-api/src/main/kotlin/org/cqfn/diktat/api/DiktatLogLevel.kt @@ -0,0 +1,10 @@ +package org.cqfn.diktat.api + +/** + * Log level of `diktat processing` + */ +enum class DiktatLogLevel { + DEBUG, + INFO, + ; +} diff --git a/diktat-runner/diktat-runner-ktlint-engine/build.gradle.kts b/diktat-runner/diktat-runner-ktlint-engine/build.gradle.kts new file mode 100644 index 0000000000..9e3534faa5 --- /dev/null +++ b/diktat-runner/diktat-runner-ktlint-engine/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("org.cqfn.diktat.buildutils.kotlin-jvm-configuration") + id("org.cqfn.diktat.buildutils.code-quality-convention") + id("org.cqfn.diktat.buildutils.publishing-signing-default-configuration") +} + +project.description = "This module builds diktat-runner implementation using ktlint" + +dependencies { + api(projects.diktatRunner.diktatRunnerApi) + implementation(projects.diktatRules) + implementation(libs.ktlint.core) +} diff --git a/diktat-runner/diktat-runner-ktlint-engine/src/main/kotlin/org/cqfn/diktat/DiktatProcessCommand.kt b/diktat-runner/diktat-runner-ktlint-engine/src/main/kotlin/org/cqfn/diktat/DiktatProcessCommand.kt new file mode 100644 index 0000000000..04a605af5d --- /dev/null +++ b/diktat-runner/diktat-runner-ktlint-engine/src/main/kotlin/org/cqfn/diktat/DiktatProcessCommand.kt @@ -0,0 +1,85 @@ +package org.cqfn.diktat + +import org.cqfn.diktat.api.DiktatCallback +import org.cqfn.diktat.api.DiktatLogLevel +import org.cqfn.diktat.ktlint.KtLintRuleSetProviderWrapper.Companion.toKtLint +import org.cqfn.diktat.ktlint.unwrap +import com.pinterest.ktlint.core.KtLint +import com.pinterest.ktlint.core.api.EditorConfigOverride +import java.nio.file.Path +import kotlin.io.path.absolutePathString + +/** + * Command to run `diktat` + * + * @property processor + * @property file + * @property fileContent + * @property isScript + * @property callback + */ +class DiktatProcessCommand private constructor( + processor: DiktatProcessor, + file: Path, + fileContent: String, + isScript: Boolean, + callback: DiktatCallback, +) : AbstractDiktatProcessCommand( + processor, + file, + fileContent, + isScript, + callback, +) { + /** + * Run `diktat fix` using parameters from current command + * + * @return result of `diktat fix` + */ + override fun fix(): String = KtLint.format(ktLintParams()) + + /** + * Run `diktat check` using parameters from current command + */ + override fun check(): Unit = KtLint.lint(ktLintParams()) + + @Suppress("DEPRECATION") + private fun ktLintParams(): KtLint.ExperimentalParams = KtLint.ExperimentalParams( + fileName = file.absolutePathString(), + text = fileContent, + ruleSets = setOf(processor.diktatRuleSetProvider.toKtLint().get()), + userData = emptyMap(), + cb = callback.unwrap(), + script = isScript, + editorConfigPath = null, + debug = processor.logLevel == DiktatLogLevel.DEBUG, + editorConfigOverride = EditorConfigOverride.emptyEditorConfigOverride, + isInvokedFromCli = false + ) + + /** + * Builder for [DiktatProcessCommand] + */ + class Builder internal constructor() : AbstractDiktatProcessCommand.Builder() { + override fun doBuild( + processor: DiktatProcessor, + file: Path, + fileContent: String, + isScript: Boolean, + callback: DiktatCallback, + ): DiktatProcessCommand = DiktatProcessCommand( + processor = processor, + file = file, + fileContent = fileContent, + isScript = isScript, + callback = callback, + ) + } + + companion object { + /** + * @return a builder for [DiktatProcessCommand] + */ + fun builder(): Builder = Builder() + } +} diff --git a/diktat-runner/diktat-runner-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/LintErrorCallbackWrapper.kt b/diktat-runner/diktat-runner-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/LintErrorCallbackWrapper.kt new file mode 100644 index 0000000000..b9352d2a57 --- /dev/null +++ b/diktat-runner/diktat-runner-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/LintErrorCallbackWrapper.kt @@ -0,0 +1,22 @@ +/** + * This file contains utility methods for LintErrorCallback + */ + +package org.cqfn.diktat.ktlint + +import org.cqfn.diktat.api.DiktatCallback +import org.cqfn.diktat.ruleset.utils.LintErrorCallback + +/** + * @return [DiktatCallback] from KtLint [LintErrorCallback] + */ +fun LintErrorCallback.wrap(): DiktatCallback = DiktatCallback { error, isCorrected -> + this(error.unwrap(), isCorrected) +} + +/** + * @return KtLint [LintErrorCallback] from [DiktatCallback] or exception + */ +fun DiktatCallback.unwrap(): LintErrorCallback = { error, isCorrected -> + this(error.wrap(), isCorrected) +} diff --git a/diktat-runner/diktat-runner-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/LintErrorReporter.kt b/diktat-runner/diktat-runner-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/LintErrorReporter.kt new file mode 100644 index 0000000000..2bd5e13828 --- /dev/null +++ b/diktat-runner/diktat-runner-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/LintErrorReporter.kt @@ -0,0 +1,30 @@ +package org.cqfn.diktat.ktlint + +import com.pinterest.ktlint.core.LintError +import com.pinterest.ktlint.core.Reporter +import java.util.concurrent.atomic.AtomicInteger + +/** + * An implementation of [Reporter] which counts [LintError]s + */ +class LintErrorReporter : Reporter { + private val errorCounter: AtomicInteger = AtomicInteger() + + override fun onLintError( + file: String, + err: LintError, + corrected: Boolean + ) { + errorCounter.incrementAndGet() + } + + /** + * @return true if there is a reported [LintError], otherwise -- false. + */ + fun isNotEmpty(): Boolean = errorCounter.get() != 0 + + /** + * @return count of reported [LintError] + */ + fun errorCount(): Int = errorCounter.get() +} diff --git a/diktat-runner/diktat-runner-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/LintErrorWrapper.kt b/diktat-runner/diktat-runner-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/LintErrorWrapper.kt new file mode 100644 index 0000000000..b293a1cee5 --- /dev/null +++ b/diktat-runner/diktat-runner-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/LintErrorWrapper.kt @@ -0,0 +1,34 @@ +package org.cqfn.diktat.ktlint + +import org.cqfn.diktat.api.DiktatError +import com.pinterest.ktlint.core.LintError + +/** + * Wrapper for KtLint error + * + * @property lintError + */ +data class LintErrorWrapper( + val lintError: LintError +) : DiktatError { + override fun getLine(): Int = lintError.line + + override fun getCol(): Int = lintError.col + + override fun getRuleId(): String = lintError.ruleId + + override fun getDetail(): String = lintError.detail + + override fun canBeAutoCorrected(): Boolean = lintError.canBeAutoCorrected +} + +/** + * @return [DiktatError] from KtLint [LintError] + */ +fun LintError.wrap(): DiktatError = LintErrorWrapper(this) + +/** + * @return KtLint [LintError] from [DiktatError] or exception + */ +fun DiktatError.unwrap(): LintError = (this as? LintErrorWrapper)?.lintError + ?: error("Unsupported wrapper of ${DiktatError::class.java.simpleName}: ${this::class.java.canonicalName}") diff --git a/gradle/plugins/src/main/kotlin/org/cqfn/diktat/buildutils/PublishingConfiguration.kt b/gradle/plugins/src/main/kotlin/org/cqfn/diktat/buildutils/PublishingConfiguration.kt index 9577cc28c2..8f92f6024f 100644 --- a/gradle/plugins/src/main/kotlin/org/cqfn/diktat/buildutils/PublishingConfiguration.kt +++ b/gradle/plugins/src/main/kotlin/org/cqfn/diktat/buildutils/PublishingConfiguration.kt @@ -13,6 +13,7 @@ import io.github.gradlenexus.publishplugin.NexusPublishExtension import org.gradle.api.Named import org.gradle.api.Project import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPom import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.tasks.bundling.Jar import org.gradle.internal.logging.text.StyledTextOutput @@ -68,8 +69,11 @@ fun Project.configureSigning() { } } +/** + * Updates pom.xml for publication + */ @Suppress("TOO_LONG_FUNCTION") -internal fun Project.configurePublications() { +fun Project.configurePublications() { val dokkaJar: Jar = tasks.create("dokkaJar") { group = "documentation" archiveClassifier.set("javadoc") @@ -86,39 +90,48 @@ internal fun Project.configurePublications() { */ this.artifact(dokkaJar) this.pom { - name.set(project.name) - description.set(project.description ?: project.name) - url.set("https://www.cqfn.org/diKTat/") - licenses { - license { - name.set("MIT License") - url.set("http://www.opensource.org/licenses/mit-license.php") - } - } - developers { - developer { - id.set("akuleshov7") - name.set("Andrey Kuleshov") - email.set("andrewkuleshov7@gmail.com") - url.set("https://github.com/akuleshov7") - } - developer { - id.set("petertrr") - name.set("Peter Trifanov") - email.set("peter.trifanov@gmail.com") - url.set("https://github.com/petertrr") - } - } - scm { - connection.set("scm:git:git://github.com/saveourtool/diktat.git") - developerConnection.set("scm:git:ssh://github.com:saveourtool/diktat.git") - url.set("http://github.com/saveourtool/diktat/tree/master") - } + configurePom(project) } } } } +/** + * Configures _pom.xml_ + * + * @param project + */ +fun MavenPom.configurePom(project: Project) { + name.set(project.name) + description.set(project.description ?: project.name) + url.set("https://www.cqfn.org/diKTat/") + licenses { + license { + name.set("MIT License") + url.set("http://www.opensource.org/licenses/mit-license.php") + } + } + developers { + developer { + id.set("akuleshov7") + name.set("Andrey Kuleshov") + email.set("andrewkuleshov7@gmail.com") + url.set("https://github.com/akuleshov7") + } + developer { + id.set("petertrr") + name.set("Peter Trifanov") + email.set("peter.trifanov@gmail.com") + url.set("https://github.com/petertrr") + } + } + scm { + connection.set("scm:git:git://github.com/saveourtool/diktat.git") + developerConnection.set("scm:git:ssh://github.com:saveourtool/diktat.git") + url.set("http://github.com/saveourtool/diktat/tree/master") + } +} + internal fun Project.configureNexusPublishing() { configure { repositories { diff --git a/gradle/plugins/src/main/kotlin/org/cqfn/diktat/buildutils/diktat-convention-configuration.gradle.kts b/gradle/plugins/src/main/kotlin/org/cqfn/diktat/buildutils/diktat-convention-configuration.gradle.kts index 49bf317b0e..351442a41b 100644 --- a/gradle/plugins/src/main/kotlin/org/cqfn/diktat/buildutils/diktat-convention-configuration.gradle.kts +++ b/gradle/plugins/src/main/kotlin/org/cqfn/diktat/buildutils/diktat-convention-configuration.gradle.kts @@ -27,8 +27,11 @@ diktat { } tasks.withType().configureEach { - javaLauncher.set(project.extensions.getByType().launcherFor { - // a temporary workaround -- diktat-gradle-plugin doesn't detect java version of `javaLauncher` - languageVersion.set(JavaLanguageVersion.of(11)) - }) + val project = this@configureEach.project + if (this is JavaExec) { + javaLauncher.set(project.extensions.getByType().launcherFor { + // a temporary workaround -- diktat-gradle-plugin doesn't detect java version of `javaLauncher` + languageVersion.set(JavaLanguageVersion.of(11)) + }) + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index d5dc0258b0..b9c909cb82 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,6 +39,8 @@ include("diktat-rules") include("diktat-ruleset") include("diktat-test-framework") include("diktat-dev-ksp") +include("diktat-runner:diktat-runner-api") +include("diktat-runner:diktat-runner-ktlint-engine") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")