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/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-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]