Compare commits

...

9 Commits

Author SHA1 Message Date
Anton Bannykh
76d2d9170a notes 2017-05-02 21:54:26 +03:00
Alexey Andreev
862ae2a247 Add simple test infrastructure for JS DCE
Add test infrastructure that allows to test handwritten JS
to test various small aspects of DCE, as opposed to box tests
which test kotlin stdlib + kotlin generated examples
2017-04-27 15:36:53 +03:00
Alexey Andreev
d7f294aa6c Add -include command line option to JS DCE tool 2017-04-27 15:36:53 +03:00
Alexey Andreev
1c53cff0a0 Suppress some DCE tests that can't pass 2017-04-27 15:36:52 +03:00
Alexey Andreev
4b5278f3a3 Generate MINIFIER_THRESHOLD directive on JS box tests 2017-04-27 15:36:52 +03:00
Alexey Andreev
dc8340d1b6 Improve test infrastructure for JS DCE tool
Introduce directives to assert minification rate (MINIFIER_THRESHOLD)
and to skip DCE test at all (SKIP_MINIFIER).
Allow to run DCE on some of the box tests by default.
2017-04-27 15:36:51 +03:00
Alexey Andreev
701263b429 Command-line tool for JS DCE 2017-04-27 15:36:51 +03:00
Alexey Andreev
f4040743f3 Fix JS parser to properly handle "." <keyword> sequence 2017-04-27 15:36:50 +03:00
Alexey Andreev
70c68c15e7 Prototyping DCE 2017-04-27 15:36:50 +03:00
1234 changed files with 3484 additions and 259 deletions

1
.idea/modules.xml generated
View File

@@ -67,6 +67,7 @@
<module fileurl="file://$PROJECT_DIR$/jps-plugin/jps-plugin.iml" filepath="$PROJECT_DIR$/jps-plugin/jps-plugin.iml" group="ide/jps" />
<module fileurl="file://$PROJECT_DIR$/jps-plugin/jps-tests/jps-tests.iml" filepath="$PROJECT_DIR$/jps-plugin/jps-tests/jps-tests.iml" group="ide/jps" />
<module fileurl="file://$PROJECT_DIR$/js/js.ast/js.ast.iml" filepath="$PROJECT_DIR$/js/js.ast/js.ast.iml" group="compiler/js" />
<module fileurl="file://$PROJECT_DIR$/js/js.dce/js.dce.iml" filepath="$PROJECT_DIR$/js/js.dce/js.dce.iml" group="compiler/js" />
<module fileurl="file://$PROJECT_DIR$/js/js.frontend/js.frontend.iml" filepath="$PROJECT_DIR$/js/js.frontend/js.frontend.iml" group="compiler/js" />
<module fileurl="file://$PROJECT_DIR$/js/js.inliner/js.inliner.iml" filepath="$PROJECT_DIR$/js/js.inliner/js.inliner.iml" group="compiler/js" />
<module fileurl="file://$PROJECT_DIR$/js/js.parser/js.parser.iml" filepath="$PROJECT_DIR$/js/js.parser/js.parser.iml" group="compiler/js" />

View File

@@ -8,7 +8,7 @@
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="package" />
<option name="VM_PARAMETERS" value="-ea -XX:+HeapDumpOnOutOfMemoryError -Xmx1250m -XX:+UseCodeCacheFlushing -Djna.nosys=true" />
<option name="VM_PARAMETERS" value="-ea -XX:+HeapDumpOnOutOfMemoryError -Xmx1250m -XX:+UseCodeCacheFlushing -Djna.nosys=true -Dkotlin.js.generateThreshold=true" />
<option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$" />
<option name="ENV_VARIABLES" />

View File

@@ -107,6 +107,7 @@
<include name="js/js.inliner/src"/>
<include name="js/js.parser/src"/>
<include name="js/js.serializer/src"/>
<include name="js/js.dce/src"/>
<include name="plugins/annotation-collector/src"/>
</dirset>
@@ -146,6 +147,7 @@
<include name="js.inliner/**"/>
<include name="js.parser/**"/>
<include name="js.serializer/**"/>
<include name="js.dce/**"/>
</patternset>
<path id="compilerSources.path">
@@ -258,6 +260,7 @@
<fileset dir="js/js.inliner/src"/>
<fileset dir="js/js.parser/src"/>
<fileset dir="js/js.serializer/src"/>
<fileset dir="js/js.dce/src"/>
<zipfileset file="${kotlin-home}/build.txt" prefix="META-INF"/>
<manifest>

22
compiler/cli/bin/kotlinc-dce-js Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Copyright 2010-2015 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.
export KOTLIN_COMPILER=org.jetbrains.kotlin.cli.js.dce.K2JSDce
DIR="${BASH_SOURCE[0]%/*}"
: ${DIR:="."}
"${DIR}"/kotlinc "$@"

View File

@@ -0,0 +1,20 @@
@echo off
rem Copyright 2010-2015 JetBrains s.r.o.
rem
rem Licensed under the Apache License, Version 2.0 (the "License");
rem you may not use this file except in compliance with the License.
rem You may obtain a copy of the License at
rem
rem http://www.apache.org/licenses/LICENSE-2.0
rem
rem Unless required by applicable law or agreed to in writing, software
rem distributed under the License is distributed on an "AS IS" BASIS,
rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
rem See the License for the specific language governing permissions and
rem limitations under the License.
setlocal
set _KOTLIN_COMPILER=org.jetbrains.kotlin.cli.js.dce.K2JSDce
call %~dps0kotlinc.bat %*

View File

