diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index bf0ba792d4..67ee0dc09f 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -33,7 +33,7 @@ jobs: # required for correct codecov upload fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin @@ -95,7 +95,7 @@ jobs: # required for correct codecov upload fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index 70f59357d3..07be6af363 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: 'Set up Java 11' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 11 diff --git a/.github/workflows/detekt.yml b/.github/workflows/detekt.yml index a903f3f918..68e082cc0c 100644 --- a/.github/workflows/detekt.yml +++ b/.github/workflows/detekt.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin diff --git a/.github/workflows/diktat.yml b/.github/workflows/diktat.yml index 7a6560ab08..cb7f431cc6 100644 --- a/.github/workflows/diktat.yml +++ b/.github/workflows/diktat.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin diff --git a/.github/workflows/diktat_snapshot.yml b/.github/workflows/diktat_snapshot.yml index 0b59d67ebd..0c4a4344a0 100644 --- a/.github/workflows/diktat_snapshot.yml +++ b/.github/workflows/diktat_snapshot.yml @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 39efd158b9..bbef86b078 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,18 +28,14 @@ jobs: fetch-depth: 0 - name: 'Set up Java' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin - - name: 'Publish a release to GitHub' - id: publish-github - uses: gradle/gradle-build-action@v2 - with: - gradle-version: wrapper - arguments: | - publishAllPublicationsToGitHubRepository + - name: 'Calculate the release version' + run: | + echo "RELEASE_VERSION=${GITHUB_REF#'refs/tags/v'}" >> $GITHUB_ENV - name: 'Publish a release to Maven Central' id: publish-sonatype @@ -60,15 +56,49 @@ jobs: -Pgradle.publish.key=${{ secrets.GRADLE_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_SECRET }} - github_release: - needs: release - name: 'Github Release' - runs-on: ubuntu-latest - steps: - - name: 'Github Release' + - name: 'Publish a release to GitHub' + id: publish-github + uses: gradle/gradle-build-action@v2 + with: + gradle-version: wrapper + arguments: | + shadowExecutableJar + publishAllPublicationsToGitHubRepository + - name: 'GitHub Release' + id: create_release uses: actions/create-release@v1 with: tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} + release_name: Release ${{ env.RELEASE_VERSION }} draft: false prerelease: false + - name: Upload Diktat CLI to GitHub release + id: upload-release-asset-cli + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./diktat-cli/build/diktat-cli-${{ env.RELEASE_VERSION }} + asset_name: diktat + asset_content_type: application/zip + - name: Upload Diktat CLI for Windows to GitHub release + id: upload-release-asset-cli-win + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./diktat-cli/src/main/script/diktat.cmd + asset_name: diktat.cmd + asset_content_type: application/octet-stream + - name: Upload Diktat ruleset for KtLint to GitHub release + id: upload-release-asset-ruleset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./diktat-ruleset/build/libs/diktat-${{ env.RELEASE_VERSION }}.jar + asset_name: diktat-${{ env.RELEASE_VERSION }}.jar + asset_content_type: application/zip diff --git a/diktat-analysis.yml b/diktat-analysis.yml index 329daf2895..10586be2ed 100644 --- a/diktat-analysis.yml +++ b/diktat-analysis.yml @@ -18,6 +18,8 @@ # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE - name: CONSTANT_UPPERCASE enabled: true + configuration: + exceptionConstNames: "serialVersionUID" # Checks that enum value is in upper SNAKE_CASE or in PascalCase depending on the config. UPPER_SNAKE_CASE is the default, but can be changed by 'enumStyle' config - name: ENUM_VALUE enabled: true diff --git a/diktat-cli/build.gradle.kts b/diktat-cli/build.gradle.kts index f9addf8d21..642b3da448 100644 --- a/diktat-cli/build.gradle.kts +++ b/diktat-cli/build.gradle.kts @@ -1,6 +1,5 @@ import com.saveourtool.diktat.buildutils.configurePublications import com.github.jengelman.gradle.plugins.shadow.ShadowExtension -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.jetbrains.kotlin.incremental.createDirectory @Suppress("DSL_SCOPE_VIOLATION", "RUN_IN_SCRIPT") // https://github.com/gradle/gradle/issues/22797 @@ -60,7 +59,7 @@ sourceSets.getByName("main") { ) } -tasks.named("shadowJar") { +tasks.shadowJar { archiveClassifier.set("") manifest { attributes["Main-Class"] = "com.saveourtool.diktat.DiktatMainKt" @@ -69,6 +68,34 @@ tasks.named("shadowJar") { duplicatesStrategy = DuplicatesStrategy.FAIL } +tasks.register("shadowExecutableJar") { + group = "Distribution" + dependsOn(tasks.shadowJar) + + val scriptFile = project.file("src/main/script/header-diktat.sh") + val shadowJarFile = tasks.shadowJar + .get() + .outputs + .files + .singleFile + val outputFile = project.layout + .buildDirectory + .file(shadowJarFile.name.removeSuffix(".jar")) + + inputs.files(scriptFile, shadowJarFile) + outputs.file(outputFile) + + doLast { + outputFile.get() + .asFile + .apply { + writeBytes(scriptFile.readBytes()) + appendBytes(shadowJarFile.readBytes()) + setExecutable(true, false) + } + } +} + // disable default jar tasks.named("jar") { enabled = false diff --git a/diktat-cli/src/main/kotlin/com/saveourtool/diktat/cli/DiktatProperties.kt b/diktat-cli/src/main/kotlin/com/saveourtool/diktat/cli/DiktatProperties.kt index f3756e6cbb..3700ff283c 100644 --- a/diktat-cli/src/main/kotlin/com/saveourtool/diktat/cli/DiktatProperties.kt +++ b/diktat-cli/src/main/kotlin/com/saveourtool/diktat/cli/DiktatProperties.kt @@ -9,8 +9,7 @@ import com.saveourtool.diktat.api.DiktatReporterCreationArguments import com.saveourtool.diktat.api.DiktatReporterFactory import com.saveourtool.diktat.api.DiktatReporterType import com.saveourtool.diktat.util.isKotlinCodeOrScript -import com.saveourtool.diktat.util.tryToPathIfExists -import com.saveourtool.diktat.util.walkByGlob +import com.saveourtool.diktat.util.listFiles import generated.DIKTAT_VERSION import org.apache.logging.log4j.LogManager @@ -96,16 +95,8 @@ data class DiktatProperties( ) } - private fun getFiles(sourceRootDir: Path): Collection = patterns - .asSequence() - .flatMap { pattern -> - pattern.tryToPathIfExists()?.let { sequenceOf(it) } - ?: sourceRootDir.walkByGlob(pattern) - } + private fun getFiles(sourceRootDir: Path): Collection = sourceRootDir.listFiles(patterns = patterns.toTypedArray()) .filter { file -> file.isKotlinCodeOrScript() } - .map { it.normalize() } - .map { it.toAbsolutePath() } - .distinct() .toList() private fun getReporterOutput(): OutputStream? = output diff --git a/diktat-cli/src/main/kotlin/com/saveourtool/diktat/util/CliUtils.kt b/diktat-cli/src/main/kotlin/com/saveourtool/diktat/util/CliUtils.kt index 632df32475..1cd816758d 100644 --- a/diktat-cli/src/main/kotlin/com/saveourtool/diktat/util/CliUtils.kt +++ b/diktat-cli/src/main/kotlin/com/saveourtool/diktat/util/CliUtils.kt @@ -5,18 +5,21 @@ package com.saveourtool.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.Path import kotlin.io.path.absolutePathString import kotlin.io.path.exists import kotlin.io.path.walk +private const val NEGATIVE_PREFIX_PATTERN = "!" +private const val PARENT_DIRECTORY_PREFIX = 3 +private const val PARENT_DIRECTORY_UNIX = "../" +private const val PARENT_DIRECTORY_WINDOWS = "..\\" + // all roots private val roots: Set = FileSystems.getDefault() .rootDirectories @@ -24,6 +27,33 @@ private val roots: Set = FileSystems.getDefault() .map { it.absolutePathString() } .toSet() +/** + * Lists all files in [this] directory based on [patterns] + * + * @param patterns a path to a file or a directory (all files from this directory will be returned) or an [Ant-style path pattern](https://ant.apache.org/manual/dirtasks.html#patterns) + * @return [Sequence] of files as [Path] matched to provided [patterns] + */ +fun Path.listFiles( + vararg patterns: String, +): Sequence { + val (includePatterns, excludePatterns) = patterns.partition { !it.startsWith(NEGATIVE_PREFIX_PATTERN) } + val exclude by lazy { + doListFiles(excludePatterns.map { it.removePrefix(NEGATIVE_PREFIX_PATTERN) }) + .toSet() + } + return doListFiles(includePatterns).filterNot { exclude.contains(it) } +} + +@OptIn(ExperimentalPathApi::class) +private fun Path.doListFiles(patterns: List): Sequence = patterns + .asSequence() + .flatMap { pattern -> + tryToResolveIfExists(pattern, this)?.walk() ?: walkByGlob(pattern) + } + .map { it.normalize() } + .map { it.toAbsolutePath() } + .distinct() + /** * Create a matcher and return a filter that uses it. * @@ -31,27 +61,40 @@ private val roots: Set = FileSystems.getDefault() * @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) } +private fun Path.walkByGlob(glob: String): Sequence = if (glob.startsWith(PARENT_DIRECTORY_UNIX) || glob.startsWith(PARENT_DIRECTORY_WINDOWS)) { + parent?.walkByGlob(glob.substring(PARENT_DIRECTORY_PREFIX)) ?: emptySequence() +} else { + getAbsoluteGlobAndRoot(glob, this) + .let { (absoluteGlob, root) -> + absoluteGlob + .replace("([^\\\\])\\\\([^\\\\])".toRegex(), "$1\\\\\\\\$2") // encode Windows separators + .let { root.fileSystem.getPathMatcher("glob:$it") } + .let { matcher -> + root.walk().filter { matcher.matches(it) } + } + } +} + +private fun String.findRoot(): Path = substring(0, indexOf('*')) + .let { withoutAsterisks -> + withoutAsterisks.substring(0, withoutAsterisks.lastIndexOfAny(charArrayOf('\\', '/'))) } + .let { Path(it) } /** + * @param candidate + * @param currentDirectory * @return path or null if path is invalid or doesn't exist */ -fun String.tryToPathIfExists(): Path? = try { - Paths.get(this).takeIf { it.exists() } +private fun tryToResolveIfExists(candidate: String, currentDirectory: Path): Path? = try { + Paths.get(candidate).takeIf { it.exists() } + ?: currentDirectory.resolve(candidate).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 getAbsoluteGlobAndRoot(glob: String, currentFolder: Path): Pair = when { + glob.startsWith("**") -> glob to currentFolder + roots.any { glob.startsWith(it, true) } -> glob to glob.findRoot() + else -> "${currentFolder.absolutePathString()}${File.separatorChar}$glob" to currentFolder } - -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/main/script/diktat.cmd b/diktat-cli/src/main/script/diktat.cmd new file mode 100644 index 0000000000..4dd7b9ccb0 --- /dev/null +++ b/diktat-cli/src/main/script/diktat.cmd @@ -0,0 +1,19 @@ +@echo off + +rem +rem diKTat command-line client for Windows +rem +rem Uses Git Bash, so requires Git to be installed. +rem + +set "git_install_location=%ProgramFiles%\Git" +set "git_url=https://github.com/git-for-windows/git/releases/latest" + +if exist "%git_install_location%" ( + setlocal + set "PATH=%git_install_location%\usr\bin;%PATH%" + for /f "usebackq tokens=*" %%p in (`cygpath "%~dpn0"`) do bash --noprofile --norc %%p %* +) else ( + echo Expecting Git for Windows at %git_install_location%; please install it from %git_url% + start %git_url% +) diff --git a/diktat-cli/src/main/script/header-diktat.sh b/diktat-cli/src/main/script/header-diktat.sh new file mode 100644 index 0000000000..b0f91b8f6e --- /dev/null +++ b/diktat-cli/src/main/script/header-diktat.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# +# vim:ai et sw=4 si sta ts=4: +# +# External variables used: +# +# - JAVA_HOME +# - GITHUB_ACTIONS + +# Bash strict mode, +# see http://redsymbol.net/articles/unofficial-bash-strict-mode/. +set -euo pipefail +IFS=$'\n' + +function error() { + local message + message="$*" + + if [[ "${GITHUB_ACTIONS:=false}" == 'true' ]] + then + # Echoing to GitHub. + echo "::error::${message}" + elif [[ -t 1 ]] + then + # Echoing to a terminal. + echo -e "\e[1m$(basename "$0"): \e[31merror:\e[0m ${message}" >&2 + else + # Echoing to a pipe. + echo "$(basename "$0"): error: ${message}" >&2 + fi +} + +# Exit codes. +# The code of 1 is returned by ktlint in the event of failure. +declare -ir ERROR_JAVA_NOT_FOUND=2 +declare -ir ERROR_INCOMPATIBLE_BASH_VERSION=3 + +if (( BASH_VERSINFO[0] < 4 )) +then + error "bash version ${BASH_VERSION} is too old, version 4+ is required" + exit ${ERROR_INCOMPATIBLE_BASH_VERSION} +fi + +JAVA_ARGS=() +DIKTAT_JAR="$0" + +# Locates Java, preferring JAVA_HOME. +# +# The 1st variable expansion prevents the "unbound variable" error if JAVA_HOME +# is unset. +function find_java() { + if [[ -n "${JAVA_HOME:=}" ]] + then + case "$(uname -s)" in + 'MINGW32_NT-'* | 'MINGW64_NT-'* | 'MSYS_NT-'* ) + JAVA_HOME="$(cygpath "${JAVA_HOME}")" + ;; + esac + + JAVA="${JAVA_HOME}/bin/java" + # Update the PATH, just in case + export PATH="${JAVA_HOME}/bin:${PATH}" + elif [[ -x "$(which java 2>/dev/null)" ]] + then + JAVA="$(which java 2>/dev/null)" + else + error 'Java is not found' + exit ${ERROR_JAVA_NOT_FOUND} + fi +} + +# On Windows, converts a UNIX path to Windows. Should be invoked before a path +# is passed to any of the Windows-native tools (e.g.: `java`). +# +# On UNIX, just returns the 1st argument. +function native_path() { + case "$(uname -s)" in + 'MINGW32_NT-'* | 'MINGW64_NT-'* | 'MSYS_NT-'* ) + cygpath --windows "$1" + ;; + *) + echo "$1" + ;; + esac +} + +find_java + +JAVA_ARGS+=('-Xmx512m') +JAVA_ARGS+=('-jar' "$(native_path "${DIKTAT_JAR}")") + +exec "${JAVA}" "${JAVA_ARGS[@]}" "$@" diff --git a/diktat-cli/src/test/kotlin/com/saveourtool/diktat/util/CliUtilsKtTest.kt b/diktat-cli/src/test/kotlin/com/saveourtool/diktat/util/CliUtilsKtTest.kt index 1eb1151b59..77c12eee11 100644 --- a/diktat-cli/src/test/kotlin/com/saveourtool/diktat/util/CliUtilsKtTest.kt +++ b/diktat-cli/src/test/kotlin/com/saveourtool/diktat/util/CliUtilsKtTest.kt @@ -1,42 +1,23 @@ package com.saveourtool.diktat.util import org.assertj.core.api.Assertions +import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledOnOs +import org.junit.jupiter.api.condition.OS import org.junit.jupiter.api.io.TempDir import java.io.File import java.nio.file.Path +import java.nio.file.Paths 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()) + fun listByFilesWithLeadingAsterisks() { + Assertions.assertThat(tmpDir.listFiles("**/Test1.kt").toList()) .containsExactlyInAnyOrder( tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test1.kt"), tmpDir.resolve("folder1").resolve("subFolder12").resolve("Test1.kt"), @@ -44,12 +25,9 @@ class CliUtilsKtTest { ) } - @Test - fun walkByGlobWithGlobalPath(@TempDir tmpDir: Path) { - setupHierarchy(tmpDir) - - Assertions.assertThat(tmpDir.walkByGlob("${tmpDir.absolutePathString()}${File.separator}**${File.separator}Test2.kt").toList()) + fun listByFilesWithGlobalPath() { + Assertions.assertThat(tmpDir.listFiles("${tmpDir.absolutePathString()}${File.separator}**${File.separator}Test2.kt").toList()) .containsExactlyInAnyOrder( tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test2.kt"), tmpDir.resolve("folder2").resolve("Test2.kt"), @@ -57,10 +35,17 @@ class CliUtilsKtTest { } @Test - fun walkByGlobWithRelativePath(@TempDir tmpDir: Path) { - setupHierarchy(tmpDir) + fun listByFilesWithGlobalPattern() { + Assertions.assertThat(tmpDir.resolve("folder2").listFiles("${tmpDir.absolutePathString()}${File.separator}**${File.separator}Test2.kt").toList()) + .containsExactlyInAnyOrder( + tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test2.kt"), + tmpDir.resolve("folder2").resolve("Test2.kt"), + ) + } - Assertions.assertThat(tmpDir.walkByGlob("folder1/subFolder11/*.kt").toList()) + @Test + fun listByFilesWithRelativePath() { + Assertions.assertThat(tmpDir.listFiles("folder1/subFolder11/*.kt").toList()) .containsExactlyInAnyOrder( tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test1.kt"), tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test2.kt"), @@ -68,14 +53,87 @@ class CliUtilsKtTest { } @Test - fun walkByGlobWithEmptyResult(@TempDir tmpDir: Path) { - setupHierarchy(tmpDir) + @EnabledOnOs(OS.WINDOWS) + fun listByFilesWithRelativePathWindows() { + Assertions.assertThat(tmpDir.listFiles("folder1\\subFolder11\\*.kt").toList()) + .containsExactlyInAnyOrder( + tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test1.kt"), + tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test2.kt"), + ) + } - Assertions.assertThat(tmpDir.walkByGlob("**/*.kts").toList()) + @Test + fun listByFilesWithEmptyResult() { + Assertions.assertThat(tmpDir.listFiles("**/*.kts").toList()) .isEmpty() } + @Test + fun listByFilesWithParentFolder() { + Assertions.assertThat(tmpDir.resolve("folder1").listFiles("../*/*.kt").toList()) + .containsExactlyInAnyOrder( + tmpDir.resolve("folder2").resolve("Test1.kt"), + tmpDir.resolve("folder2").resolve("Test2.kt"), + tmpDir.resolve("folder2").resolve("Test3.kt"), + ) + } + + @Test + fun listByFilesWithFolder() { + Assertions.assertThat(tmpDir.listFiles("folder2").toList()) + .containsExactlyInAnyOrder( + tmpDir.resolve("folder2").resolve("Test1.kt"), + tmpDir.resolve("folder2").resolve("Test2.kt"), + tmpDir.resolve("folder2").resolve("Test3.kt"), + ) + + + Assertions.assertThat(tmpDir.listFiles("folder1").toList()) + .containsExactlyInAnyOrder( + tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test1.kt"), + tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test2.kt"), + tmpDir.resolve("folder1").resolve("subFolder12").resolve("Test1.kt"), + ) + } + + @Test + fun listByFilesWithNegative() { + Assertions.assertThat(tmpDir.listFiles("**/*.kt", "!**/subFolder11/*.kt", "!**/Test3.kt").toList()) + .containsExactlyInAnyOrder( + tmpDir.resolve("folder1").resolve("subFolder12").resolve("Test1.kt"), + tmpDir.resolve("folder2").resolve("Test1.kt"), + tmpDir.resolve("folder2").resolve("Test2.kt"), + ) + } + companion object { + @JvmStatic + @TempDir + internal var tmpDir: Path = Paths.get("/invalid") + + @BeforeAll + @JvmStatic + internal fun setupHierarchy() { + tmpDir.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") + } + } + tmpDir.resolveAndCreateDirectory("folder2") + .also { folder2 -> + folder2.resolveAndCreateFile("Test1.kt") + folder2.resolveAndCreateFile("Test2.kt") + folder2.resolveAndCreateFile("Test3.kt") + } + } + private fun Path.resolveAndCreateDirectory(name: String): Path = resolve(name).also { it.createDirectory() } diff --git a/diktat-gradle-plugin/build.gradle.kts b/diktat-gradle-plugin/build.gradle.kts index 5a39973146..9e535c2b58 100644 --- a/diktat-gradle-plugin/build.gradle.kts +++ b/diktat-gradle-plugin/build.gradle.kts @@ -10,7 +10,7 @@ plugins { id("com.saveourtool.diktat.buildutils.code-quality-convention") id("com.saveourtool.diktat.buildutils.publishing-configuration") id("pl.droidsonroids.jacoco.testkit") version "1.0.12" - id("org.gradle.test-retry") version "1.5.7" + id("org.gradle.test-retry") version "1.5.8" id("com.gradle.plugin-publish") version "1.2.1" alias(libs.plugins.shadow) } diff --git a/diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/DiktatReporterFactoryImpl.kt b/diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/DiktatReporterFactoryImpl.kt index 16d2b9a9c2..c572668674 100644 --- a/diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/DiktatReporterFactoryImpl.kt +++ b/diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/DiktatReporterFactoryImpl.kt @@ -42,13 +42,8 @@ class DiktatReporterFactoryImpl : DiktatReporterFactory { } val opts = if (args is PlainDiktatReporterCreationArguments) { buildMap { - args.colorName?.let { - put("color", true) - put("color_name", it) - } ?: run { - put("color", false) - put("color_name", Color.DARK_GRAY) - } + put("color", args.colorName?.let { true } ?: false) + put("color_name", args.colorName ?: Color.DARK_GRAY) args.groupByFile?.let { put("group_by_file", it) } }.mapValues { it.value.toString() } } else if (args.reporterType == DiktatReporterType.PLAIN) { diff --git a/diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/KtLintUtils.kt b/diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/KtLintUtils.kt index 72bd186c77..8ea8d04a71 100644 --- a/diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/KtLintUtils.kt +++ b/diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/KtLintUtils.kt @@ -22,7 +22,7 @@ import java.io.PrintStream import java.nio.file.Path import kotlin.io.path.invariantSeparatorsPathString -import kotlin.io.path.relativeTo +import kotlin.io.path.relativeToOrSelf private const val CANNOT_BE_AUTOCORRECTED_SUFFIX = " (cannot be auto-corrected)" @@ -92,7 +92,7 @@ fun String.correctErrorDetail(canBeAutoCorrected: Boolean): String = if (canBeAu * @param sourceRootDir * @return relative path to [sourceRootDir] as [String] */ -fun Path.relativePathStringTo(sourceRootDir: Path?): String = (sourceRootDir?.let { relativeTo(it) } ?: this).invariantSeparatorsPathString +fun Path.relativePathStringTo(sourceRootDir: Path?): String = (sourceRootDir?.let { relativeToOrSelf(it) } ?: this).invariantSeparatorsPathString /** * @param out [OutputStream] for [ReporterV2] diff --git a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter1/IdentifierNaming.kt b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter1/IdentifierNaming.kt index 24fe0cfcba..33ac4d29d5 100644 --- a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter1/IdentifierNaming.kt +++ b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter1/IdentifierNaming.kt @@ -152,6 +152,12 @@ class IdentifierNaming(configRules: List) : DiktatRule( ) private fun checkVariableName(node: ASTNode): List { + val configuration = ConstantUpperCaseConfiguration( + configRules.getRuleConfig(CONSTANT_UPPERCASE)?.configuration + ?: emptyMap()) + + val exceptionNames = configuration.exceptionConstNames + // special case for Destructuring declarations that can be treated as parameters in lambda: var namesOfVariables = extractVariableIdentifiers(node) @@ -178,7 +184,7 @@ class IdentifierNaming(configRules: List) : DiktatRule( // check for constant variables - check for val from companion object or on global file level // it should be in UPPER_CASE, no need to raise this warning if it is one-letter variable if (node.isConstant()) { - if (!variableName.text.isUpperSnakeCase() && variableName.text.length > 1) { + if (!exceptionNames.contains(variableName.text) && !variableName.text.isUpperSnakeCase() && variableName.text.length > 1) { CONSTANT_UPPERCASE.warnOnlyOrWarnAndFix( configRules = configRules, emit = emitWarn, @@ -193,7 +199,7 @@ class IdentifierNaming(configRules: List) : DiktatRule( } } else if (variableName.text != "_" && !variableName.text.isLowerCamelCase() && // variable name should be in camel case. The only exception is a list of industry standard variables like i, j, k. - !isPairPropertyBackingField(null, node) + !isPairPropertyBackingField(null, node.psi as? KtProperty) ) { VARIABLE_NAME_INCORRECT_FORMAT.warnOnlyOrWarnAndFix( configRules = configRules, @@ -493,6 +499,10 @@ class IdentifierNaming(configRules: List) : DiktatRule( } ?: Style.SNAKE_CASE } + class ConstantUpperCaseConfiguration(config: Map) : RuleConfiguration(config) { + val exceptionConstNames = config["exceptionConstNames"]?.split(',') ?: emptyList() + } + class BooleanFunctionsConfiguration(config: Map) : RuleConfiguration(config) { /** * A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar". diff --git a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/NewlinesRule.kt b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/NewlinesRule.kt index 26b3df3d21..e69b40a18e 100644 --- a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/NewlinesRule.kt +++ b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/NewlinesRule.kt @@ -158,7 +158,7 @@ class NewlinesRule(configRules: List) : DiktatRule( ) { isDotQuaOrSafeAccessOrPostfixExpression(it) && it.elementType != POSTFIX_EXPRESSION }.reversed() - if (listDot.size > 3) { + if (listDot.size > configuration.maxCallsInOneLine) { val without = listDot.filterIndexed { index, it -> val nodeBeforeDotOrSafeAccess = it.findChildByType(DOT)?.treePrev ?: it.findChildByType(SAFE_ACCESS)?.treePrev val firstElem = it.firstChildNode @@ -510,22 +510,21 @@ class NewlinesRule(configRules: List) : DiktatRule( } } - @Suppress("AVOID_NULL_CHECKS") private fun handleValueParameterList(node: ASTNode, entryType: String) = node .children() .filter { - (it.elementType == COMMA && !it.treeNext.isNewLineNode()) || + val isNewLineNext = it.treeNext?.isNewLineNode() ?: false + val isNewLinePrev = it.treePrev?.isNewLineNode() ?: false + (it.elementType == COMMA && !isNewLineNext) || // Move RPAR to the new line - (it.elementType == RPAR && it.treePrev.elementType != COMMA && !it.treePrev.isNewLineNode()) + (it.elementType == RPAR && it.treePrev?.elementType != COMMA && !isNewLinePrev) } .toList() .takeIf { it.isNotEmpty() } ?.let { invalidCommas -> - val warnText = if (node.getParentIdentifier() != null) { + val warnText = node.getParentIdentifier()?.let { "$entryType should be placed on different lines in declaration of <${node.getParentIdentifier()}>" - } else { - "$entryType should be placed on different lines" - } + } ?: "$entryType should be placed on different lines" WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode, warnText, node.startOffset, node) { invalidCommas.forEach { commaOrRpar -> @@ -535,7 +534,7 @@ class NewlinesRule(configRules: List) : DiktatRule( commaOrRpar.appendNewlineMergingWhiteSpace(nextWhiteSpace, nextWhiteSpace.treeNext) } ?: commaOrRpar.treeNext?.treeParent?.appendNewlineMergingWhiteSpace(nextWhiteSpace, commaOrRpar.treeNext) } else { - commaOrRpar.treeParent.appendNewlineMergingWhiteSpace(nextWhiteSpace, commaOrRpar) + commaOrRpar.treeParent?.appendNewlineMergingWhiteSpace(nextWhiteSpace, commaOrRpar) } } } diff --git a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter4/NullChecksRule.kt b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter4/NullChecksRule.kt index 4a19d0afa5..9d40395f1b 100644 --- a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter4/NullChecksRule.kt +++ b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter4/NullChecksRule.kt @@ -9,6 +9,8 @@ import com.saveourtool.diktat.ruleset.utils.parent import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.BREAK +import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION +import org.jetbrains.kotlin.KtNodeTypes.CLASS_INITIALIZER import org.jetbrains.kotlin.KtNodeTypes.CONDITION import org.jetbrains.kotlin.KtNodeTypes.ELSE import org.jetbrains.kotlin.KtNodeTypes.IF @@ -26,6 +28,9 @@ import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtIfExpression import org.jetbrains.kotlin.psi.KtPsiUtil import org.jetbrains.kotlin.psi.psiUtil.blockExpressionsOrSingle +import org.jetbrains.kotlin.psi.psiUtil.parents + +typealias ThenAndElseLines = Pair?, List?> /** * This rule check and fixes explicit null checks (explicit comparison with `null`) @@ -72,35 +77,68 @@ class NullChecksRule(configRules: List) : DiktatRule( private fun conditionInIfStatement(node: ASTNode) { node.findAllDescendantsWithSpecificType(BINARY_EXPRESSION).forEach { binaryExprNode -> val condition = (binaryExprNode.psi as KtBinaryExpression) + if (isNullCheckBinaryExpression(condition)) { - when (condition.operationToken) { + val isEqualToNull = when (condition.operationToken) { // `==` and `===` comparison can be fixed with `?:` operator - KtTokens.EQEQ, KtTokens.EQEQEQ -> - warnAndFixOnNullCheck( - condition, - isFixable(node, true), - "use '.let/.also/?:/e.t.c' instead of ${condition.text}" - ) { - fixNullInIfCondition(node, condition, true) - } - // `!==` and `!==` comparison can be fixed with `.let/also` operators - KtTokens.EXCLEQ, KtTokens.EXCLEQEQEQ -> - warnAndFixOnNullCheck( - condition, - isFixable(node, false), - "use '.let/.also/?:/e.t.c' instead of ${condition.text}" - ) { - fixNullInIfCondition(node, condition, false) - } + KtTokens.EQEQ, KtTokens.EQEQEQ -> true + // `!=` and `!==` comparison can be fixed with `.let/also` operators + KtTokens.EXCLEQ, KtTokens.EXCLEQEQEQ -> false else -> return } + + val (_, elseCodeLines) = getThenAndElseLines(node, isEqualToNull) + val (numberOfStatementsInElseBlock, isAssignmentInNewElseBlock) = getInfoAboutElseBlock(node, isEqualToNull) + + // if `if-else` block inside `init` or 'run', 'with', 'apply' blocks and there is more than one statement inside 'else' block, then + // we don't have to make any fixes, because this leads to kotlin compilation error after adding 'run' block instead of 'else' block + // read https://youtrack.jetbrains.com/issue/KT-64174 for more information + if (shouldBeWarned(node, elseCodeLines, numberOfStatementsInElseBlock, isAssignmentInNewElseBlock)) { + warnAndFixOnNullCheck( + condition, + canBeAutoFixed(node, isEqualToNull), + "use '.let/.also/?:/e.t.c' instead of ${condition.text}" + ) { + fixNullInIfCondition(node, condition, isEqualToNull) + } + } } } } + /** + * Checks whether it is necessary to warn about null-check + */ + private fun shouldBeWarned( + condition: ASTNode, + elseCodeLines: List?, + numberOfStatementsInElseBlock: Int, + isAssignment: Boolean, + ): Boolean = when { + // else { "null"/empty } -> "" + isNullOrEmptyElseBlock(elseCodeLines) -> true + // else { bar() } -> ?: bar() + isOnlyOneNonAssignmentStatementInElseBlock(numberOfStatementsInElseBlock, isAssignment) -> true + // else { ... } -> ?: run { ... } + else -> isNotInsideWrongBlock(condition) + } + + private fun isOnlyOneNonAssignmentStatementInElseBlock(numberOfStatementsInElseBlock: Int, isAssignment: Boolean) = numberOfStatementsInElseBlock == 1 && !isAssignment + + private fun isNullOrEmptyElseBlock(elseCodeLines: List?) = elseCodeLines == null || elseCodeLines.singleOrNull() == "null" + + private fun isNotInsideWrongBlock(condition: ASTNode): Boolean = condition.parents().none { + it.elementType == CLASS_INITIALIZER || + (it.elementType == CALL_EXPRESSION && + it.findChildByType(REFERENCE_EXPRESSION)?.text in listOf("run", "with", "apply")) + } + + /** + * Checks whether null-check can be auto fixed + */ @Suppress("UnsafeCallOnNullableType") - private fun isFixable(condition: ASTNode, - isEqualToNull: Boolean): Boolean { + private fun canBeAutoFixed(condition: ASTNode, + isEqualToNull: Boolean): Boolean { // Handle cases with `break` word in blocks val typePair = if (isEqualToNull) { (ELSE to THEN) @@ -126,6 +164,19 @@ class NullChecksRule(configRules: List) : DiktatRule( isEqualToNull: Boolean ) { val variableName = binaryExpression.left!!.text + val (thenCodeLines, elseCodeLines) = getThenAndElseLines(condition, isEqualToNull) + val (numberOfStatementsInElseBlock, isAssignmentInNewElseBlock) = getInfoAboutElseBlock(condition, isEqualToNull) + + val elseEditedCodeLines = getEditedElseCodeLines(elseCodeLines, numberOfStatementsInElseBlock, isAssignmentInNewElseBlock) + val thenEditedCodeLines = getEditedThenCodeLines(variableName, thenCodeLines, elseEditedCodeLines) + + val newTextForReplacement = "$thenEditedCodeLines $elseEditedCodeLines" + val newNodeForReplacement = KotlinParser().createNode(newTextForReplacement) + val ifNode = condition.treeParent + ifNode.treeParent.replaceChild(ifNode, newNodeForReplacement) + } + + private fun getThenAndElseLines(condition: ASTNode, isEqualToNull: Boolean): ThenAndElseLines { val thenFromExistingCode = condition.extractLinesFromBlock(THEN) val elseFromExistingCode = condition.extractLinesFromBlock(ELSE) @@ -141,7 +192,11 @@ class NullChecksRule(configRules: List) : DiktatRule( elseFromExistingCode } - val (numberOfStatementsInElseBlock, isAssignmentInNewElseBlock) = (condition.treeParent.psi as? KtIfExpression) + return Pair(thenCodeLines, elseCodeLines) + } + + private fun getInfoAboutElseBlock(condition: ASTNode, isEqualToNull: Boolean) = + ((condition.treeParent.psi as? KtIfExpression) ?.let { if (isEqualToNull) { it.then @@ -155,28 +210,20 @@ class NullChecksRule(configRules: List) : DiktatRule( KtPsiUtil.isAssignment(element) } } - ?: Pair(0, false) - - val elseEditedCodeLines = getEditedElseCodeLines(elseCodeLines, numberOfStatementsInElseBlock, isAssignmentInNewElseBlock) - val thenEditedCodeLines = getEditedThenCodeLines(variableName, thenCodeLines, elseEditedCodeLines) - - val text = "$thenEditedCodeLines $elseEditedCodeLines" - val tree = KotlinParser().createNode(text) - val ifNode = condition.treeParent - ifNode.treeParent.replaceChild(ifNode, tree) - } + ?: Pair(0, false)) + @Suppress("UnsafeCallOnNullableType") private fun getEditedElseCodeLines( elseCodeLines: List?, numberOfStatementsInElseBlock: Int, isAssignment: Boolean, ): String = when { // else { "null"/empty } -> "" - elseCodeLines == null || elseCodeLines.singleOrNull() == "null" -> "" + isNullOrEmptyElseBlock(elseCodeLines) -> "" // else { bar() } -> ?: bar() - numberOfStatementsInElseBlock == 1 && !isAssignment -> "?: ${elseCodeLines.joinToString(postfix = "\n", separator = "\n")}" + isOnlyOneNonAssignmentStatementInElseBlock(numberOfStatementsInElseBlock, isAssignment) -> "?: ${elseCodeLines!!.joinToString(postfix = "\n", separator = "\n")}" // else { ... } -> ?: run { ... } - else -> getDefaultCaseElseCodeLines(elseCodeLines) + else -> getDefaultCaseElseCodeLines(elseCodeLines!!) } @Suppress("UnsafeCallOnNullableType") diff --git a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/CustomGetterSetterRule.kt b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/CustomGetterSetterRule.kt index f63f70670d..745642539c 100644 --- a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/CustomGetterSetterRule.kt +++ b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/CustomGetterSetterRule.kt @@ -12,6 +12,7 @@ import org.jetbrains.kotlin.lexer.KtTokens.GET_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.OVERRIDE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PRIVATE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.SET_KEYWORD +import org.jetbrains.kotlin.psi.KtProperty /** * Inspection that checks that no custom getters and setters are used for properties. @@ -34,7 +35,7 @@ class CustomGetterSetterRule(configRules: List) : DiktatRule( val isOverrideGetter = node.treeParent.getFirstChildWithType(MODIFIER_LIST)?.hasAnyChildOfTypes(OVERRIDE_KEYWORD) ?: false // find matching node - if (isPairPropertyBackingField(node.treeParent, null)) { + if (isPairPropertyBackingField(node.treeParent?.psi as? KtProperty, null)) { return } diff --git a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/AstNodeUtils.kt b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/AstNodeUtils.kt index 460bc94039..9742ffc9f5 100644 --- a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/AstNodeUtils.kt +++ b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/AstNodeUtils.kt @@ -35,6 +35,7 @@ import org.jetbrains.kotlin.KtNodeTypes.PARENTHESIZED import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.TYPE_PARAMETER_LIST +import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.builtins.PrimitiveType import org.jetbrains.kotlin.com.intellij.lang.ASTNode @@ -45,6 +46,8 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet +import org.jetbrains.kotlin.js.translate.declaration.hasCustomGetter +import org.jetbrains.kotlin.js.translate.declaration.hasCustomSetter import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.ANDAND @@ -69,8 +72,15 @@ import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtIfExpression +import org.jetbrains.kotlin.psi.KtNullableType import org.jetbrains.kotlin.psi.KtParameterList +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtTypeReference +import org.jetbrains.kotlin.psi.KtUserType import org.jetbrains.kotlin.psi.psiUtil.children +import org.jetbrains.kotlin.psi.psiUtil.getChildOfType +import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType +import org.jetbrains.kotlin.psi.psiUtil.isPrivate import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.siblings import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType @@ -1186,28 +1196,33 @@ fun doesLambdaContainIt(lambdaNode: ASTNode): Boolean { * @return true if node has a pair */ @Suppress("CyclomaticComplexMethod") -internal fun isPairPropertyBackingField(propertyNode: ASTNode?, backingFieldNode: ASTNode?): Boolean { - val node = propertyNode ?: backingFieldNode - val propertyListNullable = (node?.treeParent?.getAllChildrenWithType(PROPERTY)) ?: return false - val propertyList: List = propertyListNullable - val nodeType = node.getFirstChildWithType(KtNodeTypes.TYPE_REFERENCE) - val nodeNullableType = nodeType?.getFirstChildWithType(KtNodeTypes.NULLABLE_TYPE) - val nodeName = node.getFirstChildWithType(IDENTIFIER)?.text +internal fun isPairPropertyBackingField(propertyNode: KtProperty?, backingFieldNode: KtProperty?): Boolean { + val node = (propertyNode ?: backingFieldNode) + val propertyList = (node?.parent?.getChildrenOfType()) ?: return false + val nodeType: KtTypeReference? = node.getChildOfType() + val nodeNullableType = nodeType?.getChildOfType() + val nodeName = node.name val matchingNode = propertyList.find {pairNode -> - val propertyType = pairNode.getFirstChildWithType(KtNodeTypes.TYPE_REFERENCE) + val propertyType: KtTypeReference? = pairNode.getChildOfType() // check that property and backing field has same type val sameType = nodeType?.text == propertyType?.text + // check that property USER_TYPE is same as backing field NULLABLE_TYPE - val sameTypeWithNullable = propertyType?.getFirstChildWithType(KtNodeTypes.USER_TYPE)?.text == - nodeNullableType?.getFirstChildWithType(KtNodeTypes.USER_TYPE)?.text + val sameTypeWithNullable = propertyType?.getChildOfType()?.text == + nodeNullableType?.getChildOfType()?.text + // check matching names - val propertyName = pairNode.getFirstChildWithType(IDENTIFIER)?.text + val propertyName = pairNode.name val matchingNames = propertyNode?.let { nodeName == propertyName?.drop(1) } ?: run { nodeName?.drop(1) == propertyName } val isPrivate = propertyNode?.let { pairNode.isPrivate() } ?: run { node.isPrivate() } - val noSetterGetterBackingField = propertyNode?.let { !pairNode.hasSetterOrGetter() } ?: run { !node.hasSetterOrGetter() } - val hasSetterOrGetterProperty = propertyNode?.let { node.hasSetterOrGetter() } ?: run { pairNode.hasSetterOrGetter() } + val noSetterGetterBackingField = propertyNode?.let { !(pairNode.hasCustomSetter() || pairNode.hasCustomGetter()) } ?: run { + !(node.hasCustomSetter() || node.hasCustomGetter()) + } + val hasSetterOrGetterProperty = propertyNode?.let { node.hasCustomSetter() || node.hasCustomGetter() } ?: run { + pairNode.hasCustomSetter() || pairNode.hasCustomGetter() + } matchingNames && (sameType || sameTypeWithNullable) && isPrivate && noSetterGetterBackingField && hasSetterOrGetterProperty diff --git a/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/IdentifierNamingWarnTest.kt b/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/IdentifierNamingWarnTest.kt index 180f0e64e8..f41a6e3cd0 100644 --- a/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/IdentifierNamingWarnTest.kt +++ b/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/IdentifierNamingWarnTest.kt @@ -41,6 +41,11 @@ class IdentifierNamingWarnTest : LintTestBase(::IdentifierNaming) { RulesConfig(FUNCTION_BOOLEAN_PREFIX.name, true, mapOf("allowedPrefixes" to "equals, equivalent, foo")) ) + private val rulesConfigConstantUpperCase = listOf( + RulesConfig(CONSTANT_UPPERCASE.name, true, mapOf( + "exceptionConstNames" to "serialVersionUID" + )) + ) // ======== checks for generics ======== @Test @@ -195,6 +200,37 @@ class IdentifierNamingWarnTest : LintTestBase(::IdentifierNaming) { ) } + @Test + @Tag(WarningNames.CONSTANT_UPPERCASE) + fun `serialVersionUID should be ignored`() { + lintMethod( + """ + class TestSerializableClass() : Serializable { + companion object { + private const val serialVersionUID: Long = -1 + } + } + """.trimIndent(), + rulesConfigList = rulesConfigConstantUpperCase + ) + } + + @Test + @Tag(WarningNames.CONSTANT_UPPERCASE) + fun `should trigger when the name is not exception`() { + val code = + """ + class TestSerializableClass() : Serializable { + companion object { + private const val serialVersion: Long = -1 + } + } + """.trimIndent() + lintMethod(code, + DiktatError(3, 27, ruleId, "${CONSTANT_UPPERCASE.warnText()} serialVersion", true) + ) + } + @Test @Tags(Tag(WarningNames.IDENTIFIER_LENGTH), Tag(WarningNames.VARIABLE_NAME_INCORRECT)) fun `check variable length (check - negative)`() { diff --git a/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/files/NewlinesRuleWarnTest.kt b/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/files/NewlinesRuleWarnTest.kt index 8bacd5c5ae..cac499b664 100644 --- a/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/files/NewlinesRuleWarnTest.kt +++ b/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/files/NewlinesRuleWarnTest.kt @@ -27,6 +27,10 @@ class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) { RulesConfig(WRONG_NEWLINES.name, true, mapOf("maxCallsInOneLine" to "1")) ) + private val rulesConfigListLong: List = listOf( + RulesConfig(WRONG_NEWLINES.name, true, + mapOf("maxCallsInOneLine" to "10")) + ) private val ruleId = "$DIKTAT_RULE_SET_ID:${NewlinesRule.NAME_ID}" private val dotQuaOrSafeAccessOrPostfixExpression = "${WRONG_NEWLINES.warnText()} wrong split long `dot qualified expression` or `safe access expression`" private val shouldBreakAfter = "${WRONG_NEWLINES.warnText()} should break a line after and not before" @@ -682,7 +686,8 @@ class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) { | } |} """.trimMargin(), - DiktatError(19, 20, ruleId, "${WRONG_NEWLINES.warnText()} should follow functional style at .", true), + DiktatError(16, 9, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), + DiktatError(19, 20, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigListShort ) } @@ -829,9 +834,9 @@ class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) { |} """.trimMargin(), DiktatError(2, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), - DiktatError(3, 22, ruleId, "${WRONG_NEWLINES.warnText()} should follow functional style at .", true), + DiktatError(3, 22, ruleId, "$functionalStyleWarn .", true), DiktatError(13, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), - DiktatError(13, 23, ruleId, "${WRONG_NEWLINES.warnText()} should follow functional style at .", true) + DiktatError(13, 23, ruleId, "$functionalStyleWarn .", true) ) } @@ -893,7 +898,9 @@ class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) { | .few() |} """.trimMargin(), + DiktatError(2, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(4, 10, ruleId, "$functionalStyleWarn .", true), + DiktatError(8, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(9, 11, ruleId, "$functionalStyleWarn .", true), DiktatError(9, 17, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigListShort @@ -930,7 +937,9 @@ class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) { |} """.trimMargin(), DiktatError(2, 7, ruleId, "$functionalStyleWarn .", true), + DiktatError(15, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(16, 10, ruleId, "$functionalStyleWarn .", true), + DiktatError(21, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(21, 7, ruleId, "$functionalStyleWarn .", true), DiktatError(22, 10, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigListShort @@ -1051,6 +1060,7 @@ class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) { """.trimMargin(), DiktatError(2, 14, ruleId, "$functionalStyleWarn ?:", true), DiktatError(4, 8, ruleId, "$functionalStyleWarn ?:", true), + DiktatError(4, 11, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(4, 22, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigListShort ) @@ -1066,7 +1076,9 @@ class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) { | a().b.c()!! |} """.trimMargin(), + DiktatError(2, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(2, 11, ruleId, "$functionalStyleWarn .", true), + DiktatError(3, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(3, 9, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigListShort ) @@ -1092,6 +1104,7 @@ class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) { | .qwe() |} """.trimMargin(), + DiktatError(7, 11, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(7, 22, ruleId, "$functionalStyleWarn .", true), DiktatError(9, 15, ruleId, "$functionalStyleWarn ?:", true), DiktatError(10, 16, ruleId, "$functionalStyleWarn ?:", true), @@ -1152,12 +1165,27 @@ class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) { | if(a().b().c()) {} |} """.trimMargin(), + DiktatError(2, 7, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(2, 14, ruleId, "${COMPLEX_EXPRESSION.warnText()} .", false), DiktatError(2, 14, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigListShort ) } + @Test + @Tag(WarningNames.WRONG_NEWLINES) + fun `shouldn't fall with NPE`() { + lintMethod( + """ + |val TEST_FUNC = { _: Int, _: Set, _: Int, -> + |} + """.trimMargin(), + DiktatError(1, 19, ruleId, "${WRONG_NEWLINES.warnText()} " + + "first parameter should be placed on a separate line or all other parameters should be aligned with it in declaration of ", true), + DiktatError(1, 19, ruleId, "${WRONG_NEWLINES.warnText()} value parameters should be placed on different lines", true), + ) + } + @Test @Tag(WarningNames.WRONG_NEWLINES) fun `not complaining on fun without return type`() { @@ -1181,4 +1209,17 @@ class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) { fileName = "build.gradle.kts" ) } + + @Test + @Tags(Tag(WarningNames.WRONG_NEWLINES), Tag(WarningNames.COMPLEX_EXPRESSION)) + fun `shouldn't trigger on allowed many calls in one line`() { + lintMethod( + """ + |fun foo() { + | if(a().b().c().d().e()) {} + |} + """.trimMargin(), + rulesConfigList = rulesConfigListLong + ) + } } diff --git a/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/NullChecksRuleWarnTest.kt b/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/NullChecksRuleWarnTest.kt index e510491694..43122523ba 100644 --- a/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/NullChecksRuleWarnTest.kt +++ b/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/NullChecksRuleWarnTest.kt @@ -215,4 +215,109 @@ class NullChecksRuleWarnTest : LintTestBase(::NullChecksRule) { """.trimMargin() ) } + + @Test + @Tag(WarningNames.AVOID_NULL_CHECKS) + fun `don't trigger inside 'init' block when more than one statement in 'else' block`() { + lintMethod( + """ + |class Demo { + | val one: Int + | val two: String + | + | init { + | val number = get() + | if (number != null) { + | one = number.toInt() + | two = number + | } else { + | one = 0 + | two = "0" + | } + | } + | + | private fun get(): String? = if (Math.random() > 0.5) { "1" } else { null } + |} + """.trimMargin() + ) + } + + @Test + @Tag(WarningNames.AVOID_NULL_CHECKS) + fun `trigger inside 'init' block when only one statement in 'else' block`() { + lintMethod( + """ + |class Demo { + | val one: Int = 0 + | val two: String = "" + | + | init { + | val number = get() + | if (number != null) { + | print(number + 1) + | } else { + | print(null) + | } + | } + | + | private fun get(): String? = if (Math.random() > 0.5) { "1" } else { null } + |} + """.trimMargin(), + DiktatError(7, 13, ruleId, Warnings.AVOID_NULL_CHECKS.warnText() + + " use '.let/.also/?:/e.t.c' instead of number != null", true), + ) + } + + @Test + @Tag(WarningNames.AVOID_NULL_CHECKS) + fun `trigger inside 'init' block when no 'else' block`() { + lintMethod( + """ + |class Demo { + | val one: Int = 0 + | val two: String = "" + | + | init { + | val number = get() + | if (number != null) { + | print(number) + | } + | } + | + | private fun get(): String? = if (Math.random() > 0.5) { "1" } else { null } + |} + """.trimMargin(), + DiktatError(7, 13, ruleId, Warnings.AVOID_NULL_CHECKS.warnText() + + " use '.let/.also/?:/e.t.c' instead of number != null", true), + ) + } + + @Test + @Tag(WarningNames.AVOID_NULL_CHECKS) + fun `don't trigger inside 'run', 'with', 'apply' scope functions when more than one statement in 'else' block`() { + lintMethod( + """ + |class Demo { + | + | private fun set() { + | val one: Int + | val two: String + | + | run { + | val number: String? = get() + | if (number != null) { + | one = number.toInt() + | two = number + | } else { + | one = 0 + | two = "0" + | } + | } + | } + | + | private fun get(): String? = if (Math.random() > 0.5) { "1" } else { null } + |} + """.trimMargin() + ) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d15a676109..d9280b054d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ dokka = "1.9.10" gradle-nexus-publish-plugin = "1.3.0" jacoco = "0.8.8" # maven -maven-api = "3.9.5" +maven-api = "3.9.6" maven-plugin-tools = "3.8.1" maven-plugin-testing-harness = "3.3.0" plexus = "2.1.0" @@ -21,7 +21,7 @@ plexus = "2.1.0" jbool = "1.24" kotlin-logging = "5.1.1" log4j2 = "2.22.0" -kaml = "0.55.0" +kaml = "0.56.0" sarif4k = "0.5.0" jupiter-itf-extension = "0.12.0" @@ -33,9 +33,9 @@ gradle-shadow = "8.1.1" jetbrains-annotations = "24.1.0" kotlinx-coroutines = "1.7.3" assertj = "3.24.2" -diktat = "2.0.0-rc.6" +diktat = "2.0.0-rc.8" reckon = "0.18.1" -spotless = "6.23.2" +spotless = "6.23.3" download = "5.5.0" [plugins] @@ -46,7 +46,7 @@ kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization" kotlin-plugin-jpa = { id = "org.jetbrains.kotlin.plugin.jpa", version.ref = "kotlin" } kotlin-plugin-allopen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" } kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin-ksp" } -talaiot-base = { id = "io.github.cdsap.talaiot.plugin.base", version = "2.0.2" } +talaiot-base = { id = "io.github.cdsap.talaiot.plugin.base", version = "2.0.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" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49..d64cd49177 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fa8f862f7..1af9e0930b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/info/gradle/wrapper/gradle-wrapper.properties b/info/gradle/wrapper/gradle-wrapper.properties index e411586a54..a595206642 100644 --- a/info/gradle/wrapper/gradle-wrapper.properties +++ b/info/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index e19f7f930f..69c8621763 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -40,7 +40,7 @@ pluginManagement { } plugins { - id("com.gradle.enterprise") version "3.15.1" + id("com.gradle.enterprise") version "3.16" // starting from Gradle 8, it's needed to configure a repo from which to take Java for a toolchain id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" }