Skip to content

Commit

Permalink
Option --patterns-from-stdin and Git hook scripts update (#1606)
Browse files Browse the repository at this point in the history
* Add `--patterns-from-stdin` option to CLI
* Update Git hook scripts
  -  Instead of using `grep` to filter Kotlin (script) files, doing it  directly with `git diff`
  - Using the `-z` option for the `git` command and  the `--patterns-from-stdin=''` option for the `ktlint` command so that files with special characters are handled properly.  This also eliminates having to use `xargs` with that  `--no-run-if-empty` hack
  - Quoted the `origin/$(git rev-parse --abbref-ref HEAD)` argument, just in case
  - Removed the `if` statement at the end that checks the exit code, since it's unnecessary - it's basically the same as  `if (condition) return true else return false`

Co-authored-by: paul-dingemans <paul-dingemans@users.noreply.github.com>
  • Loading branch information
mfederczuk and paul-dingemans committed Sep 27, 2022
1 parent 8e78581 commit d1e4fe8
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 154 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Calling this API with a file path results in the `.editorconfig` files that will
### Added
* Wrap blocks in case the max line length is exceeded or in case the block contains a new line `wrapping` ([#1643](https://github.com/pinterest/ktlint/issue/1643))

* patterns can be read in from `stdin` with the `--patterns-from-stdin` command line options/flags ([#1606](https://github.com/pinterest/ktlint/pull/1606))

### Fixed

* Let a rule process all nodes even in case the rule is suppressed for a node so that the rule can update the internal state ([#1644](https://github.com/pinterest/ktlint/issue/1644))
Expand Down
3 changes: 3 additions & 0 deletions docs/install/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ ktlint installGitPrePushHook

`--relative`: Print files relative to the working directory (e.g. dir/file.kt instead of /home/user/project/dir/file.kt)

`--patterns-from-stdin[=<delimiter>]`: Reads additional patterns from `stdin`, where the patterns are separated by `<delimiter>`. If `=<delimiter>` is omitted, newline is used as fallback delimiter. If an empty string is given, the `NUL` byte is used as delimiter instead.
Options `--stdin` and `--patterns-from-stdin` are mutually exclusive, only one of them can be given at a time.

`-v`, `--verbose` or `--debug`: Turn on debug output. Also option `--trace` is available, but this is meant for ktlint library developers.

`-V` or `--version`: Prints version information and exit.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ import kotlin.system.exitProcess
import mu.KLogger
import mu.KotlinLogging
import org.jetbrains.kotlin.utils.addToStdlib.applyIf
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.ParameterException
import picocli.CommandLine.Parameters

private lateinit var logger: KLogger
Expand Down Expand Up @@ -91,6 +93,9 @@ Flags:
)
internal class KtlintCommandLine {

@CommandLine.Spec
private lateinit var commandSpec: CommandLine.Model.CommandSpec

@Option(
names = ["--android", "-a"],
description = ["Turn on Android Kotlin Style Guide compatibility"],
Expand Down Expand Up @@ -175,6 +180,18 @@ internal class KtlintCommandLine {
)
private var stdin: Boolean = false

@Option(
names = ["--patterns-from-stdin"],
description = [
"Read additional patterns to check/format from stdin. " +
"Patterns are delimited by the given argument. (default is newline) " +
"If the argument is an empty string, the NUL byte is used.",
],
arity = "0..1",
fallbackValue = "\n",
)
private var stdinDelimiter: String? = null

@Option(
names = ["--verbose", "-v"],
description = ["Show error codes"],
Expand Down Expand Up @@ -217,6 +234,11 @@ internal class KtlintCommandLine {
}
logger = configureLogger()

assertStdinAndPatternsFromStdinOptionsMutuallyExclusive()

val stdinPatterns: Set<String> = readPatternsFromStdin()
patterns.addAll(stdinPatterns)

// Set default value to patterns only after the logger has been configured to avoid a warning about initializing
// the logger multiple times
if (patterns.isEmpty()) {
Expand Down Expand Up @@ -297,6 +319,15 @@ internal class KtlintCommandLine {
}
.initKtLintKLogger()

private fun assertStdinAndPatternsFromStdinOptionsMutuallyExclusive() {
if (stdin && stdinDelimiter != null) {
throw ParameterException(
commandSpec.commandLine(),
"Options --stdin and --patterns-from-stdin mutually exclusive",
)
}
}

private fun lintFiles(
ruleProviders: Set<RuleProvider>,
editorConfigDefaults: EditorConfigDefaults,
Expand Down Expand Up @@ -532,6 +563,18 @@ internal class KtlintCommandLine {
map
}

private fun readPatternsFromStdin(): Set<String> {
val delimiter: String = stdinDelimiter
?.ifEmpty { "\u0000" }
?: return emptySet()

return String(System.`in`.readBytes())
.split(delimiter)
.let { patterns: List<String> ->
patterns.filterTo(LinkedHashSet(patterns.size), String::isNotEmpty)
}
}

private fun File.mkdirsOrFail() {
if (!mkdirs() && !isDirectory) {
throw IOException("Unable to create \"${this}\" directory")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/bin/sh
# https://github.com/pinterest/ktlint pre-commit hook
# On Linux xargs must be told to do nothing on no input. On MacOS (linux distribution "Darwin") this is default behavior and the xargs flag "--no-run-if-empty" flag does not exists
[ "$(uname -s)" != "Darwin" ] && no_run_if_empty=--no-run-if-empty
git diff --name-only --cached --relative | grep '\.kt[s"]\?$' | xargs $no_run_if_empty ktlint --android --relative
if [ $? -ne 0 ]; then exit 1; fi

# <https://github.com/pinterest/ktlint> pre-commit hook

git diff --name-only -z --cached --relative -- '*.kt' '*.kts' | ktlint --android --relative --patterns-from-stdin=''
9 changes: 4 additions & 5 deletions ktlint/src/main/resources/ktlint-git-pre-commit-hook.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/bin/sh
# https://github.com/pinterest/ktlint pre-commit hook
# On Linux xargs must be told to do nothing on no input. On MacOS (linux distribution "Darwin") this is default behavior and the xargs flag "--no-run-if-empty" flag does not exists
[ "$(uname -s)" != "Darwin" ] && no_run_if_empty=--no-run-if-empty
git diff --name-only --cached --relative | grep '\.kt[s"]\?$' | xargs $no_run_if_empty ktlint --relative
if [ $? -ne 0 ]; then exit 1; fi

# <https://github.com/pinterest/ktlint> pre-commit hook

git diff --name-only -z --cached --relative -- '*.kt' '*.kts' | ktlint --relative --patterns-from-stdin=''
9 changes: 4 additions & 5 deletions ktlint/src/main/resources/ktlint-git-pre-push-hook-android.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/bin/sh
# https://github.com/pinterest/ktlint pre-push hook
# On Linux xargs must be told to do nothing on no input. On MacOS (linux distribution "Darwin") this is default behavior and the xargs flag "--no-run-if-empty" flag does not exists
[ "$(uname -s)" != "Darwin" ] && no_run_if_empty=--no-run-if-empty
git diff --name-only HEAD origin/$(git rev-parse --abbrev-ref HEAD) | grep '\.kt[s"]\?$' | xargs $no_run_if_empty ktlint --android --relative
if [ $? -ne 0 ]; then exit 1; fi

# <https://github.com/pinterest/ktlint> pre-push hook

git diff --name-only -z HEAD "origin/$(git rev-parse --abbrev-ref HEAD)" -- '*.kt' '*.kts' | ktlint --android --relative --patterns-from-stdin=''
9 changes: 4 additions & 5 deletions ktlint/src/main/resources/ktlint-git-pre-push-hook.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/bin/sh
# https://github.com/pinterest/ktlint pre-push hook
# On Linux xargs must be told to do nothing on no input. On MacOS (linux distribution "Darwin") this is default behavior and the xargs flag "--no-run-if-empty" flag does not exists
[ "$(uname -s)" != "Darwin" ] && no_run_if_empty=--no-run-if-empty
git diff --name-only HEAD origin/$(git rev-parse --abbrev-ref HEAD) | grep '\.kt[s"]\?$' | xargs $no_run_if_empty ktlint --relative
if [ $? -ne 0 ]; then exit 1; fi

# <https://github.com/pinterest/ktlint> pre-push hook

git diff --name-only -z HEAD "origin/$(git rev-parse --abbrev-ref HEAD)" -- '*.kt' '*.kts' | ktlint --relative --patterns-from-stdin=''
63 changes: 48 additions & 15 deletions ktlint/src/test/kotlin/com/pinterest/ktlint/BaseCLITest.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.pinterest.ktlint

import java.io.File
import java.io.InputStream
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
import java.util.concurrent.TimeUnit
import org.assertj.core.api.AbstractAssert
import org.assertj.core.api.AbstractBooleanAssert
import org.assertj.core.api.AbstractIntegerAssert
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.ListAssert
import org.junit.jupiter.api.fail
import org.junit.jupiter.api.io.TempDir

Expand All @@ -27,6 +32,7 @@ abstract class BaseCLITest {
fun runKtLintCliProcess(
testProjectName: String,
arguments: List<String> = emptyList(),
stdin: InputStream? = null,
executionAssertions: ExecutionResult.() -> Unit,
) {
val projectPath = prepareTestProject(testProjectName)
Expand All @@ -43,6 +49,11 @@ abstract class BaseCLITest {
environment["PATH"] = "${System.getProperty("java.home")}${File.separator}bin${File.pathSeparator}${System.getenv()["PATH"]}"

val process = processBuilder.start()

if (stdin != null) {
process.outputStream.use(stdin::copyTo)
}

if (process.completedInAllowedDuration()) {
val output = process.inputStream.bufferedReader().use { it.readLines() }
val error = process.errorStream.bufferedReader().use { it.readLines() }
Expand Down Expand Up @@ -105,13 +116,33 @@ abstract class BaseCLITest {
)
}

protected fun ListAssert<String>.containsLineMatching(string: String): ListAssert<String> =
this.anyMatch {
it.contains(string)
}

protected fun ListAssert<String>.containsLineMatching(regex: Regex): ListAssert<String> =
this.anyMatch {
it.matches(regex)
}

protected fun ListAssert<String>.doesNotContainLineMatching(string: String): ListAssert<String> =
this.noneMatch {
it.contains(string)
}

protected fun ListAssert<String>.doesNotContainLineMatching(regex: Regex): ListAssert<String> =
this.noneMatch {
it.matches(regex)
}

data class ExecutionResult(
val exitCode: Int,
val normalOutput: List<String>,
val errorOutput: List<String>,
val testProject: Path,
) {
fun assertNormalExitCode() {
fun assertNormalExitCode(): AbstractIntegerAssert<*> =
assertThat(exitCode)
.withFailMessage(
"Expected process to exit with exitCode 0, but was $exitCode."
Expand All @@ -122,30 +153,32 @@ abstract class BaseCLITest {
),
),
).isEqualTo(0)
}

fun assertErrorExitCode() {
fun assertErrorExitCode(): AbstractIntegerAssert<*> =
assertThat(exitCode)
.withFailMessage("Execution was expected to finish with error. However, exitCode is $exitCode")
.isNotEqualTo(0)
}

fun assertErrorOutputIsEmpty() {
fun assertErrorOutputIsEmpty(): AbstractBooleanAssert<*> =
assertThat(errorOutput.isEmpty())
.withFailMessage(
"Expected error output to be empty but was:".followedByIndentedList(errorOutput),
).isTrue
}

fun assertSourceFileWasFormatted(
filePathInProject: String,
) {
val originalFile = testProjectsPath.resolve(testProject.last()).resolve(filePathInProject)
val newFile = testProject.resolve(filePathInProject)

assert(originalFile.toFile().readText() != newFile.toFile().readText()) {
"Format did not change source file $filePathInProject content:\n${originalFile.toFile().readText()}"
}
fun assertSourceFileWasFormatted(filePathInProject: String): AbstractAssert<*, *> {
val originalCode =
testProjectsPath
.resolve(testProject.last())
.resolve(filePathInProject)
.toFile()
.readText()
val formattedCode =
testProject
.resolve(filePathInProject)
.toFile()
.readText()

return assertThat(formattedCode).isNotEqualTo(originalCode)
}
}

Expand Down
Loading

0 comments on commit d1e4fe8

Please sign in to comment.