@@ -16,15 +16,9 @@
package org.jetbrains.kotlin.cli.common.arguments;
import com.intellij.util.SmartList;
import org.jetbrains.annotations.NotNull;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public abstract class CommonCompilerArguments implements Serializable {
public abstract class CommonCompilerArguments extends CommonToolArguments {
public static final long serialVersionUID = 0L;
public static final String PLUGIN_OPTION_FORMAT = "plugin:<pluginId>:<optionName>=<value>";
@@ -45,23 +39,6 @@ public abstract class CommonCompilerArguments implements Serializable {
)
public String apiVersion;
@GradleOption(DefaultValues.BooleanFalseDefault.class)
@Argument(value = "-nowarn", description = "Generate no warnings")
public boolean suppressWarnings;
@GradleOption(DefaultValues.BooleanFalseDefault.class)
@Argument(value = "-verbose", description = "Enable verbose logging output")
public boolean verbose;
@Argument(value = "-version", description = "Display compiler version")
public boolean version;
@Argument(value = "-help", shortName = "-h", description = "Print a synopsis of standard options")
public boolean help;
@Argument(value = "-X", description = "Print a synopsis of advanced options")
public boolean extraHelp;
@Argument(value = "-P", valueDescription = PLUGIN_OPTION_FORMAT, description = "Pass an option to a plugin")
public String[] pluginOptions;
@@ -107,16 +84,13 @@ public abstract class CommonCompilerArguments implements Serializable {
)
public String coroutinesState = WARN;
public List<String> freeArgs = new SmartList<>();
public transient ArgumentParseErrors errors = new ArgumentParseErrors();
@NotNull
public static CommonCompilerArguments createDefaultInstance() {
return new DummyImpl();
}
@NotNull
@Override
public String executableScriptFileName() {
return "kotlinc";
}

View File

@@ -0,0 +1,57 @@
/*
* 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.arguments
import com.intellij.util.SmartList
import java.io.Serializable
abstract class CommonToolArguments : Serializable {
companion object {
const val serialVersionUID = 0L
}
@field:JvmField
var freeArgs: MutableList<String> = SmartList()
@Transient
@JvmField
val errors = ArgumentParseErrors()
@field:Argument(value = "-help", shortName = "-h", description = "Print a synopsis of standard options")
@JvmField
var help: Boolean = false
@field:Argument(value = "-X", description = "Print a synopsis of advanced options")
@JvmField
var extraHelp: Boolean = false
@field:Argument(value = "-version", description = "Display compiler version")
@JvmField
var version: Boolean = false
@GradleOption(DefaultValues.BooleanFalseDefault::class)
@field:Argument(value = "-verbose", description = "Enable verbose logging output")
@JvmField
var verbose: Boolean = false
@GradleOption(DefaultValues.BooleanFalseDefault::class)
@field:Argument(value = "-nowarn", description = "Generate no warnings")
@JvmField
var suppressWarnings: Boolean = false
abstract fun executableScriptFileName(): String
}

View File

@@ -0,0 +1,42 @@
/*
* 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.arguments
class K2JSDceArguments : CommonToolArguments() {
companion object {
const val serialVersionUID = 0L
}
@field:GradleOption(DefaultValues.StringNullDefault::class)
@field:Argument(value = "-output-dir", valueDescription = "<path>", description = "Output directory")
@JvmField
var outputDirectory: String? = null
@field:Argument(
value = "-include",
valueDescription = "<fully.qualified.name[,]>",
description = "List of fully-qualified names of declarations that should be included into resulting module")
@JvmField
var includedDeclarations: Array<String>? = null
@field:GradleOption(DefaultValues.BooleanFalseDefault::class)
@field:Argument(value = "-Xprint-reachability-info", description = "Print declarations marked as reachable")
@JvmField
var printReachabilityInfo: Boolean = false
override fun executableScriptFileName(): String = "kotlinc-dce-js"
}

View File

@@ -50,7 +50,7 @@ data class ArgumentParseErrors(
)
// Parses arguments in the passed [result] object, or throws an [IllegalArgumentException] with the message to be displayed to the user
fun <A : CommonCompilerArguments> parseCommandLineArguments(args: Array<String>, result: A) {
fun <A : CommonToolArguments> parseCommandLineArguments(args: Array<out String>, result: A) {
data class ArgumentField(val field: Field, val argument: Argument)
val fields = result::class.java.fields.mapNotNull { field ->
@@ -118,7 +118,7 @@ fun <A : CommonCompilerArguments> parseCommandLineArguments(args: Array<String>,
}
}
private fun <A : CommonCompilerArguments> updateField(field: Field, result: A, value: Any, delimiter: String) {
private fun <A : CommonToolArguments> updateField(field: Field, result: A, value: Any, delimiter: String) {
when (field.type) {
Boolean::class.java, String::class.java -> field.set(result, value)
Array<String>::class.java -> {

View File

@@ -23,5 +23,6 @@
<orderEntry type="module" module-name="annotation-collector" />
<orderEntry type="module" module-name="builtins-serializer" />
<orderEntry type="module" module-name="frontend.script" />
<orderEntry type="module" module-name="js.dce" />
</component>
</module>

View File

@@ -19,15 +19,15 @@ 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.FilteringMessageCollector;
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.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;
@@ -35,7 +35,6 @@ import org.jetbrains.kotlin.progress.CompilationCanceledStatus;
import org.jetbrains.kotlin.progress.ProgressIndicatorAndCompilationCanceledStatus;
import org.jetbrains.kotlin.utils.StringsKt;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -45,110 +44,10 @@ 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> {
public abstract class CLICompiler<A extends CommonCompilerArguments> extends CLITool<A> {
@NotNull
public ExitCode exec(@NotNull PrintStream errStream, @NotNull String... args) {
return exec(errStream, Services.EMPTY, MessageRenderer.PLAIN_RELATIVE_PATHS, args);
}
// 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) {
return exec(errStream, services, MessageRenderer.XML, args);
}
// Used via reflection in KotlinCompilerBaseTask
@SuppressWarnings("UnusedDeclaration")
@NotNull
public ExitCode execFullPathsInMessages(@NotNull PrintStream errStream, @NotNull String[] args) {
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, 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();
@@ -325,55 +224,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) return;
messageCollector.report(INFO, "Kotlin Compiler version " + KotlinCompilerVersion.VERSION, null);
}
/**
* 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;
}
}
}

View File

@@ -0,0 +1,181 @@
/*
* 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)
}
// Used in CompilerRunnerUtil#invokeExecMethod, in Eclipse plugin (KotlinCLICompiler) and in kotlin-gradle-plugin (GradleCompilerRunner)
fun execAndOutputXml(errStream: PrintStream, services: Services, vararg args: String): ExitCode {
return exec(errStream, services, MessageRenderer.XML, args)
}
// Used via reflection in KotlinCompilerBaseTask
fun execFullPathsInMessages(errStream: PrintStream, args: Array<String>): ExitCode {
return exec(errStream, Services.EMPTY, MessageRenderer.PLAIN_FULL_PATHS, args)
}
private 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, 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
messageCollector.report(CompilerMessageSeverity.INFO, "Kotlin Compiler version " + KotlinCompilerVersion.VERSION, null)
}
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
}
}
}
}

View File

@@ -20,16 +20,17 @@ 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;
import java.lang.reflect.Field;
class Usage {
public 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 void print(@NotNull PrintStream target, @NotNull CommonCompilerArguments arguments) {
public static void print(@NotNull PrintStream target, @NotNull CommonToolArguments arguments) {
target.println("Usage: " + arguments.executableScriptFileName() + " <options> <source files>");
target.println("where " + (arguments.extraHelp ? "advanced" : "possible") + " options include:");
for (Class<?> clazz = arguments.getClass(); clazz != null; clazz = clazz.getSuperclass()) {

View File

@@ -0,0 +1,104 @@
/*
* 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.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
class K2JSDce : CLITool<K2JSDceArguments>() {
override fun createArguments(): K2JSDceArguments = K2JSDceArguments()
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 includedDeclarations = arguments.includedDeclarations.orEmpty().toSet()
val dceResult = DeadCodeElimination.run(files, includedDeclarations) {
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.name)
val outputFile = File(file.outputName)
val inputOk = when {
!inputFile.exists() -> {
messageCollector.report(CompilerMessageSeverity.ERROR, "source file or directory not found: " + file.name)
false
}
inputFile.isDirectory -> {
messageCollector.report(CompilerMessageSeverity.ERROR, "input file '" + file.name + "' 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
}
}
companion object {
@JvmStatic
fun main(args: Array<String>) {
CLITool.doMain(K2JSDce(), args)
}
}
}

View File

@@ -19,6 +19,7 @@ package org.jetbrains.kotlin.cli.jvm
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
@@ -276,7 +277,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) {

View File

@@ -0,0 +1 @@
-help

View File

@@ -0,0 +1,11 @@
Usage: kotlinc-dce-js <options> <source files>
where possible options include:
-output-dir <path> Output directory
-include <fully.qualified.name[,]>
List of fully-qualified names of declarations that should be included into resulting module
-help (-h) Print a synopsis of standard options
-X Print a synopsis of advanced options
-version Display compiler version
-verbose Enable verbose logging output
-nowarn Generate no warnings
OK

View File

@@ -0,0 +1,2 @@
-output-dir
$TESTDATA_DIR$/min

View File

@@ -0,0 +1,2 @@
error: no source files
COMPILATION_ERROR

20
compiler/testData/cli/js-dce/include.js vendored Normal file
View File

@@ -0,0 +1,20 @@
define(["exports"], function(exports) {
function foo() {
}
function bar() {
}
function baz() {
}
function ignore() {
}
baz();
exports.foo = foo;
exports.bar = bar;
exports.baz = baz;
exports.ignore = ignore;
});

View File

@@ -0,0 +1,6 @@
$TESTDATA_DIR$/include.js
-output-dir
$TEMP_DIR$/min
-include
global.foo,global.bar
-Xprint-reachability-info

View File

@@ -0,0 +1,11 @@
info:
info: <unknown>
info: global
info: bar (reachable)
info: foo (reachable)
info: include
info: baz (reachable from include.js:14)
info: module
info: exports
info: baz (reachable)
OK

View File

@@ -0,0 +1 @@
-X

View File

@@ -0,0 +1,6 @@
Usage: kotlinc-dce-js <options> <source files>
where advanced options include:
-Xprint-reachability-info Print declarations marked as reachable
Advanced options are non-standard and may be changed or removed without any notice.
OK

View File

View File

@@ -0,0 +1,4 @@
function bar() {
return 'bar';
}
console.log(bar());

View File

@@ -0,0 +1,3 @@
nonExistingSourceFile.js
-output-dir
$TEMP_DIR/min

View File

@@ -0,0 +1,2 @@
error: source file or directory not found: nonExistingSourceFile.js
COMPILATION_ERROR

View File

@@ -0,0 +1 @@
// ABSENT: min/nonExistingSourceFile.js

View File

@@ -0,0 +1,3 @@
$TESTDATA_DIR$
-output-dir
$TEMP_DIR/min

View File

@@ -0,0 +1,2 @@
error: input file 'compiler/testData/cli/js-dce' is a directory
COMPILATION_ERROR

View File

@@ -0,0 +1,3 @@
$TESTDATA_DIR$/simple.js
-output-dir
$TESTDATA_DIR$/min

View File

@@ -0,0 +1,2 @@
error: cannot open output file '$TESTDATA_DIR$/min/simple.js': is a directory
COMPILATION_ERROR

View File

@@ -0,0 +1 @@
// ABSENT: out.js

View File

@@ -0,0 +1,3 @@
$TESTDATA_DIR$/simple.js:bar
-output-dir
$TEMP_DIR$/min

View File

@@ -0,0 +1 @@
OK

View File

@@ -0,0 +1 @@
// EXISTS: min/bar.js

View File

@@ -0,0 +1,4 @@
$TESTDATA_DIR$/simple.js
-output-dir
$TEMP_DIR$/min
-Xprint-reachability-info

View File

@@ -0,0 +1,6 @@
info:
info: <unknown>
info: bar (reachable from simple.js:7)
info: console
info: log (reachable from simple.js:7)
OK

View File

@@ -0,0 +1 @@
// EXISTS: min/simple.js

View File

@@ -0,0 +1,3 @@
$TESTDATA_DIR$/simple.js
-output-dir
$TEMP_DIR$/min

View File

@@ -0,0 +1,7 @@
function foo() {
return "foo";
}
function bar() {
return "bar";
}
console.log(bar());

View File

@@ -0,0 +1 @@
OK

View File

@@ -0,0 +1 @@
// EXISTS: min/simple.js

View File

@@ -0,0 +1 @@
-version

View File

@@ -0,0 +1,2 @@
info: Kotlin Compiler version $VERSION$
OK

View File

@@ -13,11 +13,11 @@ where possible options include:
-output-postfix <path> Path to file which will be added to the end of output file
-language-version <version> Provide source compatibility with specified language version
-api-version <version> Allow to use declarations only from the specified version of bundled libraries
-nowarn Generate no warnings
-verbose Enable verbose logging output
-version Display compiler version
-help (-h) Print a synopsis of standard options
-X Print a synopsis of advanced options
-P plugin:<pluginId>:<optionName>=<value>
Pass an option to a plugin
-help (-h) Print a synopsis of standard options
-X Print a synopsis of advanced options
-version Display compiler version
-verbose Enable verbose logging output
-nowarn Generate no warnings
OK

View File

@@ -17,11 +17,11 @@ where possible options include:
-java-parameters Generate metadata for Java 1.8 reflection on method parameters
-language-version <version> Provide source compatibility with specified language version
-api-version <version> Allow to use declarations only from the specified version of bundled libraries
-nowarn Generate no warnings
-verbose Enable verbose logging output
-version Display compiler version
-help (-h) Print a synopsis of standard options
-X Print a synopsis of advanced options
-P plugin:<pluginId>:<optionName>=<value>
Pass an option to a plugin
-help (-h) Print a synopsis of standard options
-X Print a synopsis of advanced options
-version Display compiler version
-verbose Enable verbose logging output
-nowarn Generate no warnings
OK

View File

@@ -25,8 +25,10 @@ import kotlin.io.FilesKt;
import kotlin.text.Charsets;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.cli.common.CLICompiler;
import org.jetbrains.kotlin.cli.common.CLITool;
import org.jetbrains.kotlin.cli.common.ExitCode;
import org.jetbrains.kotlin.cli.js.K2JSCompiler;
import org.jetbrains.kotlin.cli.js.dce.K2JSDce;
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler;
import org.jetbrains.kotlin.config.KotlinCompilerVersion;
import org.jetbrains.kotlin.load.kotlin.JvmMetadataVersion;
@@ -48,12 +50,12 @@ import java.util.List;
public abstract class AbstractCliTest extends TestCaseWithTmpdir {
@NotNull
public static Pair<String, ExitCode> executeCompilerGrabOutput(@NotNull CLICompiler<?> compiler, @NotNull List<String> args) {
public static Pair<String, ExitCode> executeCompilerGrabOutput(@NotNull CLITool<?> compiler, @NotNull List<String> args) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
PrintStream origErr = System.err;
try {
System.setErr(new PrintStream(bytes));
ExitCode exitCode = CLICompiler.doMainNoExit(compiler, ArrayUtil.toStringArray(args));
ExitCode exitCode = CLITool.doMainNoExit(compiler, ArrayUtil.toStringArray(args));
return new Pair<>(bytes.toString("utf-8"), exitCode);
}
catch (Exception e) {
@@ -79,7 +81,7 @@ public abstract class AbstractCliTest extends TestCaseWithTmpdir {
return normalizedOutputWithoutExitCode + exitCode;
}
private void doTest(@NotNull String fileName, @NotNull CLICompiler<?> compiler) throws Exception {
private void doTest(@NotNull String fileName, @NotNull CLITool<?> compiler) throws Exception {
System.setProperty("java.awt.headless", "true");
Pair<String, ExitCode> outputAndExitCode = executeCompilerGrabOutput(compiler, readArgs(fileName, tmpdir.getPath()));
String actual = getNormalizedCompilerOutput(
@@ -153,6 +155,10 @@ public abstract class AbstractCliTest extends TestCaseWithTmpdir {
doTest(fileName, new K2JSCompiler());
}
protected void doJsDceTest(@NotNull String fileName) throws Exception {
doTest(fileName, new K2JSDce());
}
public static String removePerfOutput(String output) {
String[] lines = StringUtil.splitByLinesKeepSeparators(output);
StringBuilder result = new StringBuilder();

View File

@@ -539,4 +539,79 @@ public class CliTestGenerated extends AbstractCliTest {
doJsTest(fileName);
}
}
@TestMetadata("compiler/testData/cli/js-dce")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class Js_dce extends AbstractCliTest {
public void testAllFilesPresentInJs_dce() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/cli/js-dce"), Pattern.compile("^(.+)\\.args$"), TargetBackend.ANY, false);
}
@TestMetadata("dceHelp.args")
public void testDceHelp() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/dceHelp.args");
doJsDceTest(fileName);
}
@TestMetadata("emptySources.args")
public void testEmptySources() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/emptySources.args");
doJsDceTest(fileName);
}
@TestMetadata("includeDeclarations.args")
public void testIncludeDeclarations() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/includeDeclarations.args");
doJsDceTest(fileName);
}
@TestMetadata("jsExtraHelp.args")
public void testJsExtraHelp() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/jsExtraHelp.args");
doJsDceTest(fileName);
}
@TestMetadata("nonExistingSourcePath.args")
public void testNonExistingSourcePath() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/nonExistingSourcePath.args");
doJsDceTest(fileName);
}
@TestMetadata("notFile.args")
public void testNotFile() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/notFile.args");
doJsDceTest(fileName);
}
@TestMetadata("outputIsDirectory.args")
public void testOutputIsDirectory() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/outputIsDirectory.args");
doJsDceTest(fileName);
}
@TestMetadata("overrideOutputName.args")
public void testOverrideOutputName() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/overrideOutputName.args");
doJsDceTest(fileName);
}
@TestMetadata("printReachability.args")
public void testPrintReachability() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/printReachability.args");
doJsDceTest(fileName);
}
@TestMetadata("simple.args")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/simple.args");
doJsDceTest(fileName);
}
@TestMetadata("version.args")
public void testVersion() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/version.args");
doJsDceTest(fileName);
}
}
}

View File

@@ -138,6 +138,7 @@ import org.jetbrains.kotlin.j2k.AbstractJavaToKotlinConverterSingleFileTest
import org.jetbrains.kotlin.jps.build.*
import org.jetbrains.kotlin.jps.build.android.AbstractAndroidJpsTestCase
import org.jetbrains.kotlin.jps.incremental.AbstractProtoComparisonTest
import org.jetbrains.kotlin.js.test.AbstractDceTest
import org.jetbrains.kotlin.js.test.semantics.*
import org.jetbrains.kotlin.jvm.compiler.*
import org.jetbrains.kotlin.jvm.runtime.AbstractJvm8RuntimeDescriptorLoaderTest
@@ -359,6 +360,7 @@ fun main(args: Array<String>) {
testClass<AbstractCliTest> {
model("cli/jvm", extension = "args", testMethod = "doJvmTest", recursive = false)
model("cli/js", extension = "args", testMethod = "doJsTest", recursive = false)
model("cli/js-dce", extension = "args", testMethod = "doJsDceTest", recursive = false)
}
testClass<AbstractReplInterpreterTest> {
@@ -1288,6 +1290,10 @@ fun main(args: Array<String>) {
testClass<AbstractOutputPrefixPostfixTest> {
model("outputPrefixPostfix/", pattern = "^([^_](.+))\\.kt$", targetBackend = TargetBackend.JS)
}
testClass<AbstractDceTest> {
model("dce/", pattern = "(.+)\\.js", targetBackend = TargetBackend.JS)
}
}
testGroup("js/js.tests/test", "compiler/testData") {

View File

@@ -207,7 +207,7 @@ class JpsKotlinCompilerRunner : KotlinCompilerRunner<JpsCompilerEnvironment>() {
private fun setupK2JsArguments(_outputFile: File, sourceFiles: Collection<File>, _libraries: List<String>, settings: K2JSCompilerArguments) {
with(settings) {
noStdlib = true
freeArgs = sourceFiles.map { it.path }
freeArgs = sourceFiles.map { it.path }.toMutableList()
outputFile = _outputFile.path
metaInfo = true
libraries = _libraries.joinToString(File.pathSeparator)

16
js/js.dce/js.dce.iml Normal file
View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="PROVIDED" name="intellij-core" level="project" />
<orderEntry type="module" module-name="js.ast" />
<orderEntry type="module" module-name="js.inliner" />
<orderEntry type="module" module-name="js.translator" />
<orderEntry type="module" module-name="util" />
</component>
</module>

View File

@@ -0,0 +1,34 @@
/*
* 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.js.dce
import org.jetbrains.kotlin.js.backend.ast.JsFunction
import org.jetbrains.kotlin.js.backend.ast.JsInvocation
import org.jetbrains.kotlin.js.backend.ast.JsNode
import org.jetbrains.kotlin.js.dce.Context.Node
interface AnalysisResult {
val nodeMap: Map<JsNode, Node>
val astNodesToEliminate: Set<JsNode>
val astNodesToSkip: Set<JsNode>
val functionsToEnter: Set<JsFunction>
val invocationsToSkip: Set<JsInvocation>
}

View File

@@ -0,0 +1,388 @@
/*
* 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.js.dce
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.dce.Context.Node
import org.jetbrains.kotlin.js.inline.util.collectLocalVariables
import org.jetbrains.kotlin.js.translate.context.Namer
class Analyzer(private val context: Context) : JsVisitor() {
private val processedFunctions = mutableSetOf<JsFunction>()
private val postponedFunctions = mutableMapOf<JsName, JsFunction>()
val moduleMapping = mutableMapOf<JsStatement, String>()
private val nodeMap = mutableMapOf<JsNode, Node>()
private val astNodesToEliminate = mutableSetOf<JsNode>()
private val astNodesToSkip = mutableSetOf<JsNode>()
private val functionsToEnter = mutableSetOf<JsFunction>()
private val invocationsToSkip = mutableSetOf<JsInvocation>()
val analysisResult = object : AnalysisResult {
override val nodeMap: Map<JsNode, Node> get() = this@Analyzer.nodeMap
override val astNodesToEliminate: Set<JsNode> get() = this@Analyzer.astNodesToEliminate
override val astNodesToSkip: Set<JsNode> get() = this@Analyzer.astNodesToSkip
override val functionsToEnter: Set<JsFunction> get() = this@Analyzer.functionsToEnter
override val invocationsToSkip: Set<JsInvocation> get() = this@Analyzer.invocationsToSkip
}
override fun visitVars(x: JsVars) {
x.vars.forEach { accept(it) }
}
override fun visit(x: JsVars.JsVar) {
val rhs = x.initExpression
if (rhs != null) {
processAssignment(x, x.name.makeRef(), rhs)?.let { nodeMap[x] = it }
}
}
override fun visitExpressionStatement(x: JsExpressionStatement) {
val expression = x.expression
if (expression is JsBinaryOperation) {
if (expression.operator == JsBinaryOperator.ASG) {
processAssignment(x, expression.arg1, expression.arg2)?.let {
// Mark this statement with FQN extracted from assignment.
// Later, we eliminate such statements if corresponding FQN is reachable
nodeMap[x] = it
}
}
}
else if (expression is JsFunction) {
expression.name?.let { context.nodes[it]?.original }?.let {
nodeMap[x] = it
it.functions += expression
}
}
else if (expression is JsInvocation) {
val function = expression.qualifier
// (function(params) { ... })(arguments), assume that params = arguments and walk its body
if (function is JsFunction) {
enterFunction(function, expression.arguments)
return
}
// f(arguments), where f is a parameter of outer function and it always receives function() { } as an argument.
if (function is JsNameRef && function.qualifier == null) {
val postponedFunction = function.name?.let { postponedFunctions[it] }
if (postponedFunction != null) {
enterFunction(postponedFunction, expression.arguments)
invocationsToSkip += expression
return
}
}
// Object.defineProperty()
if (context.isObjectDefineProperty(function)) {
handleObjectDefineProperty(x, expression.arguments.getOrNull(0), expression.arguments.getOrNull(1),
expression.arguments.getOrNull(2))
}
// Kotlin.defineModule()
else if (context.isDefineModule(function)) {
// (just remove it)
astNodesToEliminate += x
}
else if (context.isAmdDefine(function)) {
handleAmdDefine(expression, expression.arguments)
}
}
}
private fun handleObjectDefineProperty(statement: JsStatement, target: JsExpression?, propertyName: JsExpression?,
propertyDescriptor: JsExpression?) {
if (target == null || propertyName !is JsStringLiteral || propertyDescriptor == null) return
val targetNode = context.extractNode(target) ?: return
val memberNode = targetNode.member(propertyName.value)
nodeMap[statement] = memberNode
memberNode.hasSideEffects = true
// Object.defineProperty(instance, name, { get: value, ... })
if (propertyDescriptor is JsObjectLiteral) {
for (initializer in propertyDescriptor.propertyInitializers) {
// process as if it was instance.name = value
processAssignment(statement, JsNameRef(propertyName.value, target), initializer.valueExpr)
}
}
// Object.defineProperty(instance, name, Object.getOwnPropertyDescriptor(otherInstance))
else if (propertyDescriptor is JsInvocation) {
val function = propertyDescriptor.qualifier
if (context.isObjectGetOwnPropertyDescriptor(function)) {
val source = propertyDescriptor.arguments.getOrNull(0)
val sourcePropertyName = propertyDescriptor.arguments.getOrNull(1)
if (source != null && sourcePropertyName is JsStringLiteral) {
// process as if it was instance.name = otherInstance.name
processAssignment(statement, JsNameRef(propertyName.value, target), JsNameRef(sourcePropertyName.value, source))
}
}
}
}
private fun handleAmdDefine(invocation: JsInvocation, arguments: List<JsExpression>) {
// Handle both named and anonymous modules
val argumentsWithoutName = when (arguments.size) {
2 -> arguments
3 -> arguments.drop(1)
else -> return
}
val dependencies = argumentsWithoutName[0] as? JsArrayLiteral ?: return
// Function can be either a function() { ... } or a reference to parameter out outer function which is known to take
// function literal
val functionRef = argumentsWithoutName[1]
val function = when (functionRef) {
is JsFunction -> functionRef
is JsNameRef -> {
if (functionRef.qualifier != null) return
postponedFunctions[functionRef.name] ?: return
}
else -> return
}
val dependencyNodes = dependencies.expressions
.map { it as? JsStringLiteral ?: return }
.map { if (it.value == "exports") context.currentModule else context.globalScope.member(it.value) }
enterFunctionWithGivenNodes(function, dependencyNodes)
astNodesToSkip += invocation.qualifier
}
override fun visitBlock(x: JsBlock) {
val newModule = moduleMapping[x]
if (newModule != null) {
context.currentModule = context.globalScope.member(newModule)
}
x.statements.forEach { accept(it) }
}
override fun visitIf(x: JsIf) {
accept(x.thenStatement)
x.elseStatement?.accept(this)
}
override fun visitReturn(x: JsReturn) {
val expr = x.expression
if (expr != null) {
context.extractNode(expr)?.let {
nodeMap[x] = it
}
}
}
private fun processAssignment(node: JsNode?, lhs: JsExpression, rhs: JsExpression): Node? {
val leftNode = context.extractNode(lhs)
val rightNode = context.extractNode(rhs)
if (leftNode != null && rightNode != null) {
// If both left and right expressions are fully-qualified names, alias them
leftNode.alias(rightNode)
return leftNode
}
else if (leftNode != null) {
// lhs = foo()
if (rhs is JsInvocation) {
val function = rhs.qualifier
// lhs = function(params) { ... }(arguments)
// see corresponding case in visitExpressionStatement
if (function is JsFunction) {
enterFunction(function, rhs.arguments)
astNodesToSkip += lhs
return null
}
// lhs = foo(arguments), where foo is a parameter of outer function that always take function literal
// see corresponding case in visitExpressionStatement
if (function is JsNameRef && function.qualifier == null) {
function.name?.let { postponedFunctions[it] }?.let {
enterFunction(it, rhs.arguments)
astNodesToSkip += lhs
return null
}
}
// lhs = Object.create(constructor)
if (context.isObjectFunction(function, "create")) {
// Do not alias lhs and constructor, make unidirectional dependency lhs -> constructor instead.
// Motivation: reachability of a base class does not imply reachability of its derived class
handleObjectCreate(leftNode, rhs.arguments.getOrNull(0))
return leftNode
}
// lhs = Kotlin.defineInlineFunction('fqn', function() { ... })
if (context.isDefineInlineFunction(function) && rhs.arguments.size == 2) {
leftNode.functions += rhs.arguments[1] as JsFunction
val defineInlineFunctionNode = context.extractNode(function)
if (defineInlineFunctionNode != null) {
leftNode.dependencies += defineInlineFunctionNode
}
return leftNode
}
}
else if (rhs is JsBinaryOperation) {
// Detect lhs = parent.child || (parent.child = {}), which is used to declare packages.
// Assume lhs = parent.child
if (rhs.operator == JsBinaryOperator.OR) {
val secondNode = context.extractNode(rhs.arg1)
val reassignment = rhs.arg2
if (reassignment is JsBinaryOperation && reassignment.operator == JsBinaryOperator.ASG) {
val reassignNode = context.extractNode(reassignment.arg1)
val reassignValue = reassignment.arg2
if (reassignNode == secondNode && reassignNode != null && reassignValue is JsObjectLiteral &&
reassignValue.propertyInitializers.isEmpty()
) {
return processAssignment(node, lhs, rhs.arg1)
}
}
}
}
else if (rhs is JsFunction) {
// lhs = function() { ... }
// During reachability tracking phase: eliminate it if lhs is unreachable, traverse function otherwise
leftNode.functions += rhs
return leftNode
}
else if (leftNode.qualifier?.memberName == Namer.METADATA) {
// lhs.$metadata$ = expression
// During reachability tracking phase: eliminate it if lhs is unreachable, traverse expression
// It's commonly used to supply class's metadata
leftNode.expressions += rhs
return leftNode
}
else if (rhs is JsObjectLiteral && rhs.propertyInitializers.isEmpty()) {
return leftNode
}
val nodeInitializedByEmptyObject = extractVariableInitializedByEmptyObject(rhs)
if (nodeInitializedByEmptyObject != null) {
astNodesToSkip += rhs
leftNode.alias(nodeInitializedByEmptyObject)
return leftNode
}
}
return null
}
private fun handleObjectCreate(target: Node, arg: JsExpression?) {
if (arg == null) return
val prototypeNode = context.extractNode(arg) ?: return
target.dependencies += prototypeNode.original
target.expressions += arg
}
// Handle typeof foo === 'undefined' ? {} : foo, where foo is FQN
// Assume foo
// This is used by UMD wrapper
private fun extractVariableInitializedByEmptyObject(expression: JsExpression): Node? {
if (expression !is JsConditional) return null
val testExpr = expression.testExpression as? JsBinaryOperation ?: return null
if (testExpr.operator != JsBinaryOperator.REF_EQ) return null
val testExprLhs = testExpr.arg1 as? JsPrefixOperation ?: return null
if (testExprLhs.operator != JsUnaryOperator.TYPEOF) return null
val testExprNode = context.extractNode(testExprLhs.arg) ?: return null
val testExprRhs = testExpr.arg2 as? JsStringLiteral ?: return null
if (testExprRhs.value != "undefined") return null
val thenExpr = expression.thenExpression as? JsObjectLiteral ?: return null
if (thenExpr.propertyInitializers.isNotEmpty()) return null
val elseNode = context.extractNode(expression.elseExpression) ?: return null
if (testExprNode.original != elseNode.original) return null
return testExprNode.original
}
// foo(), where foo is either function literal or parameter of outer function that takes function literal.
// The latter case is required to handle UMD wrapper
// Skip arguments during reachability tracker phase
// Traverse function's body
private fun enterFunction(function: JsFunction, arguments: List<JsExpression>) {
functionsToEnter += function
context.addLocalVars(function.collectLocalVariables())
for ((param, arg) in function.parameters.zip(arguments)) {
if (arg is JsFunction && arg.name == null && isProperFunctionalParameter(arg.body, param)) {
postponedFunctions[param.name] = arg
}
else {
if (processAssignment(function, param.name.makeRef(), arg) != null) {
astNodesToSkip += arg
}
}
}
processFunction(function)
}
private fun enterFunctionWithGivenNodes(function: JsFunction, arguments: List<Node>) {
functionsToEnter += function
context.addLocalVars(function.collectLocalVariables())
for ((param, arg) in function.parameters.zip(arguments)) {
val paramNode = context.nodes[param.name]!!
paramNode.alias(arg)
}
processFunction(function)
}
private fun processFunction(function: JsFunction) {
if (processedFunctions.add(function)) {
accept(function.body)
}
}
// Consider the case: (function(f) { A })(function() { B }) (commonly used in UMD wrapper)
// f = function() { B }.
// Assume A with all occurrences of f() replaced by B.
// However, we need first to ensure that f always occurs as an invocation qualifier, which is checked with this function
private fun isProperFunctionalParameter(body: JsStatement, parameter: JsParameter): Boolean {
var result = true
body.accept(object : RecursiveJsVisitor() {
override fun visitInvocation(invocation: JsInvocation) {
val qualifier = invocation.qualifier
if (qualifier is JsNameRef && qualifier.qualifier == null && qualifier.name == parameter.name) {
if (invocation.arguments.all { context.extractNode(it) != null }) {
return
}
}
if (context.isAmdDefine(qualifier)) return
super.visitInvocation(invocation)
}
override fun visitNameRef(nameRef: JsNameRef) {
if (nameRef.name == parameter.name) {
result = false
}
super.visitNameRef(nameRef)
}
})
return result
}
}

View File

@@ -0,0 +1,224 @@
/*
* 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.js.dce
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.translate.utils.jsAstUtils.array
import org.jetbrains.kotlin.js.translate.utils.jsAstUtils.index
class Context {
val globalScope = Node()
val moduleExportsNode = globalScope.member("module").member("exports")
var currentModule = globalScope
val nodes = mutableMapOf<JsName, Node>()
var thisNode: Node? = globalScope
val localVars = mutableSetOf<JsName>()
fun addLocalVars(names: Collection<JsName>) {
nodes += names.filter { it !in nodes }.associate { it to Node(it) }
}
fun extractNode(expression: JsExpression): Node? {
val node = extractNodeImpl(expression)?.original
return if (node != null && moduleExportsNode in generateSequence(node) { it.qualifier?.parent }) {
val path = node.pathFromRoot().drop(2)
path.fold(currentModule.original) { n, memberName -> n.member(memberName) }
}
else {
node
}
}
private fun extractNodeImpl(expression: JsExpression): Node? {
return when (expression) {
is JsNameRef -> {
val qualifier = expression.qualifier
if (qualifier == null) {
val name = expression.name
if (name != null) {
if (name in localVars) return null
nodes[name]?.original?.let { return it }
}
globalScope.member(expression.ident)
}
else {
extractNodeImpl(qualifier)?.member(expression.ident)
}
}
is JsArrayAccess -> {
val index = expression.index
if (index is JsStringLiteral) extractNodeImpl(expression.array)?.member(index.value) else null
}
is JsLiteral.JsThisRef -> {
thisNode
}
is JsInvocation -> {
val qualifier = expression.qualifier
if (qualifier is JsNameRef && qualifier.qualifier == null && qualifier.ident == "require" &&
qualifier.name !in nodes && expression.arguments.size == 1
) {
val argument = expression.arguments[0]
if (argument is JsStringLiteral) {
return globalScope.member(argument.value)
}
}
null
}
else -> {
null
}
}
}
class Node private constructor(val localName: JsName?, qualifier: Qualifier?) {
private val dependenciesImpl = mutableSetOf<Node>()
private val expressionsImpl = mutableSetOf<JsExpression>()
private val functionsImpl = mutableSetOf<JsFunction>()
private val usedByAstNodesImpl = mutableSetOf<JsNode>()
val dependencies: MutableSet<Node> get() = original.dependenciesImpl
val expressions: MutableSet<JsExpression> get() = original.expressionsImpl
val functions: MutableSet<JsFunction> get() = original.functionsImpl
val usedByAstNodes: MutableSet<JsNode> get() = original.usedByAstNodesImpl
var original: Node = this
get() {
if (field != this) {
field = field.original
}
return field
}
private set
private var hasSideEffectsImpl = false
private var reachableImpl = false
private var declarationReachableImpl = false
private val membersImpl = mutableMapOf<String, Node>()
private var rank = 0
var hasSideEffects: Boolean
get() = original.hasSideEffectsImpl
set(value) {
original.hasSideEffectsImpl = value
}
var reachable: Boolean
get() = original.reachableImpl
set(value) {
original.reachableImpl = value
}
var declarationReachable: Boolean
get() = original.declarationReachableImpl
set(value) {
original.declarationReachableImpl = value
}
var qualifier: Qualifier? = qualifier
get
private set
val memberNames: MutableSet<String> get() = original.membersImpl.keys
constructor(localName: JsName? = null) : this(localName, null)
val members: Map<String, Node> get() = original.membersImpl
fun member(name: String): Node = original.membersImpl.getOrPut(name) { Node(null, Qualifier(this, name)) }.original
fun alias(other: Node) {
val a = original
val b = other.original
if (a == b) return
if (a.qualifier == null && b.qualifier == null) {
a.merge(b)
}
else if (a.qualifier == null) {
if (b.root() == a) a.makeDependencies(b) else b.evacuateFrom(a)
}
else if (b.qualifier == null) {
// b.make..(a) ?
if (a.root() == b) a.makeDependencies(b) else a.evacuateFrom(b)
}
else {
a.makeDependencies(b)
}
}
private fun makeDependencies(other: Node) {
dependenciesImpl += other
other.dependenciesImpl += this
}
private fun evacuateFrom(other: Node) {
val (existingMembers, newMembers) = other.members.toList().partition { (name, _) -> name in membersImpl }
other.original = this
for ((name, member) in newMembers) {
membersImpl[name] = member
member.original.qualifier = Qualifier(this, member.original.qualifier!!.memberName)
}
for ((name, member) in existingMembers) {
membersImpl[name]!!.original.merge(member.original)
membersImpl[name] = member.original
member.original.qualifier = Qualifier(this, member.original.qualifier!!.memberName)
}
other.membersImpl.clear()
hasSideEffectsImpl = hasSideEffectsImpl || other.hasSideEffectsImpl
dependenciesImpl += other.dependenciesImpl
expressionsImpl += other.expressionsImpl
functionsImpl += other.functionsImpl
usedByAstNodesImpl += other.usedByAstNodesImpl
other.dependenciesImpl.clear()
other.expressionsImpl.clear()
other.functionsImpl.clear()
other.usedByAstNodesImpl.clear()
}
private fun merge(other: Node) {
if (this == other) return
if (rank < other.rank) {
other.evacuateFrom(this)
}
else {
evacuateFrom(other)
}
if (rank == other.rank) {
rank++
}
}
fun root(): Node = generateSequence(original) { it.qualifier?.parent?.original }.last()
fun pathFromRoot(): List<String> =
generateSequence(original) { it.qualifier?.parent?.original }.mapNotNull { it.qualifier?.memberName }
.toList().asReversed()
override fun toString(): String = (root().localName?.ident ?: "<unknown>") + pathFromRoot().joinToString("") { ".$it" }
}
class Qualifier(val parent: Node, val memberName: String)
}

View File

@@ -0,0 +1,98 @@
/*
* 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.js.dce
import com.google.gwt.dev.js.rhino.CodePosition
import com.google.gwt.dev.js.rhino.ErrorReporter
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.js.backend.ast.JsBlock
import org.jetbrains.kotlin.js.backend.ast.JsGlobalBlock
import org.jetbrains.kotlin.js.backend.ast.JsNode
import org.jetbrains.kotlin.js.backend.ast.JsProgram
import org.jetbrains.kotlin.js.dce.Context.Node
import org.jetbrains.kotlin.js.inline.util.collectDefinedNames
import org.jetbrains.kotlin.js.inline.util.fixForwardNameReferences
import org.jetbrains.kotlin.js.parser.parse
import java.io.File
object DeadCodeElimination {
fun dce(root: JsNode, logConsumer: (String) -> Unit, reachableNames: Set<String>, moduleMapping: Map<JsBlock, String>): Set<Node> {
val context = Context()
val topLevelVars = collectDefinedNames(root)
context.addLocalVars(topLevelVars)
for (name in topLevelVars) {
context.nodes[name]!!.alias(context.globalScope.member(name.ident))
}
val analyzer = Analyzer(context)
analyzer.moduleMapping += moduleMapping
root.accept(analyzer)
val usageFinder = ReachabilityTracker(context, analyzer.analysisResult, logConsumer)
root.accept(usageFinder)
for (reachableName in reachableNames) {
val path = reachableName.split(".")
val node = path.fold(context.globalScope) { node, part -> node.member(part) }
usageFinder.reach(node)
}
Eliminator(analyzer.analysisResult).accept(root)
return usageFinder.reachableNodes
}
fun run(
inputFiles: Collection<InputFile>,
rootReachableNames: Set<String>,
logConsumer: (String) -> Unit
): DeadCodeEliminationResult {
val program = JsProgram()
val moduleMapping = mutableMapOf<JsBlock, String>()
val blocks = inputFiles.map { file ->
val code = FileUtil.loadFile(File(file.name))
val block = JsGlobalBlock()
block.statements += parse(code, reporter, program.scope, file.name)
moduleMapping[block] = file.moduleName
block
}
program.globalBlock.statements += blocks
program.globalBlock.fixForwardNameReferences()
val result = dce(program.globalBlock, logConsumer, rootReachableNames, moduleMapping)
for ((file, block) in inputFiles.zip(blocks)) {
FileUtil.writeToFile(File(file.outputName), block.toString())
}
return DeadCodeEliminationResult(result)
}
private val reporter = object : ErrorReporter {
override fun warning(message: String, startPosition: CodePosition, endPosition: CodePosition) {
println("[WARN] at ${startPosition.line}, ${startPosition.offset}: $message")
}
override fun error(message: String, startPosition: CodePosition, endPosition: CodePosition) {
println("[ERRO] at ${startPosition.line}, ${startPosition.offset}: $message")
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.js.dce
// Doesn't seem to be needed
class DeadCodeEliminationResult(val reachableNodes: Set<Context.Node>)

View File

@@ -0,0 +1,51 @@
/*
* 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.js.dce
import org.jetbrains.kotlin.js.backend.ast.*
class Eliminator(private val analysisResult: AnalysisResult) : JsVisitorWithContextImpl() {
override fun visit(x: JsVars.JsVar, ctx: JsContext<*>): Boolean = removeIfNecessary(x, ctx)
override fun visit(x: JsExpressionStatement, ctx: JsContext<*>): Boolean = removeIfNecessary(x, ctx)
override fun visit(x: JsReturn, ctx: JsContext<*>): Boolean = removeIfNecessary(x, ctx)
private fun removeIfNecessary(x: JsNode, ctx: JsContext<*>): Boolean {
if (x in analysisResult.astNodesToEliminate) {
ctx.removeMe()
return false
}
val node = analysisResult.nodeMap[x]?.original
return if (!isUsed(node)) {
ctx.removeMe()
false
}
else {
true
}
}
override fun endVisit(x: JsVars, ctx: JsContext<*>) {
if (x.vars.isEmpty()) {
ctx.removeMe()
}
}
// inline?
private fun isUsed(node: Context.Node?): Boolean = node == null || node.declarationReachable
}

View File

@@ -0,0 +1,20 @@
/*
* 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.js.dce
// No need for nullability
class InputFile(val name: String, val outputName: String, val moduleName: String)

View File

@@ -0,0 +1,232 @@
/*
* 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.js.dce
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.dce.Context.Node
import org.jetbrains.kotlin.js.inline.util.collectLocalVariables
class ReachabilityTracker(
private val context: Context,
private val analysisResult: AnalysisResult,
private val logConsumer: (String) -> Unit
) : RecursiveJsVisitor() {
companion object {
private val CALL_FUNCTIONS = setOf("call", "apply")
}
private var currentNodeWithLocation: JsNode? = null
private var depth = 0
private val reachableNodesImpl = mutableSetOf<Node>()
val reachableNodes: Set<Node> get() = reachableNodesImpl
override fun visit(x: JsVars.JsVar) {
if (shouldTraverse(x)) {
super.visit(x)
}
}
override fun visitExpressionStatement(x: JsExpressionStatement) {
if (shouldTraverse(x)) {
super.visitExpressionStatement(x)
}
}
override fun visitReturn(x: JsReturn) {
if (shouldTraverse(x)) {
super.visitReturn(x)
}
}
private fun shouldTraverse(x: JsNode): Boolean =
analysisResult.nodeMap[x] == null && x !in analysisResult.astNodesToEliminate
override fun visitNameRef(nameRef: JsNameRef) {
if (nameRef in analysisResult.astNodesToSkip) return
val node = context.extractNode(nameRef)
if (node != null) {
if (!node.reachable) {
reportAndNest("reach: referenced name $node", currentNodeWithLocation) {
reach(node)
currentNodeWithLocation?.let { node.usedByAstNodes += it }
}
}
}
else {
super.visitNameRef(nameRef)
}
}
override fun visitInvocation(invocation: JsInvocation) {
val function = invocation.qualifier
when {
function is JsFunction && function in analysisResult.functionsToEnter -> {
accept(function.body)
for (argument in invocation.arguments.filter { it is JsFunction && it in analysisResult.functionsToEnter }) {
accept(argument)
}
}
invocation in analysisResult.invocationsToSkip -> {}
else -> {
val node = context.extractNode(invocation.qualifier)
if (node != null && node.qualifier?.memberName in CALL_FUNCTIONS) {
val parent = node.qualifier!!.parent
reach(parent)
currentNodeWithLocation?.let { parent.usedByAstNodes += it }
}
super.visitInvocation(invocation)
}
}
}
override fun visitFunction(x: JsFunction) {
if (x !in analysisResult.functionsToEnter) {
x.collectLocalVariables().let {
context.addLocalVars(it)
context.localVars += it
}
withErasedThis {
super.visitFunction(x)
}
}
else {
super.visitFunction(x)
}
}
private fun withErasedThis(action: () -> Unit) {
val oldThis = context.thisNode
context.thisNode = null
action()
context.thisNode = oldThis
}
override fun visitBreak(x: JsBreak) { }
override fun visitContinue(x: JsContinue) { }
fun reach(node: Node) {
if (node.reachable) return
node.reachable = true
reachableNodesImpl += node
reachDeclaration(node)
reachDependencies(node)
node.members.toList().forEach { (name, member) ->
if (!member.reachable) {
reportAndNest("reach: member $name", null) { reach(member) }
}
}
for (expr in node.functions) {
reportAndNest("traverse: function", expr) {
expr.collectLocalVariables().let {
context.addLocalVars(it)
context.localVars += it
}
withErasedThis { expr.body.accept(this) }
}
}
for (expr in node.expressions) {
reportAndNest("traverse: value", expr) {
expr.accept(this)
}
}
}
private fun reachDependencies(node: Node) {
val path = mutableListOf<String>()
var current = node
while (true) {
for (ancestorDependency in current.dependencies) {
if (current in generateSequence(ancestorDependency) { it.qualifier?.parent }) continue
val dependency = path.asReversed().fold(ancestorDependency) { n, memberName -> n.member(memberName) }
if (!dependency.reachable) {
reportAndNest("reach: dependency $dependency", null) { reach(dependency) }
}
}
val qualifier = current.qualifier ?: break
path += qualifier.memberName
current = qualifier.parent
}
}
private fun reachDeclaration(node: Node) {
if (node.hasSideEffects && !node.reachable) {
reportAndNest("reach: because of side effect", null) {
reach(node)
}
}
else if (!node.declarationReachable) {
node.declarationReachable = true
node.original.qualifier?.parent?.let {
reportAndNest("reach-decl: parent $it", null) {
reachDeclaration(it)
}
}
for (expr in node.expressions) {
reportAndNest("traverse: value", expr) {
expr.accept(this)
}
}
}
}
override fun visitPrefixOperation(x: JsPrefixOperation) {
if (x.operator == JsUnaryOperator.TYPEOF) {
val arg = x.arg
if (arg is JsNameRef && arg.qualifier == null) {
context.extractNode(arg)?.let { reachDeclaration(it) }
return
}
}
super.visitPrefixOperation(x)
}
override fun visitElement(node: JsNode) {
if (node in analysisResult.astNodesToSkip) return
val newLocation = node.extractLocation()
val old = currentNodeWithLocation
if (newLocation != null) {
currentNodeWithLocation = node
}
super.visitElement(node)
currentNodeWithLocation = old
}
private fun report(message: String) {
logConsumer(" ".repeat(depth) + message)
}
private fun reportAndNest(message: String, dueTo: JsNode?, action: () -> Unit) {
val location = dueTo?.extractLocation()
val fullMessage = if (location != null) "$message (due to ${location.asString()})" else message
report(fullMessage)
nested(action)
}
private fun nested(action: () -> Unit) {
depth++
action()
depth--
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.js.dce
import org.jetbrains.kotlin.js.dce.Context.Node
fun printTree(root: Node, consumer: (String) -> Unit, printNestedMembers: Boolean = false, showLocations: Boolean = false) {
printTree(root, consumer, 0, Settings(printNestedMembers, showLocations))
}
private fun printTree(node: Node, consumer: (String) -> Unit, depth: Int, settings: Settings) {
val sb = StringBuilder()
sb.append(" ".repeat(depth)).append(node.qualifier?.memberName ?: node.toString())
if (node.reachable) {
sb.append(" (reachable")
if (settings.showLocations) {
val locations = node.usedByAstNodes.mapNotNull { it.extractLocation() }
if (locations.isNotEmpty()) {
sb.append(" from ").append(locations.joinToString { it.asString() })
}
}
sb.append(")")
}
consumer(sb.toString())
for (memberName in node.memberNames.sorted()) {
val member = node.member(memberName)
if (!member.declarationReachable) continue
if ((!node.reachable || !member.reachable) || settings.printNestedMembers) {
printTree(member, consumer, depth + 1, settings)
}
}
}
private class Settings(val printNestedMembers: Boolean, val showLocations: Boolean)

View File

@@ -0,0 +1,83 @@
/*
* 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.js.dce
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.dce.Context.Node
fun Context.isObjectDefineProperty(function: JsExpression) = isObjectFunction(function, "defineProperty")
fun Context.isObjectGetOwnPropertyDescriptor(function: JsExpression) = isObjectFunction(function, "getOwnPropertyDescriptor")
fun Context.isDefineModule(function: JsExpression): Boolean = isKotlinFunction(function, "defineModule")
fun Context.isDefineInlineFunction(function: JsExpression): Boolean = isKotlinFunction(function, "defineInlineFunction")
fun Context.isObjectFunction(function: JsExpression, functionName: String): Boolean {
if (function !is JsNameRef) return false
if (function.ident != functionName) return false
val receiver = function.qualifier as? JsNameRef ?: return false
if (receiver.name?.let { nodes[it] } != null) return false
return receiver.ident == "Object"
}
fun Context.isKotlinFunction(function: JsExpression, name: String): Boolean {
if (function !is JsNameRef || function.ident != name) return false
val receiver = (function.qualifier as? JsNameRef)?.name ?: return false
return receiver in nodes && receiver.ident.toLowerCase() == "kotlin"
}
fun Context.isAmdDefine(function: JsExpression): Boolean = isTopLevelFunction(function, "define")
fun Context.isTopLevelFunction(function: JsExpression, name: String): Boolean {
if (function !is JsNameRef || function.qualifier != null) return false
return function.ident == name && function.name !in nodes.keys
}
// How can it ever succeed??
fun JsNode.extractLocation(): JsLocation? {
return when (this) {
is SourceInfoAwareJsNode -> source as? JsLocation
is JsExpressionStatement -> expression.source as? JsLocation
else -> null
}
}
fun JsLocation.asString(): String {
val simpleFileName = file.substring(file.lastIndexOf("/") + 1)
return "$simpleFileName:${startLine + 1}"
}
fun Set<Node>.extractRoots(): Set<Node> {
val result = mutableSetOf<Node>()
val visited = mutableSetOf<Node>()
forEach { it.original.extractRootsImpl(result, visited) }
return result
}
private fun Node.extractRootsImpl(target: MutableSet<Node>, visited: MutableSet<Node>) {
if (!visited.add(original)) return
val qualifier = original.qualifier
if (qualifier == null) {
target += original
}
else {
qualifier.parent.extractRootsImpl(target, visited)
}
}

View File

@@ -90,7 +90,7 @@ class JsCallChecker(
try {
val parserScope = JsFunctionScope(JsRootScope(JsProgram()), "<js fun>")
val statements = parse(code, errorReporter, parserScope)
val statements = parse(code, errorReporter, parserScope, reportOn.containingFile.name)
if (statements.isEmpty()) {
context.trace.report(ErrorsJs.JSCODE_NO_JAVASCRIPT_PRODUCED.on(argument))

View File

@@ -0,0 +1,75 @@
/*
* 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.js.inline.util
import org.jetbrains.kotlin.js.backend.ast.*
fun JsNode.fixForwardNameReferences() {
accept(object : RecursiveJsVisitor() {
val currentScope = collectDefinedNames(this@fixForwardNameReferences).associateBy { it.ident }.toMutableMap()
override fun visitFunction(x: JsFunction) {
val scopeBackup = mutableMapOf<String, JsName?>()
val localVars = x.collectLocalVariables()
for (localVar in localVars) {
scopeBackup[localVar.ident] = currentScope[localVar.ident]
currentScope[localVar.ident] = localVar
}
super.visitFunction(x)
for ((ident, oldName) in scopeBackup) {
if (oldName == null) {
currentScope -= ident
}
else {
currentScope[ident] = oldName
}
}
}
override fun visitCatch(x: JsCatch) {
val name = x.parameter.name
val oldName = currentScope[name.ident]
currentScope[name.ident] = name
super.visitCatch(x)
if (oldName != null) {
currentScope[name.ident] = name
}
else {
currentScope -= name.ident
}
}
override fun visitNameRef(nameRef: JsNameRef) {
super.visitNameRef(nameRef)
if (nameRef.qualifier == null) {
val ident = nameRef.ident
val name = currentScope[ident]
if (name != null) {
nameRef.name = name
}
}
}
override fun visitBreak(x: JsBreak) {}
override fun visitContinue(x: JsContinue) {}
})
}

View File

@@ -32,17 +32,28 @@ public class JsAstMapper {
private final JsProgram program;
private final ScopeContext scopeContext;
@Nullable
private String fileName;
public JsAstMapper(@NotNull JsScope scope) {
scopeContext = new ScopeContext(scope);
program = scope.getProgram();
}
public void setFileName(@Nullable String fileName) {
this.fileName = fileName;
}
private static JsParserException createParserException(String msg, Node offender) {
CodePosition position = new CodePosition(offender.getLineno(), 0);
return new JsParserException("Parser encountered internal error: " + msg, position);
}
private JsNode map(Node node) throws JsParserException {
return withLocation(mapWithoutLocation(node), node);
}
private JsNode mapWithoutLocation(Node node) throws JsParserException {
switch (node.getType()) {
case TokenStream.SCRIPT: {
JsBlock block = new JsBlock();
@@ -1115,4 +1126,18 @@ public class JsAstMapper {
int type = jsNode.getType();
return type == TokenStream.NUMBER || type == TokenStream.NUMBER;
}
private <T extends JsNode> T withLocation(T astNode, Node node) {
int lineNumber = node.getLineno();
if (lineNumber >= 0) {
JsLocation location = new JsLocation(fileName, lineNumber, 0);
if (astNode instanceof SourceInfoAwareJsNode) {
astNode.setSource(location);
}
else if (astNode instanceof JsExpressionStatement) {
((JsExpressionStatement) astNode).getExpression().setSource(location);
}
}
return astNode;
}
}

View File

@@ -995,8 +995,9 @@ public class Parser extends Observable {
int tt;
while ((tt = ts.getToken()) > ts.EOF) {
if (tt == ts.DOT) {
ts.treatKeywordAsIdentifier = true;
mustMatchToken(ts, ts.NAME, "msg.no.name.after.dot");
String s = ts.getString();
ts.treatKeywordAsIdentifier = false;
pn = nf.createBinary(ts.DOT, pn, nf.createName(ts.getString()));
/*
* pn = nf.createBinary(ts.DOT, pn, memberExpr(ts)) is the version in

View File

@@ -695,7 +695,7 @@ public class TokenStream {
in.unread();
String str = getStringFromBuffer();
if (!containsEscape) {
if (!containsEscape && !treatKeywordAsIdentifier) {
// OPT we shouldn't have to make a string (object!) to
// check if it's a keyword.
@@ -1477,6 +1477,7 @@ public class TokenStream {
CodePosition lastPosition;
private int op;
public boolean treatKeywordAsIdentifier;
// Set this to an inital non-null value so that the Parser has
// something to retrieve even if an error has occured and no

View File

@@ -29,10 +29,12 @@ import java.util.*
private val FAKE_SOURCE_INFO = SourceInfoImpl(null, 0, 0, 0, 0)
fun parse(code: String, reporter: ErrorReporter, scope: JsScope): List<JsStatement> {
fun parse(code: String, reporter: ErrorReporter, scope: JsScope, fileName: String): List<JsStatement> {
val insideFunction = scope is JsFunctionScope
val node = parse(code, 0, reporter, insideFunction, Parser::parse)
return node.toJsAst(scope, JsAstMapper::mapStatements)
return node.toJsAst(scope) {
JsAstMapper(scope).also { it.setFileName(fileName) }.mapStatements(it)
}
}
fun parseFunction(code: String, offset: Int, reporter: ErrorReporter, scope: JsScope): JsFunction =

View File

@@ -564,9 +564,10 @@ class JsAstSerializer {
var fileChanged = false
if (location != null) {
val lastFile = fileStack.peek()
fileChanged = lastFile != location.file
val newFile = location.file
fileChanged = lastFile != newFile && newFile != null
if (fileChanged) {
fileConsumer(serialize(location.file))
fileConsumer(serialize(newFile!!))
fileStack.push(location.file)
}
val locationBuilder = Location.newBuilder()

View File

@@ -16,5 +16,6 @@
<orderEntry type="module" module-name="backend-common" scope="TEST" />
<orderEntry type="module" module-name="util" />
<orderEntry type="module" module-name="js.serializer" />
<orderEntry type="module" module-name="js.dce" />
</component>
</module>

View File

@@ -0,0 +1,59 @@
/*
* 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.js.test
import com.intellij.openapi.util.io.FileUtil
import junit.framework.TestCase
import org.jetbrains.kotlin.js.dce.DeadCodeElimination
import org.jetbrains.kotlin.js.dce.InputFile
import java.io.File
import java.util.regex.Pattern
abstract class AbstractDceTest : TestCase() {
fun doTest(filePath: String) {
val file = File(filePath)
val fileContents = FileUtil.loadFile(file)
val inputFile = InputFile(filePath, File(pathToOutputDir, file.relativeTo(File(pathToTestDir)).path).path, "main")
val dceResult = DeadCodeElimination.run(setOf(inputFile), extractDeclarations(REQUEST_REACHABLE_PATTERN, fileContents)) {}
val reachableNodeStrings = dceResult.reachableNodes.map { it.toString().removePrefix("<unknown>.") }.toSet()
for (assertedDeclaration in extractDeclarations(ASSERT_REACHABLE_PATTERN, fileContents)) {
TestCase.assertTrue("Declaration $assertedDeclaration not reached", assertedDeclaration in reachableNodeStrings)
}
for (assertedDeclaration in extractDeclarations(ASSERT_UNREACHABLE_PATTERN, fileContents)) {
TestCase.assertTrue("Declaration $assertedDeclaration reached", assertedDeclaration !in reachableNodeStrings)
}
}
private fun extractDeclarations(pattern: Pattern, fileContents: String): Set<String> {
val matcher = pattern.matcher(fileContents)
val result = mutableSetOf<String>()
while (matcher.find()) {
result += matcher.group(1)
}
return result
}
companion object {
private val ASSERT_REACHABLE_PATTERN = Regex("^ *// *ASSERT_REACHABLE: (.+) *$", RegexOption.MULTILINE).toPattern()
private val ASSERT_UNREACHABLE_PATTERN = Regex("^ *// *ASSERT_UNREACHABLE: (.+) *$", RegexOption.MULTILINE).toPattern()
private val REQUEST_REACHABLE_PATTERN = Regex("^ *// *REQUEST_REACHABLE: (.+) *$", RegexOption.MULTILINE).toPattern()
private val pathToTestDir = "js/js.translator/testData/dce"
private val pathToOutputDir = "js/js.translator/testData/out/dce"
}
}

