Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ANT-like glob patterns #1856

Merged
merged 12 commits into from
Dec 13, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
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
Expand Down Expand Up @@ -96,16 +95,8 @@
)
}

private fun getFiles(sourceRootDir: Path): Collection<Path> = patterns
.asSequence()
.flatMap { pattern ->
pattern.tryToPathIfExists()?.let { sequenceOf(it) }
?: sourceRootDir.walkByGlob(pattern)
}
private fun getFiles(sourceRootDir: Path): Collection<Path> = sourceRootDir.listFiles(patterns = patterns.toTypedArray())

Check warning on line 98 in diktat-cli/src/main/kotlin/com/saveourtool/diktat/cli/DiktatProperties.kt

View check run for this annotation

Codecov / codecov/patch

diktat-cli/src/main/kotlin/com/saveourtool/diktat/cli/DiktatProperties.kt#L98

Added line #L98 was not covered by tests
.filter { file -> file.isKotlinCodeOrScript() }
.map { it.normalize() }
.map { it.toAbsolutePath() }
.distinct()
.toList()

private fun getReporterOutput(): OutputStream? = output
Expand Down
77 changes: 60 additions & 17 deletions diktat-cli/src/main/kotlin/com/saveourtool/diktat/util/CliUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,96 @@
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<String> = FileSystems.getDefault()
.rootDirectories
.asSequence()
.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<Path> {
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<String>): Sequence<Path> = 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.
*
* @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<Path> = fileSystem.globMatcher(glob)
.let { matcher ->
this.walk(PathWalkOption.INCLUDE_DIRECTORIES)
.filter { matcher.matches(it) }
private fun Path.walkByGlob(glob: String): Sequence<Path> = 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<String, Path> = when {
Fixed Show fixed Hide fixed
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) }
Original file line number Diff line number Diff line change
@@ -1,81 +1,139 @@
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"),
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())
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"),
)
}

@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"),
)
}

@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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)"

Expand Down Expand Up @@ -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]
Expand Down
Loading