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

Add "printAST" sub command. #500

Merged
merged 1 commit into from
Jul 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I ask what's the reasoning behind using a camel-cased command name instead of good old '--do-something' pattern for CLI tools? The old way just looks more natural imo.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main reason to move into subcommands direction - reduce user confusion and make command line usage more clear.

For example, now with only options, it is not clear if using --print-ast option will also do linting.

Though I am open to subcommand name changes as I don't have strong opinion here.

private const val STDIN_FILE = "<stdin>"
}
}