From 0fd74b5089afe9b48c2333f571895064b7a3c092 Mon Sep 17 00:00:00 2001 From: Nariman Abdullin Date: Thu, 6 Apr 2023 14:33:22 +0300 Subject: [PATCH] Diktat CLI runner (#1653) --- .github/workflows/build_and_test.yml | 48 +++- diktat-api/build.gradle.kts | 30 +++ .../org/cqfn/diktat/DiktatRunnerArguments.kt | 12 +- .../org/cqfn/diktat/DiktatRunnerFactory.kt | 24 +- .../cqfn/diktat/api/DiktatReporterFactory.kt | 2 +- diktat-cli/build.gradle.kts | 72 ++++++ .../main/kotlin/org/cqfn/diktat/DiktatMain.kt | 60 +++++ .../kotlin/org/cqfn/diktat/cli/DiktatMode.kt | 16 ++ .../org/cqfn/diktat/cli/DiktatProperties.kt | 242 ++++++++++++++++++ .../kotlin/org/cqfn/diktat/util/CliUtils.kt | 57 +++++ .../org/cqfn/diktat/util/CliUtilsKtTest.kt | 87 +++++++ diktat-gradle-plugin/build.gradle.kts | 1 - diktat-ktlint-engine/build.gradle.kts | 35 +++ .../ktlint/DiktatReporterFactoryImpl.kt | 6 +- diktat-ruleset/build.gradle.kts | 4 +- .../framework/config/TestArgumentsReader.kt | 2 - gradle/libs.versions.toml | 8 +- ...ktat-version-file-configuration.gradle.kts | 50 ---- settings.gradle.kts | 1 + 19 files changed, 681 insertions(+), 76 deletions(-) create mode 100644 diktat-cli/build.gradle.kts create mode 100644 diktat-cli/src/main/kotlin/org/cqfn/diktat/DiktatMain.kt create mode 100644 diktat-cli/src/main/kotlin/org/cqfn/diktat/cli/DiktatMode.kt create mode 100644 diktat-cli/src/main/kotlin/org/cqfn/diktat/cli/DiktatProperties.kt create mode 100644 diktat-cli/src/main/kotlin/org/cqfn/diktat/util/CliUtils.kt create mode 100644 diktat-cli/src/test/kotlin/org/cqfn/diktat/util/CliUtilsKtTest.kt delete mode 100644 gradle/plugins/src/main/kotlin/org/cqfn/diktat/buildutils/diktat-version-file-configuration.gradle.kts diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index bdb1df0d75..5042ca3eb8 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -57,13 +57,20 @@ jobs: name: gradle-reports path: '**/build/reports/' retention-days: 1 - - name: Upload diktat jar + - name: Upload diktat-ruleset jar uses: actions/upload-artifact@v3 with: name: diktat-ruleset path: diktat-ruleset/build/libs/diktat-*.jar # no need to store artifact longer, it is used only by dependant jobs retention-days: 1 + - name: Upload diktat-cli jar + uses: actions/upload-artifact@v3 + with: + name: diktat-cli + path: diktat-cli/build/libs/diktat-cli-*.jar + # no need to store artifact longer, it is used only by dependant jobs + retention-days: 1 - name: Code coverage report uses: codecov/codecov-action@v3 with: @@ -77,37 +84,55 @@ jobs: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] java-version: [8, 11] + type: [ ktlint, cli ] steps: - uses: actions/checkout@v3 - name: Retrieve Ktlint version + if: matrix.type == 'ktlint' run: | ktlint_version=$(cat gradle/libs.versions.toml | grep '^ktlint =' | awk -F'[=]' '{print $2}' | tr -d '" ') echo KTLINT_VERSION=$ktlint_version >> $GITHUB_ENV shell: bash - name: Setup environment + if: matrix.type == 'ktlint' run: | curl -o ktlint -sSL https://github.com/pinterest/ktlint/releases/download/${{ env.KTLINT_VERSION }}/ktlint && chmod a+x ktlint java -version shell: bash - name: Download diktat jar + if: matrix.type == 'ktlint' uses: actions/download-artifact@v3 with: name: diktat-ruleset - - name: Retrieve diktat jar file name + - name: Generate run command using ktlint + if: matrix.type == 'ktlint' + run: | + filename=$(ls -1 diktat-*.jar | head -n1) + echo DIKTAT_RUN="java -jar ktlint -R \"$filename\" --disabled_rules=standard,experimental,test,custom" >> $GITHUB_ENV + shell: bash + + - name: Download diktat cli jar + if: matrix.type == 'cli' + uses: actions/download-artifact@v3 + with: + name: diktat-cli + + - name: Generate run command using cli + if: matrix.type == 'cli' run: | - filename=$(ls diktat-*.jar) - echo DIKTAT_JAR=$filename >> $GITHUB_ENV + filename=$(ls -1 diktat-cli-*.jar | head -n1) + echo DIKTAT_RUN="java -jar \"$filename\"" >> $GITHUB_ENV shell: bash - name: Run diKTat from cli continue-on-error: true run: | - java -jar ktlint -R ${{ env.DIKTAT_JAR }} --disabled_rules=standard,experimental,test,custom 'examples/maven/src/main/kotlin/Test.kt' &>out.txt + ${{ env.DIKTAT_RUN }} 'examples/maven/src/main/kotlin/Test.kt' &>out.txt shell: bash - name: Check output @@ -121,14 +146,15 @@ jobs: continue-on-error: true if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }} run: | - java -jar ktlint -R ${{ env.DIKTAT_JAR }} --disabled_rules=standard,experimental,test,custom "$PWD/examples/maven/src/main/kotlin/Test.kt" &>out.txt + ${{ env.DIKTAT_RUN }} "$PWD/examples/maven/src/main/kotlin/Test.kt" &>out.txt shell: bash - name: Run diKTat from cli on windows (absolute paths) continue-on-error: true if: runner.os == 'Windows' + # cannot use '&>out.txt' since it's Windows run: | - java -jar ktlint -R ${{ env.DIKTAT_JAR }} --disabled_rules=standard,experimental,test,custom "%cd%/examples/maven/src/main/kotlin/Test.kt" > out.txt 2>&1 + ${{ env.DIKTAT_RUN }} "%cd%/examples/maven/src/main/kotlin/Test.kt" > out.txt 2>&1 shell: cmd - name: Check output (absolute paths) @@ -141,7 +167,7 @@ jobs: - name: Run diKTat from cli (glob paths, 1 of 4) continue-on-error: true run: | - java -jar ktlint -R ${{ env.DIKTAT_JAR }} --disabled_rules=standard,experimental,test,custom 'examples/maven/src/main/kotlin/*.kt' &>out.txt + ${{ env.DIKTAT_RUN }} 'examples/maven/src/main/kotlin/*.kt' &>out.txt shell: bash - name: Check output (glob paths, 1 of 4) @@ -154,7 +180,7 @@ jobs: - name: Run diKTat from cli (glob paths, 2 of 4) continue-on-error: true run: | - java -jar ktlint -R ${{ env.DIKTAT_JAR }} --disabled_rules=standard,experimental,test,custom 'examples/**/main/kotlin/*.kt' &>out.txt + ${{ env.DIKTAT_RUN }} 'examples/**/main/kotlin/*.kt' &>out.txt shell: bash - name: Check output (glob paths, 2 of 4) @@ -167,7 +193,7 @@ jobs: - name: Run diKTat from cli (glob paths, 3 of 4) continue-on-error: true run: | - java -jar ktlint -R ${{ env.DIKTAT_JAR }} --disabled_rules=standard,experimental,test,custom 'examples/**/*.kt' &>out.txt + ${{ env.DIKTAT_RUN }} 'examples/**/*.kt' &>out.txt shell: bash - name: Check output (glob paths, 3 of 4) @@ -180,7 +206,7 @@ jobs: - name: Run diKTat from cli (glob paths, 4 of 4) continue-on-error: true run: | - java -jar ktlint -R ${{ env.DIKTAT_JAR }} --disabled_rules=standard,experimental,test,custom '**/*.kt' &>out.txt + ${{ env.DIKTAT_RUN }} '**/*.kt' &>out.txt shell: bash - name: Check output (glob paths, 4 of 4) diff --git a/diktat-api/build.gradle.kts b/diktat-api/build.gradle.kts index 228b175173..d27314538f 100644 --- a/diktat-api/build.gradle.kts +++ b/diktat-api/build.gradle.kts @@ -9,3 +9,33 @@ project.description = "This module builds diktat-api" dependencies { implementation(libs.kotlin.compiler.embeddable) } + +val generateDiktatVersionFile by tasks.registering { + val outputDir = File("$buildDir/generated/src") + val versionsFile = outputDir.resolve("generated/DiktatVersion.kt") + + val diktatVersion = version.toString() + + inputs.property("diktat version", diktatVersion) + outputs.dir(outputDir) + + doFirst { + versionsFile.parentFile.mkdirs() + versionsFile.writeText( + """ + package generated + + const val DIKTAT_VERSION = "$diktatVersion" + + """.trimIndent() + ) + } +} + +kotlin.sourceSets.getByName("main") { + kotlin.srcDir( + generateDiktatVersionFile.map { + it.outputs.files.singleFile + } + ) +} diff --git a/diktat-api/src/main/kotlin/org/cqfn/diktat/DiktatRunnerArguments.kt b/diktat-api/src/main/kotlin/org/cqfn/diktat/DiktatRunnerArguments.kt index 781b77192e..026597824e 100644 --- a/diktat-api/src/main/kotlin/org/cqfn/diktat/DiktatRunnerArguments.kt +++ b/diktat-api/src/main/kotlin/org/cqfn/diktat/DiktatRunnerArguments.kt @@ -14,7 +14,9 @@ import kotlin.io.path.absolutePathString * @property baselineFile an optional path to file with baseline * @property reporterType type of reporter to report the detected errors * @property reporterOutput output for reporter - * @property loggingListener listener to log diktat runner phases + * @property groupByFileInPlain a flag `groupByFile` which is applicable for plain reporter only, **null** by default + * @property colorNameInPlain a color name which is applicable for plain reporter only, **null** by default + * @property loggingListener listener to log diktat runner phases, [DiktatProcessorListener.empty] by default */ data class DiktatRunnerArguments( val configFileName: String, @@ -23,6 +25,8 @@ data class DiktatRunnerArguments( val baselineFile: Path?, val reporterType: String, val reporterOutput: OutputStream?, + val groupByFileInPlain: Boolean? = null, + val colorNameInPlain: String? = null, val loggingListener: DiktatProcessorListener = DiktatProcessorListener.empty, ) { constructor( @@ -32,7 +36,9 @@ data class DiktatRunnerArguments( baselineFile: Path?, reporterType: String, reporterOutput: OutputStream?, - loggingListener: DiktatProcessorListener, + groupByFileInPlain: Boolean? = null, + colorNameInPlain: String? = null, + loggingListener: DiktatProcessorListener = DiktatProcessorListener.empty, ) : this( configFile.absolutePathString(), sourceRootDir, @@ -40,6 +46,8 @@ data class DiktatRunnerArguments( baselineFile, reporterType, reporterOutput, + groupByFileInPlain, + colorNameInPlain, loggingListener, ) } diff --git a/diktat-api/src/main/kotlin/org/cqfn/diktat/DiktatRunnerFactory.kt b/diktat-api/src/main/kotlin/org/cqfn/diktat/DiktatRunnerFactory.kt index dc1fe32221..7e886c787c 100644 --- a/diktat-api/src/main/kotlin/org/cqfn/diktat/DiktatRunnerFactory.kt +++ b/diktat-api/src/main/kotlin/org/cqfn/diktat/DiktatRunnerFactory.kt @@ -12,12 +12,14 @@ import java.nio.file.Path /** * A factory to create [DiktatRunner] + * + * @property diktatReporterFactory a factory for [DiktatReporter] */ class DiktatRunnerFactory( private val diktatRuleSetFactory: DiktatRuleSetFactory, private val diktatProcessorFactory: DiktatProcessorFactory, private val diktatBaselineFactory: DiktatBaselineFactory, - private val diktatReporterFactory: DiktatReporterFactory, + val diktatReporterFactory: DiktatReporterFactory, ) : Function1 { /** * @param args @@ -27,7 +29,11 @@ class DiktatRunnerFactory( val diktatRuleSet = diktatRuleSetFactory.create(args.configFileName) val processor = diktatProcessorFactory(diktatRuleSet) val (baseline, baselineGenerator) = resolveBaseline(args.baselineFile, args.sourceRootDir) - val (reporter, closer) = resolveReporter(args.reporterType, args.reporterOutput, args.sourceRootDir) + val (reporter, closer) = resolveReporter( + args.reporterType, args.reporterOutput, + args.colorNameInPlain, args.groupByFileInPlain, + args.sourceRootDir + ) return DiktatRunner( diktatProcessor = processor, diktatBaseline = baseline, @@ -53,6 +59,8 @@ class DiktatRunnerFactory( private fun resolveReporter( reporterType: String, reporterOutput: OutputStream?, + colorNameInPlain: String?, + groupByFileInPlain: Boolean?, sourceRootDir: Path, ): Pair { val (outputStream, closeListener) = reporterOutput @@ -60,7 +68,17 @@ class DiktatRunnerFactory( ?: run { System.`out` to DiktatProcessorListener.empty } - val actualReporter = diktatReporterFactory(reporterType, outputStream, sourceRootDir) + val actualReporter = if (reporterType == diktatReporterFactory.plainId) { + diktatReporterFactory.createPlain(outputStream, sourceRootDir, colorNameInPlain, groupByFileInPlain) + } else { + require(colorNameInPlain == null) { + "colorization is applicable only for plain reporter" + } + require(groupByFileInPlain == null) { + "groupByFile is applicable only for plain reporter" + } + diktatReporterFactory(reporterType, outputStream, sourceRootDir) + } return actualReporter to closeListener } } diff --git a/diktat-api/src/main/kotlin/org/cqfn/diktat/api/DiktatReporterFactory.kt b/diktat-api/src/main/kotlin/org/cqfn/diktat/api/DiktatReporterFactory.kt index 55f054f3ed..2fe86176bf 100644 --- a/diktat-api/src/main/kotlin/org/cqfn/diktat/api/DiktatReporterFactory.kt +++ b/diktat-api/src/main/kotlin/org/cqfn/diktat/api/DiktatReporterFactory.kt @@ -47,6 +47,6 @@ interface DiktatReporterFactory : Function3("shadowJar") { + archiveClassifier.set("") + manifest { + attributes["Main-Class"] = "org.cqfn.diktat.DiktatMainKt" + } + duplicatesStrategy = DuplicatesStrategy.FAIL +} + +// disable default jar +tasks.named("jar") { + enabled = false +} + +// it triggers shadowJar with default build +tasks { + build { + dependsOn(shadowJar) + } +} diff --git a/diktat-cli/src/main/kotlin/org/cqfn/diktat/DiktatMain.kt b/diktat-cli/src/main/kotlin/org/cqfn/diktat/DiktatMain.kt new file mode 100644 index 0000000000..aae16f9407 --- /dev/null +++ b/diktat-cli/src/main/kotlin/org/cqfn/diktat/DiktatMain.kt @@ -0,0 +1,60 @@ +/** + * The file contains main method + */ + +package org.cqfn.diktat + +import org.cqfn.diktat.api.DiktatProcessorListener +import org.cqfn.diktat.cli.DiktatMode +import org.cqfn.diktat.cli.DiktatProperties +import org.cqfn.diktat.ktlint.DiktatBaselineFactoryImpl +import org.cqfn.diktat.ktlint.DiktatProcessorFactoryImpl +import org.cqfn.diktat.ktlint.DiktatReporterFactoryImpl +import org.cqfn.diktat.ruleset.rules.DiktatRuleSetFactoryImpl + +import mu.KotlinLogging + +import java.nio.file.Path +import java.nio.file.Paths + +import kotlin.io.path.absolutePathString + +private val log = KotlinLogging.logger { } + +private val loggingListener = object : DiktatProcessorListener { + override fun before(file: Path) { + log.debug { + "Start processing the file: $file" + } + } +} + +fun main(args: Array) { + val diktatRunnerFactory = DiktatRunnerFactory( + DiktatRuleSetFactoryImpl(), + DiktatProcessorFactoryImpl(), + DiktatBaselineFactoryImpl(), + DiktatReporterFactoryImpl(), + ) + val properties = DiktatProperties.parse(diktatRunnerFactory.diktatReporterFactory, args) + properties.configureLogger() + + log.debug { + "Loading diktatRuleSet using config ${properties.config}" + } + val currentFolder = Paths.get(".").toAbsolutePath().normalize() + val diktatRunnerArguments = properties.toRunnerArguments( + sourceRootDir = currentFolder, + loggingListener = loggingListener, + ) + + val diktatRunner = diktatRunnerFactory(diktatRunnerArguments) + when (properties.mode) { + DiktatMode.CHECK -> diktatRunner.checkAll(diktatRunnerArguments) + DiktatMode.FIX -> diktatRunner.fixAll(diktatRunnerArguments) { updatedFile -> + log.warn { + "Original and formatted content differ, writing to ${updatedFile.absolutePathString()}..." + } + } + } +} diff --git a/diktat-cli/src/main/kotlin/org/cqfn/diktat/cli/DiktatMode.kt b/diktat-cli/src/main/kotlin/org/cqfn/diktat/cli/DiktatMode.kt new file mode 100644 index 0000000000..1618e008f4 --- /dev/null +++ b/diktat-cli/src/main/kotlin/org/cqfn/diktat/cli/DiktatMode.kt @@ -0,0 +1,16 @@ +package org.cqfn.diktat.cli + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Mode of `diktat` + */ +@Serializable +enum class DiktatMode { + @SerialName("check") + CHECK, + @SerialName("fix") + FIX, + ; +} diff --git a/diktat-cli/src/main/kotlin/org/cqfn/diktat/cli/DiktatProperties.kt b/diktat-cli/src/main/kotlin/org/cqfn/diktat/cli/DiktatProperties.kt new file mode 100644 index 0000000000..313a35a8b8 --- /dev/null +++ b/diktat-cli/src/main/kotlin/org/cqfn/diktat/cli/DiktatProperties.kt @@ -0,0 +1,242 @@ +package org.cqfn.diktat.cli + +import org.cqfn.diktat.DiktatRunnerArguments +import org.cqfn.diktat.api.DiktatProcessorListener +import org.cqfn.diktat.api.DiktatReporterFactory +import org.cqfn.diktat.common.config.rules.DIKTAT +import org.cqfn.diktat.common.config.rules.DIKTAT_ANALYSIS_CONF +import org.cqfn.diktat.util.isKotlinCodeOrScript +import org.cqfn.diktat.util.tryToPathIfExists +import org.cqfn.diktat.util.walkByGlob +import generated.DIKTAT_VERSION +import generated.KTLINT_VERSION +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.LoggerContext +import org.slf4j.event.Level +import java.io.OutputStream +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.createDirectories +import kotlin.io.path.outputStream +import kotlin.system.exitProcess +import kotlinx.cli.ArgParser +import kotlinx.cli.ArgType +import kotlinx.cli.default +import kotlinx.cli.vararg + +/** + * @property config path to `diktat-analysis.yml` + * @property mode mode of `diktat` + * @property reporterProviderId + * @property output + * @property patterns + */ +data class DiktatProperties( + val config: String, + val mode: DiktatMode, + val reporterProviderId: String, + val output: String?, + private val groupByFileInPlain: Boolean, + private val colorNameInPlain: String?, + private val logLevel: Level, + val patterns: List, +) { + /** + * Configure logger level using [logLevel] + */ + fun configureLogger() { + // set log level + LogManager.getContext(false) + .let { it as LoggerContext } + .also { ctx -> + ctx.configuration.rootLogger.level = when (logLevel) { + Level.ERROR -> org.apache.logging.log4j.Level.ERROR + Level.WARN -> org.apache.logging.log4j.Level.WARN + Level.INFO -> org.apache.logging.log4j.Level.INFO + Level.DEBUG -> org.apache.logging.log4j.Level.DEBUG + Level.TRACE -> org.apache.logging.log4j.Level.TRACE + } + } + .updateLoggers() + } + + /** + * @param sourceRootDir + * @param loggingListener + * @return [DiktatRunnerArguments] created from [DiktatProperties] + */ + fun toRunnerArguments( + sourceRootDir: Path, + loggingListener: DiktatProcessorListener, + ): DiktatRunnerArguments = DiktatRunnerArguments( + configFileName = config, + sourceRootDir = sourceRootDir, + files = getFiles(sourceRootDir), + baselineFile = null, + reporterType = reporterProviderId, + reporterOutput = getReporterOutput(), + groupByFileInPlain = groupByFileInPlain, + colorNameInPlain = colorNameInPlain, + loggingListener = loggingListener, + ) + + private fun getFiles(sourceRootDir: Path): Collection = patterns + .asSequence() + .flatMap { pattern -> + pattern.tryToPathIfExists()?.let { sequenceOf(it) } + ?: sourceRootDir.walkByGlob(pattern) + } + .filter { file -> file.isKotlinCodeOrScript() } + .map { it.normalize() } + .map { it.toAbsolutePath() } + .distinct() + .toList() + + private fun getReporterOutput(): OutputStream? = output + ?.let { Paths.get(it) } + ?.also { it.parent.createDirectories() } + ?.outputStream() + + companion object { + /** + * @param diktatReporterFactory + * @param args cli arguments + * @return parsed [DiktatProperties] + */ + @Suppress( + "LongMethod", + "TOO_LONG_FUNCTION" + ) + fun parse( + diktatReporterFactory: DiktatReporterFactory, + args: Array, + ): DiktatProperties { + val parser = ArgParser(DIKTAT) + val config: String by parser.option( + type = ArgType.String, + fullName = "config", + shortName = "c", + description = "Specify the location of the YAML configuration file. By default, $DIKTAT_ANALYSIS_CONF in the current directory is used.", + ).default(DIKTAT_ANALYSIS_CONF) + val mode: DiktatMode by parser.option( + type = ArgType.Choice(), + fullName = "mode", + shortName = "m", + description = "Mode of `diktat` controls that `diktat` fixes or only finds any deviations from the code style." + ).default(DiktatMode.CHECK) + val reporterType: String by parser.reporterType(diktatReporterFactory) + val output: String? by parser.option( + type = ArgType.String, + fullName = "output", + shortName = "o", + description = "Redirect the reporter output to a file.", + ) + val groupByFileInPlain: Boolean by parser.option( + type = ArgType.Boolean, + fullName = "plain-group-by-file", + shortName = null, + description = "A flag for plain reporter" + ).default(false) + val colorName: String? by parser.colorName(diktatReporterFactory) + val logLevel: Level by parser.option( + type = ArgType.Choice(), + fullName = "log-level", + shortName = "l", + description = "Enable the output with specific level", + ).default(Level.INFO) + val patterns: List by parser.argument( + type = ArgType.String, + description = "A list of files to process by diktat" + ).vararg() + + parser.addOptionAndShowTextWithExit( + fullName = "version", + shortName = "V", + description = "Output version information and exit.", + args = args, + ) { + """ + Diktat: $DIKTAT_VERSION + Ktlint: $KTLINT_VERSION + """.trimIndent() + } + parser.addOptionAndShowTextWithExit( + fullName = "license", + shortName = null, + description = "Display the license and exit.", + args = args, + ) { + val resourceName = "META-INF/diktat/LICENSE" + DiktatProperties::class.java + .classLoader + .getResource(resourceName) + ?.readText() + ?: error("Resource $resourceName not found") + } + + parser.parse(args) + return DiktatProperties( + config = config, + mode = mode, + reporterProviderId = reporterType, + output = output, + groupByFileInPlain = groupByFileInPlain, + colorNameInPlain = colorName, + logLevel = logLevel, + patterns = patterns, + ) + } + + /** + * @param diktatReporterFactory + * @return a single type of [org.cqfn.diktat.api.DiktatReporter] as parsed cli arg + */ + private fun ArgParser.reporterType(diktatReporterFactory: DiktatReporterFactory) = option( + type = ArgType.Choice( + choices = diktatReporterFactory.ids.toList(), + toVariant = { it }, + variantToString = { it }, + ), + fullName = "reporter", + shortName = "r", + description = "The reporter to use" + ) + .default(diktatReporterFactory.plainId) + + /** + * @param diktatReporterFactory + * @return a single and optional color name as parsed cli args + */ + private fun ArgParser.colorName(diktatReporterFactory: DiktatReporterFactory) = this.option( + type = ArgType.Choice( + choices = diktatReporterFactory.colorNamesInPlain.toList(), + toVariant = { it }, + variantToString = { it }, + ), + fullName = "plain-color", + shortName = null, + description = "Colorize the output.", + ) + + private fun ArgParser.addOptionAndShowTextWithExit( + fullName: String, + shortName: String?, + description: String, + args: Array, + contentSupplier: () -> String + ) { + // add here to print in help + option( + type = ArgType.Boolean, + fullName = fullName, + shortName = shortName, + description = description + ) + if (args.contains("--$fullName") || shortName?.let { args.contains("-$it") } == true) { + @Suppress("DEBUG_PRINT", "ForbiddenMethodCall") + println(contentSupplier()) + exitProcess(0) + } + } + } +} diff --git a/diktat-cli/src/main/kotlin/org/cqfn/diktat/util/CliUtils.kt b/diktat-cli/src/main/kotlin/org/cqfn/diktat/util/CliUtils.kt new file mode 100644 index 0000000000..ace7196ecf --- /dev/null +++ b/diktat-cli/src/main/kotlin/org/cqfn/diktat/util/CliUtils.kt @@ -0,0 +1,57 @@ +/** + * This class contains util methods to operate with java.nio.file.Path for CLI + */ + +package org.cqfn.diktat.util + +import java.io.File +import java.nio.file.FileSystem +import java.nio.file.FileSystems +import java.nio.file.InvalidPathException +import java.nio.file.Path +import java.nio.file.PathMatcher +import java.nio.file.Paths +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.PathWalkOption +import kotlin.io.path.absolutePathString +import kotlin.io.path.exists +import kotlin.io.path.walk + +// all roots +private val roots: Set = FileSystems.getDefault() + .rootDirectories + .asSequence() + .map { it.absolutePathString() } + .toSet() + +/** + * Create a matcher and return a filter that uses it. + * + * @param glob glob pattern to filter files + * @return a sequence of files which matches to [glob] + */ +@OptIn(ExperimentalPathApi::class) +fun Path.walkByGlob(glob: String): Sequence = fileSystem.globMatcher(glob) + .let { matcher -> + this.walk(PathWalkOption.INCLUDE_DIRECTORIES) + .filter { matcher.matches(it) } + } + +/** + * @return path or null if path is invalid or doesn't exist + */ +fun String.tryToPathIfExists(): Path? = try { + Paths.get(this).takeIf { it.exists() } +} catch (e: InvalidPathException) { + null +} + +private fun FileSystem.globMatcher(glob: String): PathMatcher = if (isAbsoluteGlob(glob)) { + getPathMatcher("glob:${glob.toUnixSeparator()}") +} else { + getPathMatcher("glob:**/${glob.toUnixSeparator()}") +} + +private fun String.toUnixSeparator(): String = replace(File.separatorChar, '/') + +private fun isAbsoluteGlob(glob: String): Boolean = glob.startsWith("**") || roots.any { glob.startsWith(it, true) } diff --git a/diktat-cli/src/test/kotlin/org/cqfn/diktat/util/CliUtilsKtTest.kt b/diktat-cli/src/test/kotlin/org/cqfn/diktat/util/CliUtilsKtTest.kt new file mode 100644 index 0000000000..19d65f1428 --- /dev/null +++ b/diktat-cli/src/test/kotlin/org/cqfn/diktat/util/CliUtilsKtTest.kt @@ -0,0 +1,87 @@ +package org.cqfn.diktat.util + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File +import java.nio.file.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.createDirectory +import kotlin.io.path.createFile +import kotlin.io.path.writeText + +class CliUtilsKtTest { + private fun setupHierarchy(dir: Path) { + dir.resolveAndCreateDirectory("folder1") + .also { folder1 -> + folder1.resolveAndCreateDirectory("subFolder11") + .also { subFolder11 -> + subFolder11.resolveAndCreateFile("Test1.kt") + subFolder11.resolveAndCreateFile("Test2.kt") + } + folder1.resolveAndCreateDirectory("subFolder12") + .also { subFolder12 -> + subFolder12.resolveAndCreateFile("Test1.kt") + } + } + dir.resolveAndCreateDirectory("folder2") + .also { folder2 -> + folder2.resolveAndCreateFile("Test1.kt") + folder2.resolveAndCreateFile("Test2.kt") + folder2.resolveAndCreateFile("Test3.kt") + } + } + + @Test + fun walkByGlobWithLeadingAsterisks(@TempDir tmpDir: Path) { + setupHierarchy(tmpDir) + + Assertions.assertThat(tmpDir.walkByGlob("**/Test1.kt").toList()) + .containsExactlyInAnyOrder( + tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test1.kt"), + tmpDir.resolve("folder1").resolve("subFolder12").resolve("Test1.kt"), + tmpDir.resolve("folder2").resolve("Test1.kt"), + ) + } + + + @Test + fun walkByGlobWithGlobalPath(@TempDir tmpDir: Path) { + setupHierarchy(tmpDir) + + Assertions.assertThat(tmpDir.walkByGlob("${tmpDir.absolutePathString()}${File.separator}**${File.separator}Test2.kt").toList()) + .containsExactlyInAnyOrder( + tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test2.kt"), + tmpDir.resolve("folder2").resolve("Test2.kt"), + ) + } + + @Test + fun walkByGlobWithRelativePath(@TempDir tmpDir: Path) { + setupHierarchy(tmpDir) + + Assertions.assertThat(tmpDir.walkByGlob("folder1/subFolder11/*.kt").toList()) + .containsExactlyInAnyOrder( + tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test1.kt"), + tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test2.kt"), + ) + } + + @Test + fun walkByGlobWithEmptyResult(@TempDir tmpDir: Path) { + setupHierarchy(tmpDir) + + Assertions.assertThat(tmpDir.walkByGlob("**/*.kts").toList()) + .isEmpty() + } + + companion object { + private fun Path.resolveAndCreateDirectory(name: String): Path = resolve(name).also { + it.createDirectory() + } + + private fun Path.resolveAndCreateFile(name: String): Path = resolve(name).also { + it.createFile().writeText("Test file: $name") + } + } +} diff --git a/diktat-gradle-plugin/build.gradle.kts b/diktat-gradle-plugin/build.gradle.kts index 02adc921ca..b76bc0e329 100644 --- a/diktat-gradle-plugin/build.gradle.kts +++ b/diktat-gradle-plugin/build.gradle.kts @@ -5,7 +5,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.cqfn.diktat.buildutils.kotlin-jvm-configuration") id("org.cqfn.diktat.buildutils.code-quality-convention") - id("org.cqfn.diktat.buildutils.diktat-version-file-configuration") id("pl.droidsonroids.jacoco.testkit") version "1.0.9" id("org.gradle.test-retry") version "1.5.2" id("com.gradle.plugin-publish") version "1.1.0" diff --git a/diktat-ktlint-engine/build.gradle.kts b/diktat-ktlint-engine/build.gradle.kts index f7853669be..37088e239b 100644 --- a/diktat-ktlint-engine/build.gradle.kts +++ b/diktat-ktlint-engine/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.accessors.dm.LibrariesForLibs + plugins { id("org.cqfn.diktat.buildutils.kotlin-jvm-configuration") id("org.cqfn.diktat.buildutils.code-quality-convention") @@ -24,3 +26,36 @@ dependencies { testImplementation(libs.assertj.core) testImplementation(libs.mockito) } + +val ktlintVersion: String = the() + .versions + .ktlint + .get() + +val generateKtlintVersionFile by tasks.registering { + val outputDir = File("$buildDir/generated/src") + val versionsFile = outputDir.resolve("generated/KtLintVersion.kt") + + inputs.property("ktlint version", ktlintVersion) + outputs.dir(outputDir) + + doFirst { + versionsFile.parentFile.mkdirs() + versionsFile.writeText( + """ + package generated + + const val KTLINT_VERSION = "$ktlintVersion" + + """.trimIndent() + ) + } +} + +kotlin.sourceSets.getByName("main") { + kotlin.srcDir( + generateKtlintVersionFile.map { + it.outputs.files.singleFile + } + ) +} diff --git a/diktat-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/DiktatReporterFactoryImpl.kt b/diktat-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/DiktatReporterFactoryImpl.kt index 1d7c2ca30c..96425526d1 100644 --- a/diktat-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/DiktatReporterFactoryImpl.kt +++ b/diktat-ktlint-engine/src/main/kotlin/org/cqfn/diktat/ktlint/DiktatReporterFactoryImpl.kt @@ -63,7 +63,7 @@ class DiktatReporterFactoryImpl : DiktatReporterFactory { outputStream: OutputStream, sourceRootDir: Path, colorName: String?, - groupByFile: Boolean, + groupByFile: Boolean?, ): DiktatReporter { val opt = buildMap { colorName?.let { @@ -73,9 +73,7 @@ class DiktatReporterFactoryImpl : DiktatReporterFactory { put("color", false) put("color_name", Color.DARK_GRAY) } - if (groupByFile) { - put("group_by_file", true) - } + groupByFile?.let { put("group_by_file", it) } }.mapValues { it.value.toString() } return plainReporterProvider.get(outputStream.asPrintStream(), opt).wrap(sourceRootDir) } diff --git a/diktat-ruleset/build.gradle.kts b/diktat-ruleset/build.gradle.kts index 58ec49b40d..b1b72bbbca 100644 --- a/diktat-ruleset/build.gradle.kts +++ b/diktat-ruleset/build.gradle.kts @@ -2,11 +2,12 @@ import org.cqfn.diktat.buildutils.configurePom import com.github.jengelman.gradle.plugins.shadow.ShadowExtension import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +@Suppress("DSL_SCOPE_VIOLATION", "RUN_IN_SCRIPT") // https://github.com/gradle/gradle/issues/22797 plugins { id("org.cqfn.diktat.buildutils.kotlin-jvm-configuration") id("org.cqfn.diktat.buildutils.code-quality-convention") id("org.cqfn.diktat.buildutils.publishing-configuration") - id("com.github.johnrengelman.shadow") version "7.1.2" + alias(libs.plugins.shadow) `maven-publish` } @@ -46,6 +47,7 @@ tasks.named("shadowJar") { archiveClassifier.set("") // need to relocate serialization from kaml to avoid conflicts with KtLint relocate("kotlinx.serialization", "com.saveourtool.kotlinx_serialization") + duplicatesStrategy = DuplicatesStrategy.FAIL } // disable default jar diff --git a/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/config/TestArgumentsReader.kt b/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/config/TestArgumentsReader.kt index fbfd9ed9ed..42ad373eee 100644 --- a/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/config/TestArgumentsReader.kt +++ b/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/config/TestArgumentsReader.kt @@ -16,7 +16,6 @@ import java.io.IOException import java.util.stream.Collectors import kotlin.system.exitProcess -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json @@ -86,7 +85,6 @@ class TestArgumentsReader( * @param fileStream a [BufferedReader] representing input JSON * @return list of [CliArgument]s */ - @OptIn(ExperimentalSerializationApi::class) @Throws(IOException::class) override fun parseResource(fileStream: BufferedReader): List { val jsonValue = fileStream.lines().collect(Collectors.joining()) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 745aab6dff..72722f4d60 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,6 +29,9 @@ jupiter-itf-extension = "0.12.0" # FIXME: need to migrate to mockito mockito-all = "1.10.19" +# executable jar +kotlinx-cli = "0.3.5" +gradle-shadow = "7.1.2" # copied from save-cloud jetbrains-annotations = "24.0.1" @@ -49,7 +52,6 @@ commons-compress = "1.22" zip4j = "2.11.5" ktoml = "0.4.1" springdoc = "1.6.15" -kotlinx-cli = "0.3.5" spotless = "6.18.0" arrow-kt = "1.1.5" publish = "1.3.0" @@ -67,6 +69,7 @@ talaiot-base = { id = "io.github.cdsap.talaiot.plugin.base", version = "1.5.3" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } spotless = { id = "com.diffplug.gradle.spotless", version.ref = "spotless" } download = { id = "de.undercouch.download", version.ref = "download" } +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "gradle-shadow" } [libraries] save-common = { module = "com.saveourtool.save:save-common", version.ref = "save-cli" } @@ -126,6 +129,9 @@ kotlin-logging = { module = "io.github.microutils:kotlin-logging", version.ref = log4j2-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j2" } log4j2-slf4j = { module = "org.apache.logging.log4j:log4j-slf4j-impl", version.ref = "log4j2" } +# cli +kotlinx-cli = { module = "org.jetbrains.kotlinx:kotlinx-cli", version.ref = "kotlinx-cli" } + # testing junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } diff --git a/gradle/plugins/src/main/kotlin/org/cqfn/diktat/buildutils/diktat-version-file-configuration.gradle.kts b/gradle/plugins/src/main/kotlin/org/cqfn/diktat/buildutils/diktat-version-file-configuration.gradle.kts deleted file mode 100644 index fa5f3267f2..0000000000 --- a/gradle/plugins/src/main/kotlin/org/cqfn/diktat/buildutils/diktat-version-file-configuration.gradle.kts +++ /dev/null @@ -1,50 +0,0 @@ -package org.cqfn.diktat.buildutils - -import org.gradle.accessors.dm.LibrariesForLibs -import org.gradle.kotlin.dsl.getValue -import org.gradle.kotlin.dsl.kotlin -import org.gradle.kotlin.dsl.provideDelegate -import org.gradle.kotlin.dsl.registering -import org.gradle.kotlin.dsl.the -import java.io.File - -plugins { - kotlin("jvm") -} - -val ktlintVersion: String = the() - .versions - .ktlint - .get() - -val generateVersionsFile by tasks.registering { - val outputDir = File("$buildDir/generated/src") - val versionsFile = outputDir.resolve("generated/Versions.kt") - - val diktatVersion = version.toString() - - inputs.property("diktat version", diktatVersion) - inputs.property("ktlint version", ktlintVersion) - outputs.dir(outputDir) - - doFirst { - versionsFile.parentFile.mkdirs() - versionsFile.writeText( - """ - package generated - - internal const val DIKTAT_VERSION = "$diktatVersion" - internal const val KTLINT_VERSION = "$ktlintVersion" - - """.trimIndent() - ) - } -} - -kotlin.sourceSets.getByName("main") { - kotlin.srcDir( - generateVersionsFile.map { - it.outputs.files.singleFile - } - ) -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 9ec48905fa..2d8095a152 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,6 +41,7 @@ include("diktat-rules") include("diktat-ruleset") include("diktat-test-framework") include("diktat-dev-ksp") +include("diktat-cli") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")