Skip to content

Commit

Permalink
Support ANT-like glob patterns (#1856)
Browse files Browse the repository at this point in the history
- supported folders as input pattern
- supported `../` in glob pattern
- supported `!` as exclude pattern
- supported absolute globs
  • Loading branch information
nulls authored Dec 13, 2023
1 parent c20f2d5 commit c047d84
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -96,16 +95,8 @@ data class DiktatProperties(
)
}

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())
.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 {
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

0 comments on commit c047d84

Please sign in to comment.