View File

@@ -35,6 +35,8 @@ import org.jetbrains.kotlin.js.backend.ast.JsProgram
import org.jetbrains.kotlin.js.config.EcmaVersion
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
import org.jetbrains.kotlin.js.config.JsConfig
import org.jetbrains.kotlin.js.dce.DeadCodeElimination
import org.jetbrains.kotlin.js.dce.InputFile
import org.jetbrains.kotlin.js.facade.K2JSTranslator
import org.jetbrains.kotlin.js.facade.MainCallParameters
import org.jetbrains.kotlin.js.facade.TranslationResult
@@ -72,6 +74,8 @@ abstract class BasicBoxTest(
protected open fun getOutputPrefixFile(testFilePath: String): File? = null
protected open fun getOutputPostfixFile(testFilePath: String): File? = null
protected open val runMinifierByDefault: Boolean = false
fun doTest(filePath: String) {
doTest(filePath, "OK", MainCallParameters.noCall())
}
@@ -101,8 +105,9 @@ abstract class BasicBoxTest(
generateJavaScriptFile(file.parent, module, outputFileName, dependencies, modules.size > 1,
outputPrefixFile, outputPostfixFile, mainCallParameters)
if (!module.name.endsWith(OLD_MODULE_SUFFIX)) outputFileName else null
if (!module.name.endsWith(OLD_MODULE_SUFFIX)) Pair(outputFileName, module) else null
}
val mainModuleName = if (TEST_MODULE in modules) TEST_MODULE else DEFAULT_MODULE
val mainModule = modules[mainModuleName]!!
@@ -139,7 +144,7 @@ abstract class BasicBoxTest(
additionalFiles += additionalJsFile
}
val allJsFiles = additionalFiles + inputJsFiles + generatedJsFiles + globalCommonFiles + localCommonFiles +
val allJsFiles = additionalFiles + inputJsFiles + generatedJsFiles.map { it.first } + globalCommonFiles + localCommonFiles +
additionalCommonFiles
if (generateNodeJsRunner && !SKIP_NODE_JS.matcher(fileContent).find()) {
@@ -151,7 +156,47 @@ abstract class BasicBoxTest(
runGeneratedCode(allJsFiles, mainModuleName, testFactory.testPackage, TEST_FUNCTION, expectedResult, withModuleSystem)
performAdditionalChecks(generatedJsFiles, outputPrefixFile, outputPostfixFile)
performAdditionalChecks(generatedJsFiles.map { it.first }, outputPrefixFile, outputPostfixFile)
val minificationThresholdMatcher = MINIFICATION_THRESHOLD.matcher(fileContent)
val minificationThresholdFound = minificationThresholdMatcher.find()
val skipMinification = System.getProperty("kotlin.js.skipMinificationTest", "false").toBoolean()
if (!skipMinification &&
(runMinifierByDefault || minificationThresholdFound) &&
!SKIP_MINIFICATION.matcher(fileContent).find()
) {
val thresholdChecker: (Int) -> Unit = if (!minificationThresholdFound) {
({ reachableNodesCount ->
if (!System.getProperty("kotlin.js.generateThreshold", "false").toBoolean()) {
fail("Minification threshold was not set. Reachable nodes: $reachableNodesCount")
}
else {
val suggestedThreshold = reachableNodesCount * 11 / 10
val prefix = "// MINIFICATION_THRESHOLD: $suggestedThreshold\n"
FileUtil.writeToFile(file, prefix + fileContent)
}
})
}
else {
val threshold = minificationThresholdMatcher.group(1).toInt()
({ reachableNodesCount ->
if (reachableNodesCount > threshold) {
fail("DCE marked $reachableNodesCount as reachable, while threshold was $threshold")
}
})
}
minifyAndRun(
workDir = File(File(outputDir, "min"), file.nameWithoutExtension),
allJsFiles = allJsFiles,
generatedJsFiles = generatedJsFiles,
expectedResult = expectedResult,
testModuleName = mainModuleName,
testPackage = testFactory.testPackage,
testFunction = TEST_FUNCTION,
withModuleSystem = withModuleSystem,
minificationThresholdChecker = thresholdChecker)
}
}
}
@@ -394,6 +439,50 @@ abstract class BasicBoxTest(
return JsConfig(project, configuration)
}
private fun minifyAndRun(
workDir: File, allJsFiles: List<String>, generatedJsFiles: List<Pair<String, TestModule>>,
expectedResult: String, testModuleName: String, testPackage: String?, testFunction: String, withModuleSystem: Boolean,
minificationThresholdChecker: (Int) -> Unit
) {
val kotlinJsLib = DIST_DIR_JS_PATH + "kotlin.js"
val kotlinTestJsLib = DIST_DIR_JS_PATH + "kotlin-test.js"
val kotlinJsLibOutput = File(workDir, "kotlin.min.js").path
val kotlinTestJsLibOutput = File(workDir, "kotlin-test.min.js").path
val kotlinJsInputFile = InputFile(kotlinJsLib, kotlinJsLibOutput, "kotlin")
val kotlinTestJsInputFile = InputFile(kotlinTestJsLib, kotlinTestJsLibOutput, "kotlin-test")
val filesToMinify = generatedJsFiles.associate { (fileName, module) ->
val inputFileName = File(fileName).nameWithoutExtension
fileName to InputFile(fileName, File(workDir, inputFileName + ".min.js").absolutePath, module.name)
}
val testFunctionFqn = testModuleName + (if (testPackage.isNullOrEmpty()) "" else ".$testPackage") + ".$testFunction"
val additionalReachableNodes = setOf(
testFunctionFqn, "kotlin.kotlin.io.BufferedOutput", "kotlin.kotlin.io.output.flush",
"kotlin.kotlin.io.output.buffer", "kotlin-test.kotlin.test.overrideAsserter_wbnzx$"
)
val allFilesToMinify = filesToMinify.values + kotlinJsInputFile + kotlinTestJsInputFile
val dceResult = DeadCodeElimination.run(allFilesToMinify, additionalReachableNodes) { }
val reachableNodes = dceResult.reachableNodes
minificationThresholdChecker(reachableNodes.size)
val runList = mutableListOf<String>()
runList += kotlinJsLibOutput
runList += kotlinTestJsLibOutput
runList += TEST_DATA_DIR_PATH + "nashorn-polyfills.js"
runList += allJsFiles.map { filesToMinify[it]?.outputName ?: it }
val result = engineForMinifier.runAndRestoreContext {
runList.forEach(this::loadFile)
overrideAsserter()
eval(NashornJsTestChecker.SETUP_KOTLIN_OUTPUT)
runTestFunction(testModuleName, testPackage, testFunction, withModuleSystem)
}
TestCase.assertEquals(expectedResult, result)
}
private inner class TestFileFactoryImpl : TestFileFactory<TestModule, TestFile>, Closeable {
var testPackage: String? = null
val tmpDir = KotlinTestUtils.tmpDir("js-tests")
@@ -477,6 +566,8 @@ abstract class BasicBoxTest(
private val NO_MODULE_SYSTEM_PATTERN = Pattern.compile("^// *NO_JS_MODULE_SYSTEM", Pattern.MULTILINE)
private val NO_INLINE_PATTERN = Pattern.compile("^// *NO_INLINE *$", Pattern.MULTILINE)
private val SKIP_NODE_JS = Pattern.compile("^// *SKIP_NODE_JS *$", Pattern.MULTILINE)
private val SKIP_MINIFICATION = Pattern.compile("^// *SKIP_MINIFICATION *$", Pattern.MULTILINE)
private val MINIFICATION_THRESHOLD = Pattern.compile("^// *MINIFICATION_THRESHOLD: *([0-9]+) *$", Pattern.MULTILINE)
private val RECOMPILE_PATTERN = Pattern.compile("^// *RECOMPILE *$", Pattern.MULTILINE)
private val AST_EXTENSION = "jsast"
private val METADATA_EXTENSION = "jsmeta"
@@ -488,5 +579,7 @@ abstract class BasicBoxTest(
private val OLD_MODULE_SUFFIX = "-old"
const val KOTLIN_TEST_INTERNAL = "\$kotlin_test_internal\$"
private val engineForMinifier = createScriptEngine()
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.js.test;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TargetBackend;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("js/js.translator/testData/dce")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class DceTestGenerated extends AbstractDceTest {
public void testAllFilesPresentInDce() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("js/js.translator/testData/dce"), Pattern.compile("(.+)\\.js"), TargetBackend.JS, true);
}
@TestMetadata("amd.js")
public void testAmd() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/dce/amd.js");
doTest(fileName);
}
@TestMetadata("commonjs.js")
public void testCommonjs() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/dce/commonjs.js");
doTest(fileName);
}
@TestMetadata("cycle.js")
public void testCycle() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/dce/cycle.js");
doTest(fileName);
}
}

