Skip to content

Commit

Permalink
Add "printAST" sub command. (pinterest#500)
Browse files Browse the repository at this point in the history
Move old one (--print-ast) option to be a ktlint sub command - new syntax:
`ktlint printAST`. Old one `--print-ast` is still working, but
deprecated.

Signed-off-by: Yahor Berdnikau <egorr.berd@gmail.com>
  • Loading branch information
Tapchicoma authored and shashachu committed Jul 18, 2019
1 parent 64f3269 commit 231fcc6
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 77 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,12 @@ A complete sample project (with tests and build files) is included in this repo
#### AST

While writing/debugging [Rule](ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/Rule.kt)s it's often helpful to have an AST
printed out to see the structure rules have to work with. ktlint >= 0.15.0 has `--print-ast` flag specifically for this purpose
(usage: `ktlint --color --print-ast <file>`).
printed out to see the structure rules have to work with. ktlint >= 0.15.0 has `printAST` subcommand specifically for this purpose
(usage: `ktlint --color printAST <file>`).
An example of the output is shown below.

```sh
$ printf "fun main() {}" | ktlint --color --print-ast --stdin
$ printf "fun main() {}" | ktlint --color printAST --stdin

1: ~.psi.KtFile (~.psi.stubs.elements.KtFileElementType.kotlin.FILE)
1: ~.psi.KtPackageDirective (~.psi.stubs.elements.KtPlaceHolderStubElementType.PACKAGE_DIRECTIVE) ""
Expand Down
92 changes: 18 additions & 74 deletions ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
@file:JvmName("Main")
package com.pinterest.ktlint

import com.github.shyiko.klob.Glob
import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.LintError
import com.pinterest.ktlint.core.ParseException
Expand All @@ -16,8 +15,12 @@ import com.pinterest.ktlint.internal.GitPrePushHookSubCommand
import com.pinterest.ktlint.internal.IntellijIDEAIntegration
import com.pinterest.ktlint.internal.KtlintVersionProvider
import com.pinterest.ktlint.internal.MavenDependencyResolver
import com.pinterest.ktlint.internal.PrintASTSubCommand
import com.pinterest.ktlint.internal.expandTilde
import com.pinterest.ktlint.internal.fileSequence
import com.pinterest.ktlint.internal.lintFile
import com.pinterest.ktlint.internal.location
import com.pinterest.ktlint.internal.printHelpOrVersionUsage
import com.pinterest.ktlint.test.DumpAST
import java.io.File
import java.io.IOException
import java.io.PrintStream
Expand Down Expand Up @@ -56,6 +59,7 @@ fun main(args: Array<String>) {
val commandLine = CommandLine(ktlintCommand)
.addSubcommand(GitPreCommitHookSubCommand.COMMAND_NAME, GitPreCommitHookSubCommand())
.addSubcommand(GitPrePushHookSubCommand.COMMAND_NAME, GitPrePushHookSubCommand())
.addSubcommand(PrintASTSubCommand.COMMAND_NAME, PrintASTSubCommand())
val parseResult = commandLine.parseArgs(*args)

commandLine.printHelpOrVersionUsage()
Expand All @@ -74,6 +78,7 @@ fun handleSubCommand(
when (val subCommand = parseResult.subcommand().commandSpec().userObject()) {
is GitPreCommitHookSubCommand -> subCommand.run()
is GitPrePushHookSubCommand -> subCommand.run()
is PrintASTSubCommand -> subCommand.run()
else -> commandLine.usage(System.out, CommandLine.Help.Ansi.OFF)
}
}
Expand Down Expand Up @@ -139,13 +144,13 @@ class KtlintCommandLine {
names = ["--color"],
description = ["Make output colorful"]
)
private var color: Boolean = false
var color: Boolean = false

@Option(
names = ["--debug"],
description = ["Turn on debug output"]
)
private var debug: Boolean = false
var debug: Boolean = false

// todo: this should have been a command, not a flag (consider changing in 1.0.0)
@Option(
Expand All @@ -161,20 +166,14 @@ class KtlintCommandLine {
private var limit: Int = -1
get() = if (field < 0) Int.MAX_VALUE else field

@Option(
names = ["--print-ast"],
description = ["Print AST (useful when writing/debugging rules)"]
)
private var printAST: Boolean = false

@Option(
names = ["--relative"],
description = [
"Print files relative to the working directory " +
"(e.g. dir/file.kt instead of /home/user/project/dir/file.kt)"
]
)
private var relative: Boolean = false
var relative: Boolean = false

@Option(
names = ["--reporter"],
Expand Down Expand Up @@ -248,17 +247,12 @@ class KtlintCommandLine {
private var patterns = ArrayList<String>()

private val workDir = File(".").canonicalPath
private fun File.location() = if (relative) this.toRelativeString(File(workDir)) else this.path

fun run() {
if (apply || applyToProject) {
applyToIDEA()
exitProcess(0)
}
if (printAST) {
printAST()
exitProcess(0)
}
val start = System.currentTimeMillis()
// load 3rd party ruleset(s) (if any)
val dependencyResolver = lazy(LazyThreadSafetyMode.NONE) { buildDependencyResolver() }
Expand Down Expand Up @@ -289,7 +283,8 @@ class KtlintCommandLine {
data class LintErrorWithCorrectionInfo(val err: LintError, val corrected: Boolean)
fun process(fileName: String, fileContent: String): List<LintErrorWithCorrectionInfo> {
if (debug) {
System.err.println("[DEBUG] Checking ${if (fileName != "<text>") File(fileName).location() else fileName}")
val fileLocation = if (fileName != "<text>") File(fileName).location(relative) else fileName
System.err.println("[DEBUG] Checking $fileLocation")
}
val result = ArrayList<LintErrorWithCorrectionInfo>()
val userData = resolveUserData(fileName)
Expand All @@ -315,7 +310,7 @@ class KtlintCommandLine {
}
} else {
try {
lint(fileName, fileContent, ruleSetProviders.map { it.second.get() }, userData) { err ->
lintFile(fileName, fileContent, ruleSetProviders.map { it.second.get() }, userData) { err ->
result.add(LintErrorWithCorrectionInfo(err, false))
tripped.set(true)
}
Expand Down Expand Up @@ -345,10 +340,10 @@ class KtlintCommandLine {
if (stdin) {
report("<text>", process("<text>", String(System.`in`.readBytes())))
} else {
fileSequence()
patterns.fileSequence()
.takeWhile { errorNumber.get() < limit }
.map { file -> Callable { file to process(file.path, file.readText()) } }
.parallel({ (file, errList) -> report(file.location(), errList) })
.parallel({ (file, errList) -> report(file.location(relative), errList) })
}
reporter.afterAll()
if (debug) {
Expand Down Expand Up @@ -401,7 +396,7 @@ class KtlintCommandLine {
private fun printEditorConfigChain(ec: EditorConfig, predicate: (EditorConfig) -> Boolean = { true }) {
for (lec in generateSequence(ec) { it.parent }.takeWhile(predicate)) {
System.err.println(
"[DEBUG] Discovered .editorconfig (${lec.path.parent.toFile().location()})" +
"[DEBUG] Discovered .editorconfig (${lec.path.parent.toFile().location(relative)})" +
" {${lec.entries.joinToString(", ")}}"
)
}
Expand Down Expand Up @@ -460,7 +455,8 @@ class KtlintCommandLine {
reporter.afterAll()
stream.close()
if (tripped()) {
System.err.println("\"$id\" report written to ${File(output).absoluteFile.location()}")
val outputLocation = File(output).absoluteFile.location(relative)
System.err.println("\"$id\" report written to $outputLocation")
}
}
}
Expand Down Expand Up @@ -495,41 +491,6 @@ class KtlintCommandLine {
}
}

private fun printAST() {
fun process(fileName: String, fileContent: String) {
if (debug) {
System.err.println("[DEBUG] Analyzing ${if (fileName != "<text>") File(fileName).location() else fileName}")
}
try {
lint(fileName, fileContent, listOf(RuleSet("debug", DumpAST(System.out, color))), emptyMap()) {}
} catch (e: Exception) {
if (e is ParseException) {
throw ParseException(e.line, e.col, "Not a valid Kotlin file (${e.message?.toLowerCase()})")
}
throw e
}
}
if (stdin) {
process("<text>", String(System.`in`.readBytes()))
} else {
for (file in fileSequence()) {
process(file.path, file.readText())
}
}
}

private fun fileSequence() =
when {
patterns.isEmpty() ->
Glob.from("**/*.kt", "**/*.kts")
.iterate(Paths.get(workDir), Glob.IterationOption.SKIP_HIDDEN)
else ->
Glob.from(*patterns.map { expandTilde(it) }.toTypedArray())
.iterate(Paths.get(workDir))
}
.asSequence()
.map(Path::toFile)

private fun applyToIDEA() {
try {
val workDir = Paths.get(".")
Expand Down Expand Up @@ -566,10 +527,6 @@ class KtlintCommandLine {
System.err.println("(if you experience any issues please report them at https://github.com/pinterest/ktlint)")
}

// 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) = path.replaceFirst(Regex("^~"), System.getProperty("user.home"))

private fun <T> List<T>.head(limit: Int) = if (limit == size) this else this.subList(0, limit)

private fun buildDependencyResolver(): MavenDependencyResolver {
Expand Down Expand Up @@ -687,19 +644,6 @@ class KtlintCommandLine {
map
}

private fun lint(
fileName: String,
text: String,
ruleSets: Iterable<RuleSet>,
userData: Map<String, String>,
cb: (e: LintError) -> Unit
) =
if (fileName.endsWith(".kt", ignoreCase = true)) {
KtLint.lint(text, ruleSets, userData, cb)
} else {
KtLint.lintScript(text, ruleSets, userData, cb)
}

private fun format(
fileName: String,
text: String,
Expand Down
55 changes: 55 additions & 0 deletions ktlint/src/main/kotlin/com/pinterest/ktlint/internal/FileUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.pinterest.ktlint.internal

import com.github.shyiko.klob.Glob
import com.pinterest.ktlint.core.KtLint.lint
import com.pinterest.ktlint.core.KtLint.lintScript
import com.pinterest.ktlint.core.LintError
import com.pinterest.ktlint.core.RuleSet
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths

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

internal fun List<String>.fileSequence(): Sequence<File> {
val kotlinFiles = if (isEmpty()) {
Glob.from("**/*.kt", "**/*.kts")
.iterate(
Paths.get(workDir),
Glob.IterationOption.SKIP_HIDDEN
)
} else {
val normalizedPatterns = map(::expandTilde).toTypedArray()
Glob.from(*normalizedPatterns)
.iterate(Paths.get(workDir))
}

return kotlinFiles
.asSequence()
.map(Path::toFile)
}

// 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 (~/)
internal fun expandTilde(path: String): String = path.replaceFirst(Regex("^~"), System.getProperty("user.home"))

internal fun File.location(
relative: Boolean
) = if (relative) this.toRelativeString(File(workDir)) else this.path

/**
* Run lint over common kotlin file or kotlin script file.
*/
internal fun lintFile(
fileName: String,
fileContent: String,
ruleSetList: List<RuleSet>,
userData: Map<String, String> = emptyMap(),
lintErrorCallback: (LintError) -> Unit = {}
) {
if (fileName.endsWith(".kt", ignoreCase = true)) {
lint(fileContent, ruleSetList, userData, lintErrorCallback)
} else {
lintScript(fileContent, ruleSetList, userData, lintErrorCallback)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.pinterest.ktlint.internal

import com.pinterest.ktlint.KtlintCommandLine
import com.pinterest.ktlint.core.ParseException
import com.pinterest.ktlint.core.RuleSet
import com.pinterest.ktlint.test.DumpAST
import java.io.File
import picocli.CommandLine

@CommandLine.Command(
description = [
"Print AST (useful when writing/debugging rules)",
"Usage of \"--print-ast\" command line option is deprecated!"
],
aliases = ["--print-ast"],
mixinStandardHelpOptions = true,
versionProvider = KtlintVersionProvider::class
)
internal class PrintASTSubCommand : Runnable {
@CommandLine.ParentCommand
private lateinit var ktlintCommand: KtlintCommandLine

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

@CommandLine.Parameters(
description = ["include all files under this .gitignore-like patterns"]
)
private var patterns = ArrayList<String>()

@CommandLine.Option(
names = ["--stdin"],
description = ["Read file content from stdin"]
)
private var stdin: Boolean = false

private val astRuleSet by lazy(LazyThreadSafetyMode.NONE) {
listOf(
RuleSet("debug", DumpAST(System.out, ktlintCommand.color))
)
}

override fun run() {
commandSpec.commandLine().printHelpOrVersionUsage()

if (stdin) {
printAST(STDIN_FILE, String(System.`in`.readBytes()))
} else {
for (file in patterns.fileSequence()) {
printAST(file.path, file.readText())
}
}
}

private fun printAST(
fileName: String,
fileContent: String
) {
if (ktlintCommand.debug) {
val fileLocation = if (fileName != STDIN_FILE) {
File(fileName).location(ktlintCommand.relative)
} else {
"stdin"
}
println("[DEBUG] Analyzing $fileLocation")
}

try {
lintFile(fileName, fileContent, astRuleSet)
} catch (e: Exception) {
if (e is ParseException) {
throw ParseException(e.line, e.col, "Not a valid Kotlin file (${e.message?.toLowerCase()})")
}
throw e
}
}

companion object {
internal const val COMMAND_NAME = "printAST"
private const val STDIN_FILE = "<stdin>"
}
}

0 comments on commit 231fcc6

Please sign in to comment.