diff --git a/README.md b/README.md index 4aea0f5db8..1029e8fb1b 100644 --- a/README.md +++ b/README.md @@ -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 `). +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 `). 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) "" diff --git a/ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt b/ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt index e7ef0198a4..4812aac3f4 100644 --- a/ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt +++ b/ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt @@ -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 @@ -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 @@ -56,6 +59,7 @@ fun main(args: Array) { 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() @@ -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) } } @@ -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( @@ -161,12 +166,6 @@ 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 = [ @@ -174,7 +173,7 @@ class KtlintCommandLine { "(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"], @@ -248,17 +247,12 @@ class KtlintCommandLine { private var patterns = ArrayList() 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() } @@ -289,7 +283,8 @@ class KtlintCommandLine { data class LintErrorWithCorrectionInfo(val err: LintError, val corrected: Boolean) fun process(fileName: String, fileContent: String): List { if (debug) { - System.err.println("[DEBUG] Checking ${if (fileName != "") File(fileName).location() else fileName}") + val fileLocation = if (fileName != "") File(fileName).location(relative) else fileName + System.err.println("[DEBUG] Checking $fileLocation") } val result = ArrayList() val userData = resolveUserData(fileName) @@ -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) } @@ -345,10 +340,10 @@ class KtlintCommandLine { if (stdin) { report("", process("", 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) { @@ -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(", ")}}" ) } @@ -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") } } } @@ -495,41 +491,6 @@ class KtlintCommandLine { } } - private fun printAST() { - fun process(fileName: String, fileContent: String) { - if (debug) { - System.err.println("[DEBUG] Analyzing ${if (fileName != "") 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("", 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(".") @@ -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 List.head(limit: Int) = if (limit == size) this else this.subList(0, limit) private fun buildDependencyResolver(): MavenDependencyResolver { @@ -687,19 +644,6 @@ class KtlintCommandLine { map } - private fun lint( - fileName: String, - text: String, - ruleSets: Iterable, - userData: Map, - 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, diff --git a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/FileUtils.kt b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/FileUtils.kt new file mode 100644 index 0000000000..7eb0df7561 --- /dev/null +++ b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/FileUtils.kt @@ -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.fileSequence(): Sequence { + 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, + userData: Map = emptyMap(), + lintErrorCallback: (LintError) -> Unit = {} +) { + if (fileName.endsWith(".kt", ignoreCase = true)) { + lint(fileContent, ruleSetList, userData, lintErrorCallback) + } else { + lintScript(fileContent, ruleSetList, userData, lintErrorCallback) + } +} diff --git a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/PrintASTSubCommand.kt b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/PrintASTSubCommand.kt new file mode 100644 index 0000000000..ab6e151eb4 --- /dev/null +++ b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/PrintASTSubCommand.kt @@ -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() + + @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 = "" + } +}