View File

@@ -27,14 +27,63 @@ fun createScriptEngine(): ScriptEngine =
// TODO use "-strict"
NashornScriptEngineFactory().getScriptEngine("--language=es5", "--no-java", "--no-syntax-extensions")
private fun ScriptEngine.loadFile(path: String) {
fun ScriptEngine.overrideAsserter() {
eval("this['kotlin-test'].kotlin.test.overrideAsserter_wbnzx$(new this['kotlin-test'].kotlin.test.DefaultAsserter());")
}
fun ScriptEngine.runTestFunction(
testModuleName: String, testPackageName: String?, testFunctionName: String,
withModuleSystem: Boolean
): Any? {
val testModule =
when {
withModuleSystem ->
eval(BasicBoxTest.Companion.KOTLIN_TEST_INTERNAL + ".require('" + testModuleName + "')")
else ->
get(testModuleName)
}
testModule as ScriptObjectMirror
val testPackage =
when {
testPackageName === null ->
testModule
testPackageName.contains(".") ->
testPackageName.split(".").fold(testModule) { p, part -> p[part] as ScriptObjectMirror }
else ->
testModule[testPackageName]!!
}
return (this as Invocable).invokeMethod(testPackage, testFunctionName)
}
fun ScriptEngine.loadFile(path: String) {
eval("load('$path');")
}
fun ScriptEngine.runAndRestoreContext(
f: ScriptEngine.() -> Any?
): Any? {
val globalObject = eval("this") as ScriptObjectMirror
val before = globalObject.toMapWithAllMembers()
return try {
this.f()
}
finally {
val after = globalObject.toMapWithAllMembers()
val diff = after.entries - before.entries
diff.forEach {
globalObject.put(it.key, before[it.key] ?: ScriptRuntime.UNDEFINED)
}
}
}
private fun ScriptObjectMirror.toMapWithAllMembers(): Map<String, Any?> = getOwnKeys(true).associate { it to this[it] }
object NashornJsTestChecker {
private val SETUP_KOTLIN_OUTPUT = "kotlin.kotlin.io.output = new kotlin.kotlin.io.BufferedOutput();"
val SETUP_KOTLIN_OUTPUT = "kotlin.kotlin.io.output = new kotlin.kotlin.io.BufferedOutput();"
private val GET_KOTLIN_OUTPUT = "kotlin.kotlin.io.output.buffer;"
private val engine = createScriptEngineForTest()
@@ -68,47 +117,17 @@ object NashornJsTestChecker {
testFunctionName: String,
withModuleSystem: Boolean
) = run(files) {
val testModule =
when {
withModuleSystem ->
engine.eval(BasicBoxTest.Companion.KOTLIN_TEST_INTERNAL + ".require('" + testModuleName + "')") as ScriptObjectMirror
else ->
engine.get(testModuleName) as ScriptObjectMirror
}
val testPackage =
when {
testPackageName === null ->
testModule
testPackageName.contains(".") ->
testPackageName.split(".").fold(testModule) { p, part -> p[part] as ScriptObjectMirror }
else ->
testModule[testPackageName]!!
}
(engine as Invocable).invokeMethod(testPackage, testFunctionName)
runTestFunction(testModuleName, testPackageName, testFunctionName, withModuleSystem)
}
private fun run(
files: List<String>,
f: ScriptEngine.() -> Any?
): Any? {
val globalObject = engine.eval("this") as ScriptObjectMirror
val before = globalObject.toMapWithAllMembers()
engine.eval(SETUP_KOTLIN_OUTPUT)
try {
return engine.runAndRestoreContext {
files.forEach(engine::loadFile)
return engine.f()
}
finally {
val after = globalObject.toMapWithAllMembers()
val diff = after.entries - before.entries
diff.forEach {
globalObject.put(it.key, before[it.key] ?: ScriptRuntime.UNDEFINED)
}
engine.f()
}
}
@@ -121,7 +140,7 @@ object NashornJsTestChecker {
BasicBoxTest.DIST_DIR_JS_PATH + "kotlin-test.js"
).forEach(engine::loadFile)
engine.eval("this['kotlin-test'].kotlin.test.overrideAsserter_wbnzx$(new this['kotlin-test'].kotlin.test.DefaultAsserter());")
engine.overrideAsserter()
return engine
}

View File

@@ -57,8 +57,8 @@ class NameResolutionTest {
val expectedCode = FileUtil.loadFile(File(expectedName))
val parserScope = JsFunctionScope(JsRootScope(JsProgram()), "<js fun>")
val originalAst = JsGlobalBlock().apply { statements += parse(originalCode, errorReporter, parserScope) }
val expectedAst = JsGlobalBlock().apply { statements += parse(expectedCode, errorReporter, parserScope) }
val originalAst = JsGlobalBlock().apply { statements += parse(originalCode, errorReporter, parserScope, originalName) }
val expectedAst = JsGlobalBlock().apply { statements += parse(expectedCode, errorReporter, parserScope, expectedName) }
originalAst.accept(object : RecursiveJsVisitor() {
val cache = mutableMapOf<JsName, JsName>()

View File

@@ -50,12 +50,12 @@ abstract class BasicOptimizerTest(private var basePath: String) {
runScript(unoptimizedName, unoptimizedCode)
runScript(optimizedName, optimizedCode)
checkOptimizer(unoptimizedCode, optimizedCode)
checkOptimizer(unoptimizedCode, optimizedCode, unoptimizedName, optimizedName)
}
private fun checkOptimizer(unoptimizedCode: String, optimizedCode: String) {
private fun checkOptimizer(unoptimizedCode: String, optimizedCode: String, unoptimizedName: String, optimizedName: String) {
val parserScope = JsFunctionScope(JsRootScope(JsProgram()), "<js fun>")
val unoptimizedAst = parse(unoptimizedCode, errorReporter, parserScope)
val unoptimizedAst = parse(unoptimizedCode, errorReporter, parserScope, unoptimizedName)
updateMetadata(unoptimizedCode, unoptimizedAst)
@@ -63,7 +63,7 @@ abstract class BasicOptimizerTest(private var basePath: String) {
process(statement)
}
val optimizedAst = parse(optimizedCode, errorReporter, parserScope)
val optimizedAst = parse(optimizedCode, errorReporter, parserScope, optimizedName)
Assert.assertEquals(astToString(optimizedAst), astToString(unoptimizedAst))
}

View File

@@ -4646,6 +4646,12 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest {
doTest(fileName);
}
@TestMetadata("keywordAsMemberName.kt")
public void testKeywordAsMemberName() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/inlineMultiModule/keywordAsMemberName.kt");
doTest(fileName);
}
@TestMetadata("kt16144.kt")
public void testKt16144() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/inlineMultiModule/kt16144.kt");

View File

@@ -59,5 +59,4 @@ public class OutputPrefixPostfixTestGenerated extends AbstractOutputPrefixPostfi
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/outputPrefixPostfix/simpleWithPrefixAndPostfix.kt");
doTest(fileName);
}
}

View File

@@ -53,5 +53,4 @@ public class SourceMapGenerationSmokeTestGenerated extends AbstractSourceMapGene
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/sourcemap/methodCallInMethod.kt");
doTest(fileName);
}
}

View File

@@ -40,7 +40,9 @@ abstract class AbstractEnumValuesInlineTests : BorrowedInlineTest("enum/")
abstract class AbstractBoxJsTest : BasicBoxTest(
BasicBoxTest.TEST_DATA_DIR_PATH + "box/",
BasicBoxTest.TEST_DATA_DIR_PATH + "out/box/"
)
) {
override val runMinifierByDefault: Boolean = true
}
abstract class AbstractJsCodegenBoxTest : BasicBoxTest(
"compiler/testData/codegen/box/",

View File

@@ -175,6 +175,6 @@ public final class CallExpressionTranslator extends AbstractCallExpressionTransl
assert currentScope instanceof JsFunctionScope : "Usage of js outside of function is unexpected";
JsScope temporaryRootScope = new JsRootScope(new JsProgram());
JsScope scope = new DelegatingJsFunctionScopeWithTemporaryParent((JsFunctionScope) currentScope, temporaryRootScope);
return ParserUtilsKt.parse(jsCode, ThrowExceptionOnErrorReporter.INSTANCE, scope);
return ParserUtilsKt.parse(jsCode, ThrowExceptionOnErrorReporter.INSTANCE, scope, jsCodeExpression.getContainingKtFile().getName());
}
}

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 535
package foo
annotation class bar

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 548
// This test was adapted from compiler/testData/codegen/box/callableReference/function/.
package foo

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 543
package foo
class A {

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 545
package foo
fun run(a: A, arg: String, funRef:(A, String) -> String): String {

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 542
// This test was adapted from compiler/testData/codegen/box/callableReference/function/.
package foo

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 543
// This test was adapted from compiler/testData/codegen/box/callableReference/function/.
package foo

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 541
// This test was adapted from compiler/testData/codegen/box/callableReference/function/.
package foo

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 541
// This test was adapted from compiler/testData/codegen/box/callableReference/function/.
package foo

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 541
// This test was adapted from compiler/testData/codegen/box/callableReference/function/.
package foo

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 541
// This test was adapted from compiler/testData/codegen/box/callableReference/function/.
package foo

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 548
package foo
open class A {

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 537
// This test was adapted from compiler/testData/codegen/box/callableReference/function/local/.
package foo

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 539
// This test was adapted from compiler/testData/codegen/box/callableReference/function/.
package foo

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 539
// This test was adapted from compiler/testData/codegen/box/callableReference/function/.
package foo

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 541
package foo
class A(val x:Int) {

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 540
// This test was adapted from compiler/testData/codegen/box/callableReference/function/local/.
package foo

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 542
// This test was adapted from compiler/testData/codegen/box/callableReference/function/.
package foo

View File

@@ -1,3 +1,4 @@
// MINIFICATION_THRESHOLD: 541
// This test was adapted from compiler/testData/codegen/box/callableReference/function/.
package foo

Some files were not shown because too many files have changed in this diff Show More