Skip to content

Commit

Permalink
When an (existing) directory (without glob pattern) is specified then…
Browse files Browse the repository at this point in the history
… only process files with default kotlin extensions in that directory or its subdirectories. This is equivalent to execute ktlint in that directory without specifying any pattern.

Closes pinterest#917
  • Loading branch information
paul-dingemans committed Jul 3, 2022
1 parent d3b4b42 commit 189fbac
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 53 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).

* Fix cli argument "--disabled_rules" ([#1520](https://github.com/pinterest/ktlint/issue/1520)).
* When a glob is specified then ensure that it matches files in the current directory and not only in subdirectories of the current directory ([#1533](https://github.com/pinterest/ktlint/issue/1533)).
* Execute `ktlint` cli on default kotlin extensions only when an (existing) path to a directory is given. ([#917](https://github.com/pinterest/ktlint/issue/917)).


### Changed
Expand Down
103 changes: 68 additions & 35 deletions ktlint/src/main/kotlin/com/pinterest/ktlint/internal/FileUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,35 @@ import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
import kotlin.io.path.isDirectory
import kotlin.system.exitProcess
import mu.KotlinLogging
import org.jetbrains.kotlin.util.prefixIfNot

private val logger = KotlinLogging.logger {}.initKtLintKLogger()

internal val workDir: String = File(".").canonicalPath

private val tildeRegex = Regex("^(!)?~")
private const val NEGATION_PREFIX = "!"

private val os = System.getProperty("os.name")
private val userHome = System.getProperty("user.home")

internal val defaultPatterns = setOf(
"**$globSeparator*.kt",
"**$globSeparator*.kts"
)
private val defaultKotlinFileExtensions = setOf("kt", "kts")
internal val defaultPatterns = defaultKotlinFileExtensions.map { "**$globSeparator*.$it" }

/**
* Transform the [patterns] to a sequence of files. Each element in [patterns] can be a glob, a file or directory path
* relative to the [rootDir] or a absolute file or directory path.
*/
internal fun FileSystem.fileSequence(
globs: List<String>,
patterns: List<String>,
rootDir: Path = Paths.get(".").toAbsolutePath().normalize()
): Sequence<Path> {
val result = mutableListOf<Path>()

val (existingFiles, actualGlobs) = globs.partition {
val (existingFiles, patternsExclusiveExistingFiles) = patterns.partition {
try {
Files.isRegularFile(rootDir.resolve(it))
} catch (e: InvalidPathException) {
Expand All @@ -48,29 +53,29 @@ internal fun FileSystem.fileSequence(
existingFiles.mapTo(result) { rootDir.resolve(it) }

// Return early and don't traverse the file system if all the input globs are absolute paths
if (result.isNotEmpty() && actualGlobs.isEmpty()) {
if (result.isNotEmpty() && patternsExclusiveExistingFiles.isEmpty()) {
return result.asSequence()
}

val pathMatchers = if (actualGlobs.isEmpty()) {
val globs = expand(patternsExclusiveExistingFiles, rootDir)

val pathMatchers = if (globs.isEmpty()) {
defaultPatterns
.map { getPathMatcher("glob:$it") }
.toSet()
} else {
actualGlobs
.filterNot { it.startsWith("!") }
.map {
getPathMatcher(toGlob(it))
}
globs
.filterNot { it.startsWith(NEGATION_PREFIX) }
.map { getPathMatcher(it) }
}

val negatedPathMatchers = if (actualGlobs.isEmpty()) {
val negatedPathMatchers = if (globs.isEmpty()) {
emptySet()
} else {
actualGlobs
.filter { it.startsWith("!") }
globs
.filter { it.startsWith(NEGATION_PREFIX) }
.map {
getPathMatcher(toGlob(it.removePrefix("!")))
getPathMatcher(it.removePrefix(NEGATION_PREFIX))
}
}

Expand Down Expand Up @@ -105,27 +110,49 @@ internal fun FileSystem.fileSequence(
return result.asSequence()
}

private fun FileSystem.isGlobAbsolutePath(glob: String): Boolean {
val rootDirs = rootDirectories.map { it.toString() }
return rootDirs.any { glob.removePrefix("!").startsWith(it) }
}

internal fun FileSystem.toGlob(pattern: String): String {
val expandedPath = if (os.startsWith("windows", true)) {
// Windows sometimes inserts `~` into paths when using short directory names notation, e.g. `C:\Users\USERNA~1\Documents
pattern
private fun FileSystem.expand(
patterns: List<String>,
rootDir: Path
) =
patterns
.map { it.expandTildeToFullPath() }
.map { it.replace(File.separator, globSeparator) }
.flatMap { path -> toGlob(path, rootDir) }

private fun FileSystem.toGlob(
path: String,
rootDir: Path
): List<String> {
val negation = if (path.startsWith(NEGATION_PREFIX)) {
NEGATION_PREFIX
} else {
expandTilde(pattern)
}.replace(File.separator, globSeparator)

val fullPath = if (isGlobAbsolutePath(expandedPath)) {
expandedPath
""
}
val pathWithoutNegationPrefix = path.removePrefix(NEGATION_PREFIX)
val resolvedPath = rootDir.resolve(pathWithoutNegationPrefix)
val expandedGlobs = if (resolvedPath.isDirectory()) {
getDefaultPatternsForPath(resolvedPath)
} else if (isGlobAbsolutePath(pathWithoutNegationPrefix)) {
listOf(pathWithoutNegationPrefix)
} else {
expandedPath.prefixIfNot("**$globSeparator")
listOf(pathWithoutNegationPrefix.prefixIfNot("**$globSeparator"))
}
return "glob:$fullPath"
return expandedGlobs.map { "${negation}glob:$it" }
}

private fun getDefaultPatternsForPath(path: Path?) = defaultKotlinFileExtensions
.flatMap {
listOf(
"$path$globSeparator*.$it",
"$path$globSeparator**$globSeparator*.$it"
)
}

private fun FileSystem.isGlobAbsolutePath(glob: String) =
rootDirectories
.map { it.toString() }
.any { glob.startsWith(it) }

private val globSeparator: String get() =
when {
os.startsWith("windows", ignoreCase = true) -> "\\\\"
Expand All @@ -138,7 +165,7 @@ private val globSeparator: String get() =
internal typealias JarFiles = List<String>

internal fun JarFiles.toFilesURIList() = map {
val jarFile = File(expandTilde(it))
val jarFile = File(it.expandTildeToFullPath())
if (!jarFile.exists()) {
logger.error { "File $it does not exist" }
exitProcess(1)
Expand All @@ -148,7 +175,13 @@ internal fun JarFiles.toFilesURIList() = map {

// a complete solution would be to implement https://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html
// this implementation takes care only of the most commonly used case (~/)
private fun expandTilde(path: String): String = path.replaceFirst(tildeRegex, userHome)
private fun String.expandTildeToFullPath(): String =
if (os.startsWith("windows", true)) {
// Windows sometimes inserts `~` into paths when using short directory names notation, e.g. `C:\Users\USERNA~1\Documents
this
} else {
replaceFirst(tildeRegex, userHome)
}

internal fun File.location(
relative: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource

/**
* Tests for [fileSequence] method.
Expand Down Expand Up @@ -80,7 +82,7 @@ internal class FileUtilsFileSequenceTest {
}

@Test
fun `Given some patterns and no workdir then ignore all files in hidden directories`() {
fun `Given some globs and no workdir then ignore all files in hidden directories`() {
val foundFiles = getFiles(
patterns = listOf(
"project1/**/*.kt".normalizeGlob(),
Expand All @@ -103,7 +105,7 @@ internal class FileUtilsFileSequenceTest {
@Nested
inner class NegatePattern {
@Test
fun `Given some patterns including a negate pattern and no workdir then select all files except files in the negate pattern`() {
fun `Given some globs including a negate pattern and no workdir then select all files except files in the negate pattern`() {
val foundFiles = getFiles(
patterns = listOf(
"project1/src/**/*.kt".normalizeGlob(),
Expand All @@ -117,7 +119,7 @@ internal class FileUtilsFileSequenceTest {
}

@Test
fun `Given the Windows OS and some unescaped patterns including a negate pattern and no workdir then ignore all files in the negate pattern`() {
fun `Given the Windows OS and some unescaped globs including a negate pattern and no workdir then ignore all files in the negate pattern`() {
assumeTrue(
System
.getProperty("os.name")
Expand All @@ -139,7 +141,7 @@ internal class FileUtilsFileSequenceTest {
}

@Test
fun `Given a pattern and a workdir then find all files in that workdir and all its sub directories that match the pattern`() {
fun `Given a glob and a workdir then find all files in that workdir and all its sub directories that match the pattern`() {
val foundFiles = getFiles(
patterns = listOf(
"**/main/**/*.kt".normalizeGlob()
Expand Down Expand Up @@ -196,26 +198,38 @@ internal class FileUtilsFileSequenceTest {
)
}

@Test
fun `transforming globs with leading tilde`() {
assumeTrue(
System
.getProperty("os.name")
.lowercase(Locale.getDefault())
.startsWith("linux")
)
@ParameterizedTest(name = "Pattern: {0}")
@ValueSource(
strings = [
"~/project/src/main/kotlin/One.kt",
"~/project/src/main/kotlin/*.kt",
"~/project/src/main/kotlin/",
"~/project/src/main/kotlin",
"~/project/src/main/**/*.kt"
]
)
fun `Given a non-Windows OS a pattern that starts with a tilde then transform the globs to the user home directory`(
pattern: String
) {
val os = System
.getProperty("os.name")
.lowercase()
assumeTrue(os != "windows")

val glob = tempFileSystem.toGlob(
"~/project/src/main/kotlin/One.kt"
)
val homeDir = System.getProperty("user.home")
assertThat(glob).isEqualTo(
"glob:$homeDir/project/src/main/kotlin/One.kt"
tempFileSystem.createFile("$homeDir/project/src/main/kotlin/One.kt")

val foundFiles = getFiles(
patterns = listOf(pattern)
)

assertThat(foundFiles).containsExactlyInAnyOrder(
"$homeDir/project/src/main/kotlin/One.kt"
)
}

@Test
fun `Given a pattern containing ** and a workdir without subdirectories then find all files in that workdir`() {
fun `Given a pattern containing a double star and a workdir without subdirectories then find all files in that workdir`() {
val foundFiles = getFiles(
patterns = listOf(
"**/*.kt".normalizeGlob()
Expand All @@ -229,6 +243,21 @@ internal class FileUtilsFileSequenceTest {
)
}

@Test
fun `Given an (relative) directory path (but not a glob) from the workdir then find all files in that workdir and it subdirectories having the default kotlin extensions`() {
val foundFiles = getFiles(
patterns = listOf("src/main/kotlin".normalizeGlob()),
rootDir = tempFileSystem.getPath("${rootDir}project1".normalizePath())
)

assertThat(foundFiles).containsExactlyInAnyOrder(
ktFile1InProjectSubDirectory,
ktFile2InProjectSubDirectory
).doesNotContain(
javaFileInProjectSubDirectory
)
}

private fun String.normalizePath() = replace('/', File.separatorChar)
private fun String.normalizeGlob(): String = replace("/", rawGlobSeparator)

Expand Down

0 comments on commit 189fbac

Please sign in to comment.