diff options
Diffstat (limited to 'ktlint/src/main/kotlin/com/github/shyiko/ktlint/Main.kt')
-rw-r--r-- | ktlint/src/main/kotlin/com/github/shyiko/ktlint/Main.kt | 572 |
1 files changed, 337 insertions, 235 deletions
diff --git a/ktlint/src/main/kotlin/com/github/shyiko/ktlint/Main.kt b/ktlint/src/main/kotlin/com/github/shyiko/ktlint/Main.kt index ce805632..9541cdcb 100644 --- a/ktlint/src/main/kotlin/com/github/shyiko/ktlint/Main.kt +++ b/ktlint/src/main/kotlin/com/github/shyiko/ktlint/Main.kt @@ -12,6 +12,7 @@ import com.github.shyiko.ktlint.core.RuleSetProvider import com.github.shyiko.ktlint.internal.EditorConfig import com.github.shyiko.ktlint.internal.IntellijIDEAIntegration import com.github.shyiko.ktlint.internal.MavenDependencyResolver +import com.github.shyiko.ktlint.test.DumpAST import org.eclipse.aether.RepositoryException import org.eclipse.aether.artifact.DefaultArtifact import org.eclipse.aether.repository.RemoteRepository @@ -19,19 +20,14 @@ import org.eclipse.aether.repository.RepositoryPolicy import org.eclipse.aether.repository.RepositoryPolicy.CHECKSUM_POLICY_IGNORE import org.eclipse.aether.repository.RepositoryPolicy.UPDATE_POLICY_NEVER import org.jetbrains.kotlin.preprocessor.mkdirsOrFail -import org.kohsuke.args4j.Argument -import org.kohsuke.args4j.CmdLineException -import org.kohsuke.args4j.CmdLineParser -import org.kohsuke.args4j.NamedOptionDef -import org.kohsuke.args4j.Option -import org.kohsuke.args4j.OptionHandlerFilter -import org.kohsuke.args4j.ParserProperties -import org.kohsuke.args4j.spi.OptionHandler +import picocli.CommandLine +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters import java.io.ByteArrayOutputStream import java.io.File import java.io.FileNotFoundException import java.io.PrintStream -import java.io.PrintWriter import java.math.BigInteger import java.net.URLDecoder import java.nio.file.Path @@ -39,8 +35,8 @@ import java.nio.file.Paths import java.security.MessageDigest import java.util.ArrayList import java.util.Arrays +import java.util.LinkedHashMap import java.util.NoSuchElementException -import java.util.ResourceBundle import java.util.Scanner import java.util.ServiceLoader import java.util.concurrent.ArrayBlockingQueue @@ -50,8 +46,41 @@ import java.util.concurrent.Future import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger +import java.util.jar.Manifest import kotlin.system.exitProcess +@Command( + headerHeading = """An anti-bikeshedding Kotlin linter with built-in formatter +(https://github.com/shyiko/ktlint). + +Usage: + ktlint <flags> [patterns] + java -jar ktlint <flags> [patterns] + +Examples: + # check the style of all Kotlin files inside the current dir (recursively) + # (hidden folders will be skipped) + ktlint + + # check only certain locations (prepend ! to negate the pattern) + ktlint "src/**/*.kt" "!src/**/*Test.kt" + + # auto-correct style violations + ktlint -F "src/**/*.kt" + + # custom reporter + ktlint --reporter=plain?group_by_file + # multiple reporters can be specified like this + ktlint --reporter=plain \ + --reporter=checkstyle,output=ktlint-checkstyle-report.xml + # 3rd-party reporter + ktlint --reporter=html,artifact=com.gihub.user:repo:master-SNAPSHOT + +Flags:""", + synopsisHeading = "", + customSynopsis = arrayOf(""), + sortOptions = false +) object Main { private val DEPRECATED_FLAGS = mapOf( @@ -64,103 +93,127 @@ object Main { "--reporter-update" to "--repository-update" ) - private val CLI_MAX_LINE_LENGTH_REGEX = Regex("(.{0,120})(?:\\s|$)") + @Option(names = arrayOf("--android", "-a"), description = arrayOf("Turn on Android Kotlin Style Guide compatibility")) + private var android: Boolean = false - // todo: this should have been a command, not a flag (consider changing in 1.0.0) - @Option(name="--format", aliases = arrayOf("-F"), usage = "Fix any deviations from the code style") - private var format: Boolean = false + // todo: make it a command in 1.0.0 (it's too late now as we might interfere with valid "lint" patterns) + @Option(names = arrayOf("--apply-to-idea"), description = arrayOf("Update Intellij IDEA settings (global)")) + private var apply: Boolean = false - @Option(name="--android", aliases = arrayOf("-a"), usage = "Turn on Android Kotlin Style Guide compatibility") - private var android: Boolean = false + // todo: make it a command in 1.0.0 (it's too late now as we might interfere with valid "lint" patterns) + @Option(names = arrayOf("--apply-to-idea-project"), description = arrayOf("Update Intellij IDEA project settings")) + private var applyToProject: Boolean = false - @Option(name="--reporter", - usage = "A reporter to use (built-in: plain (default), plain?group_by_file, json, checkstyle). " + - "To use a third-party reporter specify either a path to a JAR file on the filesystem or a" + - "<groupId>:<artifactId>:<version> triple pointing to a remote artifact (in which case ktlint will first " + - "check local cache (~/.m2/repository) and then, if not found, attempt downloading it from " + - "Maven Central/JCenter/JitPack/user-provided repository)") - private var reporters = ArrayList<String>() + @Option(names = arrayOf("--color"), description = arrayOf("Make output colorful")) + private var color: Boolean = false - @Option(name="--ruleset", aliases = arrayOf("-R"), - usage = "A path to a JAR file containing additional ruleset(s) or a " + - "<groupId>:<artifactId>:<version> triple pointing to a remote artifact (in which case ktlint will first " + - "check local cache (~/.m2/repository) and then, if not found, attempt downloading it from " + - "Maven Central/JCenter/JitPack/user-provided repository)") - private var rulesets = ArrayList<String>() + @Option(names = arrayOf("--debug"), description = arrayOf("Turn on debug output")) + private var debug: Boolean = false - @Option(name="--repository", aliases = arrayOf("--ruleset-repository", "--reporter-repository"), - usage = "An additional Maven repository (Maven Central/JCenter/JitPack are active by default) " + - "(value format: <id>=<url>)") - private var repositories = ArrayList<String>() + // todo: this should have been a command, not a flag (consider changing in 1.0.0) + @Option(names = arrayOf("--format", "-F"), description = arrayOf("Fix any deviations from the code style")) + private var format: Boolean = false - @Option(name="--repository-update", aliases = arrayOf("-U", "--ruleset-update", "--reporter-update"), - usage = "Check remote repositories for updated snapshots") - private var forceUpdate: Boolean = false + @Option(names = arrayOf("--install-git-pre-commit-hook"), description = arrayOf( + "Install git hook to automatically check files for style violations on commit" + )) + private var installGitPreCommitHook: Boolean = false - @Option(name="--limit", usage = "Maximum number of errors to show (default: show all)") + @Option(names = arrayOf("--limit"), description = arrayOf( + "Maximum number of errors to show (default: show all)" + )) private var limit: Int = -1 + get() = if (field < 0) Int.MAX_VALUE else field + + @Option(names = arrayOf("--print-ast"), description = arrayOf( + "Print AST (useful when writing/debugging rules)" + )) + private var printAST: Boolean = false - @Option(name="--relative", usage = "Print files relative to the working directory " + - "(e.g. dir/file.kt instead of /home/user/project/dir/file.kt)") + @Option(names = arrayOf("--relative"), description = arrayOf( + "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 - @Option(name="--verbose", aliases = arrayOf("-v"), usage = "Show error codes") - private var verbose: Boolean = false + @Option(names = arrayOf("--reporter"), description = arrayOf( + "A reporter to use (built-in: plain (default), plain?group_by_file, json, checkstyle). " + + "To use a third-party reporter specify either a path to a JAR file on the filesystem or a" + + "<groupId>:<artifactId>:<version> triple pointing to a remote artifact (in which case ktlint will first " + + "check local cache (~/.m2/repository) and then, if not found, attempt downloading it from " + + "Maven Central/JCenter/JitPack/user-provided repository)\n" + + "e.g. \"html,artifact=com.github.username:ktlint-reporter-html:master-SNAPSHOT\"" + )) + private var reporters = ArrayList<String>() + + @Option(names = arrayOf("--repository"), description = arrayOf( + "An additional Maven repository (Maven Central/JCenter/JitPack are active by default) " + + "(value format: <id>=<url>)" + )) + private var repositories = ArrayList<String>() + @Option(names = arrayOf("--ruleset-repository", "--reporter-repository"), hidden = true) + private var repositoriesDeprecated = ArrayList<String>() + + @Option(names = arrayOf("--repository-update", "-U"), description = arrayOf( + "Check remote repositories for updated snapshots" + )) + private var forceUpdate: Boolean? = null + @Option(names = arrayOf("--ruleset-update", "--reporter-update"), hidden = true) + private var forceUpdateDeprecated: Boolean? = null + + @Option(names = arrayOf("--ruleset", "-R"), description = arrayOf( + "A path to a JAR file containing additional ruleset(s) or a " + + "<groupId>:<artifactId>:<version> triple pointing to a remote artifact (in which case ktlint will first " + + "check local cache (~/.m2/repository) and then, if not found, attempt downloading it from " + + "Maven Central/JCenter/JitPack/user-provided repository)" + )) + private var rulesets = ArrayList<String>() + + @Option(names = arrayOf("--skip-classpath-check"), description = arrayOf("Do not check classpath for pottential conflicts")) + private var skipClasspathCheck: Boolean = false - @Option(name="--stdin", usage = "Read file from stdin") + @Option(names = arrayOf("--stdin"), description = arrayOf("Read file from stdin")) private var stdin: Boolean = false - @Option(name="--version", usage = "Version", help = true) + @Option(names = arrayOf("--verbose", "-v"), description = arrayOf("Show error codes")) + private var verbose: Boolean = false + + @Option(names = arrayOf("--version"), description = arrayOf("Print version information")) private var version: Boolean = false - @Option(name="--help", aliases = arrayOf("-h"), help = true) + @Option(names = arrayOf("--help", "-h"), help = true, hidden = true) private var help: Boolean = false - @Option(name="--debug", usage = "Turn on debug output") - private var debug: Boolean = false - - // todo: make it a command in 1.0.0 (it's too late now as we might interfere with valid "lint" patterns) - @Option(name="--apply-to-idea", usage = "Update Intellij IDEA project settings") - private var apply: Boolean = false - @Option(name="--install-git-pre-commit-hook", usage = "Install git hook to automatically check files for style violations on commit") - private var installGitPreCommitHook: Boolean = false - @Option(name="-y", hidden = true) + @Option(names = arrayOf("-y"), hidden = true) private var forceApply: Boolean = false - @Argument + @Parameters(hidden = true) private var patterns = ArrayList<String>() - private fun CmdLineParser.usage(): String = - """ - An anti-bikeshedding Kotlin linter with built-in formatter (https://github.com/shyiko/ktlint). - - Usage: - ktlint <flags> [patterns] - java -jar ktlint <flags> [patterns] + private val workDir = File(".").canonicalPath + private fun File.location() = if (relative) this.toRelativeString(File(workDir)) else this.path - Examples: - # check the style of all Kotlin files inside the current dir (recursively) - # (hidden folders will be skipped) - ktlint + private fun usage() = + ByteArrayOutputStream() + .also { CommandLine.usage(this, PrintStream(it), CommandLine.Help.Ansi.OFF) } + .toString() + .replace(" ".repeat(32), " ".repeat(30)) - # check only certain locations (prepend ! to negate the pattern) - ktlint "src/**/*.kt" "!src/**/*Test.kt" - - # auto-correct style violations - ktlint -F "src/**/*.kt" - - # use custom reporter - ktlint --reporter=plain?group_by_file - # multiple reporters can be specified like this - ktlint --reporter=plain --reporter=checkstyle,output=ktlint-checkstyle-report.xml - - Flags: -${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().split("\n") - .map { line -> (" " + line).replace(CLI_MAX_LINE_LENGTH_REGEX, " $1\n").trimEnd() } - .joinToString("\n")} - """.trimIndent() - - fun parseCmdLine(args: Array<String>) { + private fun parseCmdLine(args: Array<String>) { + try { + CommandLine.populateCommand(this, *args) + repositories.addAll(repositoriesDeprecated) + if (forceUpdateDeprecated != null && forceUpdate == null) { + forceUpdate = forceUpdateDeprecated + } + } catch (e: Exception) { + System.err.println("Error: ${e.message}\n\n${usage()}") + exitProcess(1) + } + if (help) { + println(usage()) + exitProcess(0) + } args.forEach { arg -> if (arg.startsWith("--") && arg.contains("=")) { val flag = arg.substringBefore("=") @@ -170,43 +223,13 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s } } } - val parser = object : CmdLineParser(this, ParserProperties.defaults() - .withShowDefaults(false) - .withUsageWidth(512) - .withOptionSorter { l, r -> - l.option.toString().replace("-", "").compareTo(r.option.toString().replace("-", "")) - }) { - - override fun printOption(out: PrintWriter, handler: OptionHandler<*>, len: Int, rb: ResourceBundle?, filter: OptionHandlerFilter?) { - handler.defaultMetaVariable - val opt = handler.option as? NamedOptionDef ?: return - if (opt.hidden() || opt.help()) { - return - } - val maxNameLength = options.map { h -> - val o = h.option - (o as? NamedOptionDef)?.let { it.name().length + 1 + (h.defaultMetaVariable ?: "").length } ?: 0 - }.max()!! - val shorthand = opt.aliases().find { it.startsWith("-") && !it.startsWith("--") } - val line = (if (shorthand != null) "$shorthand, " else " ") + - (opt.name() + " " + (handler.defaultMetaVariable ?: "")).padEnd(maxNameLength, ' ') + " " + opt.usage() - out.println(line) - } - } - try { - parser.parseArgument(*args) - } catch (err: CmdLineException) { - System.err.println("Error: ${err.message}\n\n${parser.usage()}") - exitProcess(1) - } - if (help) { println(parser.usage()); exitProcess(0) } } @JvmStatic fun main(args: Array<String>) { parseCmdLine(args) if (version) { - println(javaClass.`package`.implementationVersion) + println(getImplementationVersion()) exitProcess(0) } if (installGitPreCommitHook) { @@ -215,116 +238,59 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s exitProcess(0) } } - if (apply) { + if (apply || applyToProject) { applyToIDEA() exitProcess(0) } - val workDir = File(".").canonicalPath + if (printAST) { + printAST() + exitProcess(0) + } val start = System.currentTimeMillis() // load 3rd party ruleset(s) (if any) - val dependencyResolver by lazy { buildDependencyResolver() } + val dependencyResolver = lazy(LazyThreadSafetyMode.NONE) { buildDependencyResolver() } if (!rulesets.isEmpty()) { loadJARs(dependencyResolver, rulesets) } // standard should go first - val rp = ServiceLoader.load(RuleSetProvider::class.java) + val ruleSetProviders = ServiceLoader.load(RuleSetProvider::class.java) .map { it.get().id to it } .sortedBy { if (it.first == "standard") "\u0000${it.first}" else it.first } if (debug) { - rp.forEach { System.err.println("[DEBUG] Discovered ruleset \"${it.first}\"") } - } - data class R(val id: String, val config: Map<String, String>, var output: String?) - if (reporters.isEmpty()) { - reporters.add("plain") + ruleSetProviders.forEach { System.err.println("[DEBUG] Discovered ruleset \"${it.first}\"") } } - val rr = this.reporters.map { reporter -> - val split = reporter.split(",") - val (reporterId, rawReporterConfig) = split[0].split("?", limit = 2) + listOf("") - R(reporterId, mapOf("verbose" to verbose.toString()) + parseQuery(rawReporterConfig), - split.getOrNull(1)?.let { if (it.startsWith("output=")) it.split("=")[1] else null }) - }.distinct() - // load reporter - val reporterLoader = ServiceLoader.load(ReporterProvider::class.java) - val reporterProviderById = reporterLoader.associate { it.id to it }.let { map -> - val missingReporters = rr.map { it.id }.distinct().filter { !map.containsKey(it) } - if (!missingReporters.isEmpty()) { - loadJARs(dependencyResolver, missingReporters) - reporterLoader.reload() - reporterLoader.associate { it.id to it } - } else map - } - if (debug) { - reporterProviderById.forEach { (id) -> System.err.println("[DEBUG] Discovered reporter \"$id\"") } - } - val reporter = Reporter.from(*rr.map { r -> - val reporterProvider = reporterProviderById[r.id] - if (reporterProvider == null) { - System.err.println("Error: reporter \"${r.id}\" wasn't found (available: ${ - reporterProviderById.keys.sorted().joinToString(",")})") - exitProcess(1) - } - if (debug) { - System.err.println("[DEBUG] Initializing \"${r.id}\" reporter with ${r.config}" + - (r.output?.let { ", output=$it" } ?: "")) - } - val output = if (r.output != null) { File(r.output).parentFile?.mkdirsOrFail(); PrintStream(r.output) } else - if (stdin) System.err else System.out - reporterProvider.get(output, r.config).let { reporter -> - if (r.output != null) - object : Reporter by reporter { - override fun afterAll() { - reporter.afterAll() - output.close() - } - } - else - reporter - } - }.toTypedArray()) + val reporter = loadReporter(dependencyResolver) // load .editorconfig val userData = ( EditorConfig.of(workDir) ?.also { editorConfig -> if (debug) { - System.err.println("[DEBUG] Discovered .editorconfig (${editorConfig.path.parent})") + System.err.println("[DEBUG] Discovered .editorconfig (${ + generateSequence(editorConfig) { it.parent }.map { it.path.parent.toFile().location() }.joinToString() + })") System.err.println("[DEBUG] ${editorConfig.mapKeys { it.key }} loaded from .editorconfig") } } ?: emptyMap<String, String>() ) + mapOf("android" to android.toString()) - data class LintErrorWithCorrectionInfo(val err: LintError, val corrected: Boolean) - fun lintErrorFrom(e: Exception): LintError = when (e) { - is ParseException -> - LintError(e.line, e.col, "", - "Not a valid Kotlin file (${e.message?.toLowerCase()})") - is RuleExecutionException -> { - if (debug) { - System.err.println("[DEBUG] Internal Error (${e.ruleId})") - e.printStackTrace(System.err) - } - LintError(e.line, e.col, "", "Internal Error (${e.ruleId}). " + - "Please create a ticket at https://github.com/shyiko/ktlint/issue " + - "(if possible, provide the source code that triggered an error)") - } - else -> throw e - } val tripped = AtomicBoolean() + 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 (relative && fileName != "<text>") File(fileName).toRelativeString(File(workDir)) else fileName - }") + System.err.println("[DEBUG] Checking ${if (fileName != "<text>") File(fileName).location() else fileName}") } val result = ArrayList<LintErrorWithCorrectionInfo>() + val localUserData = if (fileName != "<text>") userData + ("file_path" to fileName) else userData if (format) { val formattedFileContent = try { - format(fileName, fileContent, rp.map { it.second.get() }, userData) { err, corrected -> + format(fileName, fileContent, ruleSetProviders.map { it.second.get() }, localUserData) { err, corrected -> if (!corrected) { result.add(LintErrorWithCorrectionInfo(err, corrected)) + tripped.set(true) } } } catch (e: Exception) { - result.add(LintErrorWithCorrectionInfo(lintErrorFrom(e), false)) + result.add(LintErrorWithCorrectionInfo(e.toLintError(), false)) tripped.set(true) fileContent // making sure `cat file | ktlint --stdint > file` is (relatively) safe } @@ -337,19 +303,17 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s } } else { try { - lint(fileName, fileContent, rp.map { it.second.get() }, userData) { err -> - tripped.set(true) + lint(fileName, fileContent, ruleSetProviders.map { it.second.get() }, localUserData) { err -> result.add(LintErrorWithCorrectionInfo(err, false)) + tripped.set(true) } } catch (e: Exception) { - result.add(LintErrorWithCorrectionInfo(lintErrorFrom(e), false)) + result.add(LintErrorWithCorrectionInfo(e.toLintError(), false)) + tripped.set(true) } } return result } - if (limit < 0) { - limit = Int.MAX_VALUE - } val (fileNumber, errorNumber) = Pair(AtomicInteger(), AtomicInteger()) fun report(fileName: String, errList: List<LintErrorWithCorrectionInfo>) { fileNumber.incrementAndGet() @@ -365,35 +329,141 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s if (stdin) { report("<text>", process("<text>", String(System.`in`.readBytes()))) } else { - val pathIterator = 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)) - } - pathIterator - .asSequence() + fileSequence() .takeWhile { errorNumber.get() < limit } - .map(Path::toFile) - .map { file -> - Callable { file to process(file.path, file.readText()) } - } - .parallel({ (file, errList) -> - report(if (relative) file.toRelativeString(File(workDir)) else file.path, errList) }) + .map { file -> Callable { file to process(file.path, file.readText()) } } + .parallel({ (file, errList) -> report(file.location(), errList) }) } reporter.afterAll() if (debug) { - System.err.println("[DEBUG] ${(System.currentTimeMillis() - start) - }ms / $fileNumber file(s) / $errorNumber error(s)") + System.err.println("[DEBUG] ${ + System.currentTimeMillis() - start + }ms / $fileNumber file(s) / $errorNumber error(s)") } if (tripped.get()) { exitProcess(1) } } - fun installGitPreCommitHook() { + private fun getImplementationVersion() = javaClass.`package`.implementationVersion + // JDK 9 regression workaround (https://bugs.openjdk.java.net/browse/JDK-8190987, fixed in JDK 10) + // (note that version reported by the fallback might not be null if META-INF/MANIFEST.MF is + // loaded from another JAR on the classpath (e.g. if META-INF/MANIFEST.MF wasn't created as part of the build)) + ?: javaClass.getResourceAsStream("/META-INF/MANIFEST.MF") + ?.let { stream -> + Manifest(stream).mainAttributes.getValue("Implementation-Version") + } + + private fun loadReporter(dependencyResolver: Lazy<MavenDependencyResolver>): Reporter { + data class ReporterTemplate(val id: String, val artifact: String?, val config: Map<String, String>, var output: String?) + val tpls = (if (reporters.isEmpty()) listOf("plain") else reporters) + .map { reporter -> + val split = reporter.split(",") + val (reporterId, rawReporterConfig) = split[0].split("?", limit = 2) + listOf("") + ReporterTemplate( + reporterId, + split.lastOrNull { it.startsWith("artifact=") }?.let { it.split("=")[1] }, + mapOf("verbose" to verbose.toString(), "color" to color.toString()) + parseQuery(rawReporterConfig), + split.lastOrNull { it.startsWith("output=") }?.let { it.split("=")[1] } + ) + } + .distinct() + val reporterLoader = ServiceLoader.load(ReporterProvider::class.java) + val reporterProviderById = reporterLoader.associate { it.id to it }.let { map -> + val missingReporters = tpls.filter { !map.containsKey(it.id) }.mapNotNull { it.artifact }.distinct() + if (!missingReporters.isEmpty()) { + loadJARs(dependencyResolver, missingReporters) + reporterLoader.reload() + reporterLoader.associate { it.id to it } + } else map + } + if (debug) { + reporterProviderById.forEach { (id) -> System.err.println("[DEBUG] Discovered reporter \"$id\"") } + } + fun ReporterTemplate.toReporter(): Reporter { + val reporterProvider = reporterProviderById[id] + if (reporterProvider == null) { + System.err.println("Error: reporter \"$id\" wasn't found (available: ${ + reporterProviderById.keys.sorted().joinToString(",") + })") + exitProcess(1) + } + if (debug) { + System.err.println("[DEBUG] Initializing \"$id\" reporter with $config" + + (output?.let { ", output=$it" } ?: "")) + } + val stream = if (output != null) { + File(output).parentFile?.mkdirsOrFail(); PrintStream(output, "UTF-8") + } else if (stdin) System.err else System.out + return reporterProvider.get(stream, config) + .let { reporter -> + if (output != null) + object : Reporter by reporter { + override fun afterAll() { + reporter.afterAll() + stream.close() + } + } + else reporter + } + } + return Reporter.from(*tpls.map { it.toReporter() }.toTypedArray()) + } + + private fun Exception.toLintError(): LintError = this.let { e -> + when (e) { + is ParseException -> + LintError(e.line, e.col, "", + "Not a valid Kotlin file (${e.message?.toLowerCase()})") + is RuleExecutionException -> { + if (debug) { + System.err.println("[DEBUG] Internal Error (${e.ruleId})") + e.printStackTrace(System.err) + } + LintError(e.line, e.col, "", "Internal Error (${e.ruleId}). " + + "Please create a ticket at https://github.com/shyiko/ktlint/issue " + + "(if possible, provide the source code that triggered an error)") + } + else -> throw e + } + } + + 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 installGitPreCommitHook() { if (!File(".git").isDirectory) { System.err.println(".git directory not found. " + "Are you sure you are inside project root directory?") @@ -417,17 +487,16 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s System.err.println(".git/hooks/pre-commit installed") } - fun applyToIDEA() { + private fun applyToIDEA() { try { val workDir = Paths.get(".") if (!forceApply) { - val fileList = IntellijIDEAIntegration.apply(workDir, true, android) + val fileList = IntellijIDEAIntegration.apply(workDir, true, android, applyToProject) System.err.println("The following files are going to be updated:\n\n\t" + fileList.joinToString("\n\t") + "\n\nDo you wish to proceed? [y/n]\n" + "(in future, use -y flag if you wish to skip confirmation)") val scanner = Scanner(System.`in`) - val res = generateSequence { try { scanner.next() } catch (e: NoSuchElementException) { null } } @@ -438,7 +507,7 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s exitProcess(1) } } - IntellijIDEAIntegration.apply(workDir, false, android) + IntellijIDEAIntegration.apply(workDir, false, android, applyToProject) } catch (e: IntellijIDEAIntegration.ProjectNotFoundException) { System.err.println(".idea directory not found. " + "Are you sure you are inside project root directory?") @@ -449,15 +518,15 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s System.err.println("(if you experience any issues please report them at https://github.com/shyiko/ktlint)") } - fun hex(input: ByteArray) = BigInteger(MessageDigest.getInstance("SHA-256").digest(input)).toString(16) + private fun hex(input: ByteArray) = BigInteger(MessageDigest.getInstance("SHA-256").digest(input)).toString(16) // 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 (~/) - fun expandTilde(path: String) = path.replaceFirst(Regex("^~"), System.getProperty("user.home")) + private fun expandTilde(path: String) = path.replaceFirst(Regex("^~"), System.getProperty("user.home")) - fun <T> List<T>.head(limit: Int) = if (limit == size) this else this.subList(0, limit) + private fun <T> List<T>.head(limit: Int) = if (limit == size) this else this.subList(0, limit) - fun buildDependencyResolver(): MavenDependencyResolver { + private fun buildDependencyResolver(): MavenDependencyResolver { val mavenLocal = File(File(System.getProperty("user.home"), ".m2"), "repository") mavenLocal.mkdirsOrFail() val dependencyResolver = MavenDependencyResolver( @@ -482,7 +551,7 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s val url = repository.substring(colon + 1) RemoteRepository.Builder(id, "default", url).build() }, - forceUpdate + forceUpdate == true ) if (debug) { dependencyResolver.setTransferEventListener { e -> @@ -493,14 +562,15 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s return dependencyResolver } - fun loadJARs(dependencyResolver: MavenDependencyResolver, artifacts: List<String>) { + // fixme: isn't going to work on JDK 9 + private fun loadJARs(dependencyResolver: Lazy<MavenDependencyResolver>, artifacts: List<String>) { (ClassLoader.getSystemClassLoader() as java.net.URLClassLoader) .addURLs(artifacts.flatMap { artifact -> if (debug) { System.err.println("[DEBUG] Resolving $artifact") } val result = try { - dependencyResolver.resolve(DefaultArtifact(artifact)).map { it.toURI().toURL() } + dependencyResolver.value.resolve(DefaultArtifact(artifact)).map { it.toURI().toURL() } } catch (e: IllegalArgumentException) { val file = File(expandTilde(artifact)) if (!file.exists()) { @@ -518,11 +588,25 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s if (debug) { result.forEach { url -> System.err.println("[DEBUG] Loading $url") } } + if (!skipClasspathCheck) { + if (result.any { it.toString().substringAfterLast("/").startsWith("ktlint-core-") }) { + System.err.println("\"$artifact\" appears to have a runtime/compile dependency on \"ktlint-core\".\n" + + "Please inform the author that \"com.github.shyiko:ktlint*\" should be marked " + + "compileOnly (Gradle) / provided (Maven).\n" + + "(to suppress this warning use --skip-classpath-check)") + } + if (result.any { it.toString().substringAfterLast("/").startsWith("kotlin-stdlib-") }) { + System.err.println("\"$artifact\" appears to have a runtime/compile dependency on \"kotlin-stdlib\".\n" + + "Please inform the author that \"org.jetbrains.kotlin:kotlin-stdlib*\" should be marked " + + "compileOnly (Gradle) / provided (Maven).\n" + + "(to suppress this warning use --skip-classpath-check)") + } + } result }) } - fun parseQuery(query: String) = query.split("&") + private fun parseQuery(query: String) = query.split("&") .fold(LinkedHashMap<String, String>()) { map, s -> if (!s.isEmpty()) { s.split("=", limit = 2).let { e -> map.put(e[0], @@ -531,24 +615,42 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s map } - 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 + 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) + } - fun format(fileName: String, text: String, ruleSets: Iterable<RuleSet>, userData: Map<String, String>, - cb: (e: LintError, corrected: Boolean) -> Unit): String = - if (fileName.endsWith(".kt", ignoreCase = true)) KtLint.format(text, ruleSets, userData, cb) else + private fun format( + fileName: String, + text: String, + ruleSets: Iterable<RuleSet>, + userData: Map<String, String>, + cb: (e: LintError, corrected: Boolean) -> Unit + ): String = + if (fileName.endsWith(".kt", ignoreCase = true)) { + KtLint.format(text, ruleSets, userData, cb) + } else { KtLint.formatScript(text, ruleSets, userData, cb) + } - fun java.net.URLClassLoader.addURLs(url: Iterable<java.net.URL>) { + private fun java.net.URLClassLoader.addURLs(url: Iterable<java.net.URL>) { val method = java.net.URLClassLoader::class.java.getDeclaredMethod("addURL", java.net.URL::class.java) method.isAccessible = true url.forEach { method.invoke(this, it) } } - fun <T>Sequence<Callable<T>>.parallel(cb: (T) -> Unit, - numberOfThreads: Int = Runtime.getRuntime().availableProcessors()) { + private fun <T> Sequence<Callable<T>>.parallel( + cb: (T) -> Unit, + numberOfThreads: Int = Runtime.getRuntime().availableProcessors() + ) { val q = ArrayBlockingQueue<Future<T>>(numberOfThreads) val pill = object : Future<T> { |