Command-line tool for JS DCE

This commit is contained in:
Alexey Andreev
2017-04-24 17:20:14 +03:00
parent ca7062d776
commit 8a8fdf1968
46 changed files with 559 additions and 207 deletions
@@ -19,15 +19,14 @@ package org.jetbrains.kotlin.cli.common;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.util.Disposer;
import kotlin.collections.ArraysKt;
import org.fusesource.jansi.AnsiConsole;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.cli.common.arguments.ArgumentParseErrors;
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments;
import org.jetbrains.kotlin.cli.common.arguments.ParseCommandLineArgumentsKt;
import org.jetbrains.kotlin.cli.common.messages.*;
import org.jetbrains.kotlin.cli.common.messages.GroupingMessageCollector;
import org.jetbrains.kotlin.cli.common.messages.MessageCollector;
import org.jetbrains.kotlin.cli.common.messages.MessageCollectorUtil;
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer;
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler;
import org.jetbrains.kotlin.cli.jvm.compiler.CompileEnvironmentException;
import org.jetbrains.kotlin.cli.jvm.compiler.CompilerJarLocator;
import org.jetbrains.kotlin.config.*;
import org.jetbrains.kotlin.progress.CompilationCanceledException;
@@ -39,19 +38,12 @@ import java.io.PrintStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import static org.jetbrains.kotlin.cli.common.ExitCode.*;
import static org.jetbrains.kotlin.cli.common.environment.UtilKt.setIdeaIoUseFallback;
import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.*;
public abstract class CLICompiler<A extends CommonCompilerArguments> {
@NotNull
public ExitCode exec(@NotNull PrintStream errStream, @NotNull String... args) {
return exec(errStream, Services.EMPTY, MessageRenderer.PLAIN_RELATIVE_PATHS, args);
}
public abstract class CLICompiler<A extends CommonCompilerArguments> extends CLITool<A> {
// Used in CompilerRunnerUtil#invokeExecMethod, in Eclipse plugin (KotlinCLICompiler) and in kotlin-gradle-plugin (GradleCompilerRunner)
@NotNull
public ExitCode execAndOutputXml(@NotNull PrintStream errStream, @NotNull Services services, @NotNull String... args) {
@@ -65,90 +57,9 @@ public abstract class CLICompiler<A extends CommonCompilerArguments> {
return exec(errStream, Services.EMPTY, MessageRenderer.PLAIN_FULL_PATHS, args);
}
@Nullable
private A parseArguments(@NotNull MessageCollector messageCollector, @NotNull String[] args) {
try {
A arguments = createArguments();
parseArguments(args, arguments);
return arguments;
}
catch (IllegalArgumentException e) {
throw e;
}
catch (Throwable t) {
messageCollector.report(EXCEPTION, OutputMessageUtil.renderException(t), null);
return null;
}
}
// Used in kotlin-maven-plugin (KotlinCompileMojoBase) and in kotlin-gradle-plugin (KotlinJvmOptionsImpl, KotlinJsOptionsImpl)
public void parseArguments(@NotNull String[] args, @NotNull A arguments) {
ParseCommandLineArgumentsKt.parseCommandLineArguments(args, arguments);
String message = ParseCommandLineArgumentsKt.validateArguments(arguments.errors);
if (message != null) {
throw new IllegalArgumentException(message);
}
}
@NotNull
protected abstract A createArguments();
@NotNull
private ExitCode exec(
@NotNull PrintStream errStream,
@NotNull Services services,
@NotNull MessageRenderer messageRenderer,
@NotNull String[] args
) {
K2JVMCompiler.Companion.resetInitStartTime();
MessageCollector parseArgumentsCollector = new PrintingMessageCollector(errStream, messageRenderer, false);
A arguments;
try {
arguments = parseArguments(parseArgumentsCollector, args);
if (arguments == null) return INTERNAL_ERROR;
}
catch (IllegalArgumentException e) {
parseArgumentsCollector.report(ERROR, e.getMessage(), null);
parseArgumentsCollector.report(INFO, "Use -help for more information", null);
return COMPILATION_ERROR;
}
if (arguments.help || arguments.extraHelp) {
Usage.print(errStream, this, arguments);
return OK;
}
MessageCollector collector = new PrintingMessageCollector(errStream, messageRenderer, arguments.verbose);
try {
if (PlainTextMessageRenderer.COLOR_ENABLED) {
AnsiConsole.systemInstall();
}
errStream.print(messageRenderer.renderPreamble());
return exec(collector, services, arguments);
}
finally {
errStream.print(messageRenderer.renderConclusion());
if (PlainTextMessageRenderer.COLOR_ENABLED) {
AnsiConsole.systemUninstall();
}
}
}
// Used in kotlin-maven-plugin (KotlinCompileMojoBase)
@NotNull
public ExitCode exec(@NotNull MessageCollector messageCollector, @NotNull Services services, @NotNull A arguments) {
printVersionIfNeeded(messageCollector, arguments);
if (arguments.suppressWarnings) {
messageCollector = new FilteringMessageCollector(messageCollector, Predicate.isEqual(WARNING));
}
reportArgumentParseProblems(messageCollector, arguments.errors);
@Override
public ExitCode execImpl(@NotNull MessageCollector messageCollector, @NotNull Services services, @NotNull A arguments) {
GroupingMessageCollector groupingCollector = new GroupingMessageCollector(messageCollector);
CompilerConfiguration configuration = new CompilerConfiguration();
@@ -328,63 +239,10 @@ public abstract class CLICompiler<A extends CommonCompilerArguments> {
@NotNull CompilerConfiguration configuration, @NotNull A arguments, @NotNull Services services
);
private static void reportArgumentParseProblems(@NotNull MessageCollector collector, @NotNull ArgumentParseErrors errors) {
for (String flag : errors.getUnknownExtraFlags()) {
collector.report(STRONG_WARNING, "Flag is not supported by this version of the compiler: " + flag, null);
}
for (String argument : errors.getExtraArgumentsPassedInObsoleteForm()) {
collector.report(STRONG_WARNING, "Advanced option value is passed in an obsolete form. Please use the '=' character " +
"to specify the value: " + argument + "=...", null);
}
for (Map.Entry<String, String> argument : errors.getDuplicateArguments().entrySet()) {
collector.report(STRONG_WARNING, "Argument " + argument.getKey() + " is passed multiple times. " +
"Only the last value will be used: " + argument.getValue(), null);
}
}
@NotNull
protected abstract ExitCode doExecute(
@NotNull A arguments,
@NotNull CompilerConfiguration configuration,
@NotNull Disposable rootDisposable
);
private void printVersionIfNeeded(@NotNull MessageCollector messageCollector, @NotNull A arguments) {
if (arguments.version) {
messageCollector.report(
CompilerMessageSeverity.INFO,
executableScriptFileName() + " " + KotlinCompilerVersion.VERSION +
" (JRE " + System.getProperty("java.runtime.version") + ")",
null
);
}
}
@NotNull
public abstract String executableScriptFileName();
/**
* Useful main for derived command line tools
*/
public static void doMain(@NotNull CLICompiler compiler, @NotNull String[] args) {
// We depend on swing (indirectly through PSI or something), so we want to declare headless mode,
// to avoid accidentally starting the UI thread
System.setProperty("java.awt.headless", "true");
ExitCode exitCode = doMainNoExit(compiler, args);
if (exitCode != OK) {
System.exit(exitCode.getCode());
}
}
@SuppressWarnings("UseOfSystemOutOrSystemErr")
@NotNull
public static ExitCode doMainNoExit(@NotNull CLICompiler compiler, @NotNull String[] args) {
try {
return compiler.exec(System.err, args);
}
catch (CompileEnvironmentException e) {
System.err.println(e.getMessage());
return INTERNAL_ERROR;
}
}
}
@@ -0,0 +1,179 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.cli.common
import org.fusesource.jansi.AnsiConsole
import org.jetbrains.kotlin.cli.common.arguments.ArgumentParseErrors
import org.jetbrains.kotlin.cli.common.arguments.CommonToolArguments
import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments
import org.jetbrains.kotlin.cli.common.arguments.validateArguments
import org.jetbrains.kotlin.cli.common.messages.*
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import org.jetbrains.kotlin.cli.jvm.compiler.CompileEnvironmentException
import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.config.Services
import java.io.PrintStream
import java.util.function.Predicate
abstract class CLITool<A : CommonToolArguments> {
fun exec(errStream: PrintStream, vararg args: String): ExitCode {
return exec(errStream, Services.EMPTY, MessageRenderer.PLAIN_RELATIVE_PATHS, args)
}
protected fun exec(
errStream: PrintStream,
services: Services,
messageRenderer: MessageRenderer,
args: Array<out String>
): ExitCode {
K2JVMCompiler.resetInitStartTime()
val parseArgumentsCollector = PrintingMessageCollector(errStream, messageRenderer, false)
val arguments = try {
parseArguments(parseArgumentsCollector, args) ?: return ExitCode.INTERNAL_ERROR
}
catch (e: IllegalArgumentException) {
parseArgumentsCollector.report(CompilerMessageSeverity.ERROR, e.message!!, null)
parseArgumentsCollector.report(CompilerMessageSeverity.INFO, "Use -help for more information", null)
return ExitCode.COMPILATION_ERROR
}
if (arguments.help || arguments.extraHelp) {
Usage.print(errStream, this, arguments)
return ExitCode.OK
}
val collector = PrintingMessageCollector(errStream, messageRenderer, arguments.verbose)
try {
if (PlainTextMessageRenderer.COLOR_ENABLED) {
AnsiConsole.systemInstall()
}
errStream.print(messageRenderer.renderPreamble())
return exec(collector, services, arguments)
}
finally {
errStream.print(messageRenderer.renderConclusion())
if (PlainTextMessageRenderer.COLOR_ENABLED) {
AnsiConsole.systemUninstall()
}
}
}
fun exec(messageCollector: MessageCollector, services: Services, arguments: A): ExitCode {
printVersionIfNeeded(messageCollector, arguments)
val fixedMessageCollector = if (arguments.suppressWarnings) {
FilteringMessageCollector(messageCollector, Predicate.isEqual(CompilerMessageSeverity.WARNING))
}
else {
messageCollector
}
reportArgumentParseProblems(fixedMessageCollector, arguments.errors)
return execImpl(fixedMessageCollector, services, arguments)
}
// Used in kotlin-maven-plugin (KotlinCompileMojoBase)
protected abstract fun execImpl(messageCollector: MessageCollector, services: Services, arguments: A): ExitCode
private fun parseArguments(messageCollector: MessageCollector, args: Array<out String>): A? {
return try {
createArguments().also { parseArguments(args, it) }
}
catch (e: IllegalArgumentException) {
throw e
}
catch (t: Throwable) {
messageCollector.report(CompilerMessageSeverity.EXCEPTION, OutputMessageUtil.renderException(t), null)
null
}
}
protected abstract fun createArguments(): A
// Used in kotlin-maven-plugin (KotlinCompileMojoBase) and in kotlin-gradle-plugin (KotlinJvmOptionsImpl, KotlinJsOptionsImpl)
fun parseArguments(args: Array<out String>, arguments: A) {
parseCommandLineArguments(args, arguments)
val message = validateArguments(arguments.errors)
if (message != null) {
throw IllegalArgumentException(message)
}
}
private fun reportArgumentParseProblems(collector: MessageCollector, errors: ArgumentParseErrors) {
for (flag in errors.unknownExtraFlags) {
collector.report(
CompilerMessageSeverity.STRONG_WARNING,
"Flag is not supported by this version of the compiler: " + flag, null)
}
for (argument in errors.extraArgumentsPassedInObsoleteForm) {
collector.report(
CompilerMessageSeverity.STRONG_WARNING,
"Advanced option value is passed in an obsolete form. Please use the '=' character " +
"to specify the value: " + argument + "=...", null)
}
for ((key, value) in errors.duplicateArguments) {
collector.report(
CompilerMessageSeverity.STRONG_WARNING,
"Argument $key is passed multiple times. Only the last value will be used: $value", null)
}
}
protected fun <A : CommonToolArguments> printVersionIfNeeded(messageCollector: MessageCollector, arguments: A) {
if (!arguments.version) return
if (arguments.version) {
val jreVersion = System.getProperty("java.runtime.version")
messageCollector.report(CompilerMessageSeverity.INFO,
"${executableScriptFileName()} ${KotlinCompilerVersion.VERSION} (JRE $jreVersion)",
null
)
}
}
abstract fun executableScriptFileName(): String
companion object {
/**
* Useful main for derived command line tools
*/
@JvmStatic
fun doMain(compiler: CLITool<*>, args: Array<String>) {
// We depend on swing (indirectly through PSI or something), so we want to declare headless mode,
// to avoid accidentally starting the UI thread
System.setProperty("java.awt.headless", "true")
val exitCode = doMainNoExit(compiler, args)
if (exitCode != ExitCode.OK) {
System.exit(exitCode.code)
}
}
@JvmStatic
fun doMainNoExit(compiler: CLITool<*>, args: Array<String>): ExitCode {
try {
return compiler.exec(System.err, *args)
}
catch (e: CompileEnvironmentException) {
System.err.println(e.message)
return ExitCode.INTERNAL_ERROR
}
}
}
}
@@ -19,7 +19,7 @@ package org.jetbrains.kotlin.cli.common;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.cli.common.arguments.Argument;
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments;
import org.jetbrains.kotlin.cli.common.arguments.CommonToolArguments;
import org.jetbrains.kotlin.cli.common.arguments.ParseCommandLineArgumentsKt;
import java.io.PrintStream;
@@ -29,8 +29,8 @@ class Usage {
// The magic number 29 corresponds to the similar padding width in javac and scalac command line compilers
private static final int OPTION_NAME_PADDING_WIDTH = 29;
public static <A extends CommonCompilerArguments> void print(
@NotNull PrintStream target, @NotNull CLICompiler<A> compiler, @NotNull A arguments
public static <A extends CommonToolArguments> void print(
@NotNull PrintStream target, @NotNull CLITool<A> compiler, @NotNull A arguments
) {
target.println("Usage: " + compiler.executableScriptFileName() + " <options> <source files>");
target.println("where " + (arguments.extraHelp ? "advanced" : "possible") + " options include:");
@@ -14,20 +14,91 @@
* limitations under the License.
*/
@file:JvmName("K2JSDce")
package org.jetbrains.kotlin.cli.js.dce
import org.jetbrains.kotlin.cli.common.CLITool
import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.arguments.K2JSDceArguments
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.config.Services
import org.jetbrains.kotlin.js.dce.DeadCodeElimination
import org.jetbrains.kotlin.js.dce.InputFile
import org.jetbrains.kotlin.js.dce.extractRoots
import org.jetbrains.kotlin.js.dce.printTree
import java.io.File
fun main(args: Array<String>) {
val files = args.map { InputFile(it) }
val nodes = DeadCodeElimination.run(files, emptySet()) { println(it) }
class K2JSDce : CLITool<K2JSDceArguments>() {
override fun createArguments(): K2JSDceArguments = K2JSDceArguments()
println()
for (node in nodes.extractRoots()) {
printTree(node, { println(it) }, printNestedMembers = false, showLocations = true)
override fun execImpl(messageCollector: MessageCollector, services: Services, arguments: K2JSDceArguments): ExitCode {
val baseDir = File(arguments.outputDirectory ?: "min")
val files = arguments.freeArgs.map { arg ->
val parts = arg.split(File.pathSeparator, ignoreCase = false, limit = 2)
val inputName = parts[0]
val moduleName = parts.getOrNull(1) ?: ""
val resolvedModuleName = if (!moduleName.isEmpty()) moduleName else File(inputName).nameWithoutExtension
InputFile(inputName, File(baseDir, resolvedModuleName + ".js").absolutePath, resolvedModuleName)
}
if (files.isEmpty() && !arguments.version) {
messageCollector.report(CompilerMessageSeverity.ERROR, "no source files")
return ExitCode.COMPILATION_ERROR
}
if (!checkSourceFiles(messageCollector, files)) {
return ExitCode.COMPILATION_ERROR
}
val dceResult = DeadCodeElimination.run(files, emptySet()) {
messageCollector.report(CompilerMessageSeverity.LOGGING, it)
}
val nodes = dceResult.reachableNodes
val reachabilitySeverity = if (arguments.printReachabilityInfo) CompilerMessageSeverity.INFO else CompilerMessageSeverity.LOGGING
messageCollector.report(reachabilitySeverity, "")
for (node in nodes.extractRoots()) {
printTree(node, { messageCollector.report(reachabilitySeverity, it) },
printNestedMembers = false, showLocations = true)
}
return ExitCode.OK
}
private fun checkSourceFiles(messageCollector: MessageCollector, files: List<InputFile>): Boolean {
return files.fold(true) { ok, file ->
val inputFile = File(file.path)
val outputFile = File(file.outputPath)
val inputOk = when {
!inputFile.exists() -> {
messageCollector.report(CompilerMessageSeverity.ERROR, "source file or directory not found: " + file.path)
false
}
inputFile.isDirectory -> {
messageCollector.report(CompilerMessageSeverity.ERROR, "input file '" + file.path + "' is a directory")
false
}
else -> true
}
val outputOk = when {
outputFile.exists() && outputFile.isDirectory -> {
messageCollector.report(CompilerMessageSeverity.ERROR, "cannot open output file '${outputFile.path}': is a directory")
false
}
else -> true
}
ok and inputOk and outputOk
}
}
override fun executableScriptFileName(): String = "kotlin-dce-js"
companion object {
@JvmStatic
fun main(args: Array<String>) {
CLITool.doMain(K2JSDce(), args)
}
}
}
@@ -20,6 +20,7 @@ import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.Disposable
import org.jetbrains.kotlin.cli.common.CLICompiler
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.CLITool
import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.ExitCode.*
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
@@ -309,7 +310,7 @@ class K2JVMCompiler : CLICompiler<K2JVMCompilerArguments>() {
}
@JvmStatic fun main(args: Array<String>) {
CLICompiler.doMain(K2JVMCompiler(), args)
CLITool.doMain(K2JVMCompiler(), args)
}
fun reportPerf(configuration: CompilerConfiguration, message: String) {