diff --git a/.idea/modules.xml b/.idea/modules.xml
index 3585088a404..d9a277ce3b1 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -68,6 +68,7 @@
+
diff --git a/build.xml b/build.xml
index dbddee457bf..11ed6dce1cc 100644
--- a/build.xml
+++ b/build.xml
@@ -112,6 +112,7 @@
+
@@ -152,6 +153,7 @@
+
@@ -265,6 +267,7 @@
+
diff --git a/compiler/cli/cli.iml b/compiler/cli/cli.iml
index d9040ff4b01..0dd3db7abef 100644
--- a/compiler/cli/cli.iml
+++ b/compiler/cli/cli.iml
@@ -24,5 +24,6 @@
+
\ No newline at end of file
diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/js/dce/K2JSDce.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/js/dce/K2JSDce.kt
new file mode 100644
index 00000000000..fff1eaf5981
--- /dev/null
+++ b/compiler/cli/src/org/jetbrains/kotlin/cli/js/dce/K2JSDce.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+@file:JvmName("K2JSDce")
+package org.jetbrains.kotlin.cli.js.dce
+
+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
+
+fun main(args: Array) {
+ val files = args.map { InputFile(it) }
+ val nodes = DeadCodeElimination.run(files, emptySet()) { println(it) }
+
+ println()
+ for (node in nodes.extractRoots()) {
+ printTree(node, { println(it) }, printNestedMembers = false, showLocations = true)
+ }
+}
\ No newline at end of file
diff --git a/js/js.dce/js.dce.iml b/js/js.dce/js.dce.iml
new file mode 100644
index 00000000000..50c3196e4e0
--- /dev/null
+++ b/js/js.dce/js.dce.iml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/AnalysisResult.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/AnalysisResult.kt
new file mode 100644
index 00000000000..a93bce9c41e
--- /dev/null
+++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/AnalysisResult.kt
@@ -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
+
+ val astNodesToEliminate: Set
+
+ val astNodesToSkip: Set
+
+ val functionsToEnter: Set
+
+ val invocationsToSkip: Set
+}
diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/Analyzer.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/Analyzer.kt
new file mode 100644
index 00000000000..16d7967a578
--- /dev/null
+++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/Analyzer.kt
@@ -0,0 +1,387 @@
+/*
+ * 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()
+ private val postponedFunctions = mutableMapOf()
+ private val nodeMap = mutableMapOf()
+ private val astNodesToEliminate = mutableSetOf()
+ private val astNodesToSkip = mutableSetOf()
+ private val invocationsToSkip = mutableSetOf()
+ val moduleMapping = mutableMapOf()
+ private val functionsToEnter = mutableSetOf()
+
+ val analysisResult = object : AnalysisResult {
+ override val nodeMap: Map get() = this@Analyzer.nodeMap
+
+ override val astNodesToEliminate: Set get() = this@Analyzer.astNodesToEliminate
+
+ override val astNodesToSkip: Set get() = this@Analyzer.astNodesToSkip
+
+ override val functionsToEnter: Set get() = this@Analyzer.functionsToEnter
+
+ override val invocationsToSkip: Set 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) {
+ // 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) {
+ functionsToEnter += function
+ context.addNodesForLocalVars(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) {
+ functionsToEnter += function
+ context.addNodesForLocalVars(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
+ }
+}
\ No newline at end of file
diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/Context.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/Context.kt
new file mode 100644
index 00000000000..8f3c7e06cd3
--- /dev/null
+++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/Context.kt
@@ -0,0 +1,221 @@
+/*
+ * 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()
+ var thisNode: Node? = globalScope
+ val namesOfLocalVars = mutableSetOf()
+
+ fun addNodesForLocalVars(names: Collection) {
+ 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 namesOfLocalVars) 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()
+ private val expressionsImpl = mutableSetOf()
+ private val functionsImpl = mutableSetOf()
+ private var hasSideEffectsImpl = false
+ private var reachableImpl = false
+ private var declarationReachableImpl = false
+ private val membersImpl = mutableMapOf()
+ private val usedByAstNodesImpl = mutableSetOf()
+ private var rank = 0
+
+ val dependencies: MutableSet get() = original.dependenciesImpl
+
+ val expressions: MutableSet get() = original.expressionsImpl
+
+ val functions: MutableSet get() = original.functionsImpl
+
+ 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 usedByAstNodes: MutableSet get() = original.usedByAstNodesImpl
+
+ val memberNames: MutableSet get() = original.membersImpl.keys
+
+ constructor(localName: JsName? = null) : this(localName, null)
+
+ var original: Node = this
+ get() {
+ if (field != this) {
+ field = field.original
+ }
+ return field
+ }
+ private set
+
+ val members: Map 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) {
+ 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
+ expressionsImpl += other.expressionsImpl
+ functionsImpl += other.functionsImpl
+ dependenciesImpl += other.dependenciesImpl
+ usedByAstNodesImpl += other.usedByAstNodesImpl
+
+ other.expressionsImpl.clear()
+ other.functionsImpl.clear()
+ other.dependenciesImpl.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 =
+ generateSequence(original) { it.qualifier?.parent?.original }.mapNotNull { it.qualifier?.memberName }
+ .toList().asReversed()
+
+ override fun toString(): String = (root().localName?.ident ?: "") + pathFromRoot().joinToString("") { ".$it" }
+ }
+
+ class Qualifier(val parent: Node, val memberName: String)
+}
\ No newline at end of file
diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeElimination.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeElimination.kt
new file mode 100644
index 00000000000..5815a3e6914
--- /dev/null
+++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeElimination.kt
@@ -0,0 +1,107 @@
+/*
+ * 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 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
+
+class DeadCodeElimination(val logConsumer: (String) -> Unit) {
+ val moduleMapping = mutableMapOf()
+ private val reachableNames = mutableSetOf()
+
+ var reachableNodes = setOf()
+ get
+ private set
+
+ fun apply(root: JsNode) {
+ val context = Context()
+
+ val topLevelVars = collectDefinedNames(root)
+ context.addNodesForLocalVars(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)
+ }
+ reachableNodes = usageFinder.reachableNodes
+
+ Eliminator(analyzer.analysisResult).accept(root)
+ }
+
+ companion object {
+ fun run(
+ inputFiles: Collection,
+ rootReachableNames: Set,
+ logConsumer: (String) -> Unit
+ ): DeadCodeEliminationResult {
+ val program = JsProgram()
+ val dce = DeadCodeElimination(logConsumer)
+
+ val blocks = inputFiles.map { file ->
+ val block = JsGlobalBlock()
+ val code = File(file.path).readText()
+ block.statements += parse(code, reporter, program.scope, file.path)
+ file.moduleName?.let { dce.moduleMapping[block] = it }
+ block
+ }
+ program.globalBlock.statements += blocks
+ program.globalBlock.fixForwardNameReferences()
+
+ dce.reachableNames += rootReachableNames
+ dce.apply(program.globalBlock)
+
+ for ((file, block) in inputFiles.zip(blocks)) {
+ with(File(file.outputPath)) {
+ parentFile.mkdirs()
+ writeText(block.toString())
+ }
+ }
+
+ return DeadCodeEliminationResult(dce.reachableNodes)
+ }
+
+ 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")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeEliminationResult.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeEliminationResult.kt
new file mode 100644
index 00000000000..9479280be78
--- /dev/null
+++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeEliminationResult.kt
@@ -0,0 +1,19 @@
+/*
+ * 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
+
+class DeadCodeEliminationResult(val reachableNodes: Set)
\ No newline at end of file
diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/Eliminator.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/Eliminator.kt
new file mode 100644
index 00000000000..c4306424c56
--- /dev/null
+++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/Eliminator.kt
@@ -0,0 +1,50 @@
+/*
+ * 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()
+ }
+ }
+
+ private fun isUsed(node: Context.Node?): Boolean = node == null || node.declarationReachable
+}
\ No newline at end of file
diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputFile.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputFile.kt
new file mode 100644
index 00000000000..1e6e7b08857
--- /dev/null
+++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputFile.kt
@@ -0,0 +1,19 @@
+/*
+ * 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
+
+class InputFile(val path: String, val outputPath: String, val moduleName: String? = null)
\ No newline at end of file
diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/ReachabilityTracker.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/ReachabilityTracker.kt
new file mode 100644
index 00000000000..2566e47e9c0
--- /dev/null
+++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/ReachabilityTracker.kt
@@ -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()
+
+ val reachableNodes: Set 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.addNodesForLocalVars(it)
+ context.namesOfLocalVars += 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.addNodesForLocalVars(it)
+ context.namesOfLocalVars += 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()
+ 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--
+ }
+}
\ No newline at end of file
diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/printTree.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/printTree.kt
new file mode 100644
index 00000000000..7da25da5839
--- /dev/null
+++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/printTree.kt
@@ -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)
\ No newline at end of file
diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/util.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/util.kt
new file mode 100644
index 00000000000..a5c1a67fe95
--- /dev/null
+++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/util.kt
@@ -0,0 +1,82 @@
+/*
+ * 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
+}
+
+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.extractRoots(): Set {
+ val result = mutableSetOf()
+ val visited = mutableSetOf()
+ forEach { it.original.extractRootsImpl(result, visited) }
+ return result
+}
+
+private fun Node.extractRootsImpl(target: MutableSet, visited: MutableSet) {
+ if (!visited.add(original)) return
+ val qualifier = original.qualifier
+ if (qualifier == null) {
+ target += original
+ }
+ else {
+ qualifier.parent.extractRootsImpl(target, visited)
+ }
+}
diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCallChecker.kt b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCallChecker.kt
index 487c7f62537..988de5b76d8 100644
--- a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCallChecker.kt
+++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCallChecker.kt
@@ -90,7 +90,7 @@ class JsCallChecker(
try {
val parserScope = JsFunctionScope(JsRootScope(JsProgram()), "")
- 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))
diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt
index de201c476eb..cca07ac8fbf 100644
--- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt
+++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt
@@ -57,7 +57,7 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName
* kotlinVariable: kotlin object variable.
* The default variable is Kotlin, but it can be renamed by minifier.
*/
- data class ModuleInfo(val fileContent: String, val moduleVariable: String, val kotlinVariable: String)
+ data class ModuleInfo(val filePath: String, val fileContent: String, val moduleVariable: String, val kotlinVariable: String)
private val moduleNameToInfo = HashMultimap.create()
@@ -68,7 +68,7 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName
moduleNameMap = buildModuleNameMap(fragments)
- JsLibraryUtils.traverseJsLibraries(libs) { fileContent, _ ->
+ JsLibraryUtils.traverseJsLibraries(libs) { fileContent, filePath ->
var current = 0
while (true) {
@@ -83,7 +83,7 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName
val moduleName = preciseMatcher.group(3)
val moduleVariable = preciseMatcher.group(4)
val kotlinVariable = preciseMatcher.group(1)
- moduleNameToInfo.put(moduleName, ModuleInfo(fileContent, moduleVariable, kotlinVariable))
+ moduleNameToInfo.put(moduleName, ModuleInfo(filePath, fileContent, moduleVariable, kotlinVariable))
}
}
}
@@ -152,7 +152,7 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName
offset++
}
- val function = parseFunction(source, offset, ThrowExceptionOnErrorReporter, JsRootScope(JsProgram()))
+ val function = parseFunction(source, info.filePath, offset, ThrowExceptionOnErrorReporter, JsRootScope(JsProgram()))
val moduleReference = moduleNameMap[tag] ?: currentModuleName.makeRef()
val replacements = hashMapOf(info.moduleVariable to moduleReference,
diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/fixForwardNameReferences.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/fixForwardNameReferences.kt
new file mode 100644
index 00000000000..81f6debdd35
--- /dev/null
+++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/fixForwardNameReferences.kt
@@ -0,0 +1,79 @@
+/*
+ * 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 = mutableMapOf()
+
+ init {
+ currentScope += collectDefinedNames(this@fixForwardNameReferences).associateBy { it.ident }
+ }
+
+ override fun visitFunction(x: JsFunction) {
+ val scopeBackup = mutableMapOf()
+ 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) {}
+ })
+}
\ No newline at end of file
diff --git a/js/js.parser/src/com/google/gwt/dev/js/JsAstMapper.java b/js/js.parser/src/com/google/gwt/dev/js/JsAstMapper.java
index 9e47f4221f7..078ba9c7977 100644
--- a/js/js.parser/src/com/google/gwt/dev/js/JsAstMapper.java
+++ b/js/js.parser/src/com/google/gwt/dev/js/JsAstMapper.java
@@ -32,9 +32,13 @@ public class JsAstMapper {
private final JsProgram program;
private final ScopeContext scopeContext;
- public JsAstMapper(@NotNull JsScope scope) {
+ @NotNull
+ private final String fileName;
+
+ public JsAstMapper(@NotNull JsScope scope, @NotNull String fileName) {
scopeContext = new ScopeContext(scope);
program = scope.getProgram();
+ this.fileName = fileName;
}
private static JsParserException createParserException(String msg, Node offender) {
@@ -43,6 +47,10 @@ public class JsAstMapper {
}
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 +1123,18 @@ public class JsAstMapper {
int type = jsNode.getType();
return type == TokenStream.NUMBER || type == TokenStream.NUMBER;
}
+
+ private 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;
+ }
}
diff --git a/js/js.parser/src/org/jetbrains/kotlin/js/parser/parserUtils.kt b/js/js.parser/src/org/jetbrains/kotlin/js/parser/parserUtils.kt
index aa28ee802ab..9066a42b5b8 100644
--- a/js/js.parser/src/org/jetbrains/kotlin/js/parser/parserUtils.kt
+++ b/js/js.parser/src/org/jetbrains/kotlin/js/parser/parserUtils.kt
@@ -29,17 +29,19 @@ import java.util.*
private val FAKE_SOURCE_INFO = SourceInfoImpl(null, 0, 0, 0, 0)
-fun parse(code: String, reporter: ErrorReporter, scope: JsScope): List {
+fun parse(code: String, reporter: ErrorReporter, scope: JsScope, fileName: String): List {
val insideFunction = scope is JsFunctionScope
val node = parse(code, 0, reporter, insideFunction, Parser::parse)
- return node.toJsAst(scope, JsAstMapper::mapStatements)
+ return node.toJsAst(scope, fileName) {
+ mapStatements(it)
+ }
}
-fun parseFunction(code: String, offset: Int, reporter: ErrorReporter, scope: JsScope): JsFunction =
+fun parseFunction(code: String, fileName: String, offset: Int, reporter: ErrorReporter, scope: JsScope): JsFunction =
parse(code, offset, reporter, insideFunction = false) {
addObserver(FunctionParsingObserver())
primaryExpr(it)
- }.toJsAst(scope, JsAstMapper::mapFunction)
+ }.toJsAst(scope, fileName, JsAstMapper::mapFunction)
private class FunctionParsingObserver : Observer {
var functionsStarted = 0
@@ -80,8 +82,8 @@ private fun parse(
}
inline
-private fun Node.toJsAst(scope: JsScope, mapAction: JsAstMapper.(Node)->T): T =
- JsAstMapper(scope).mapAction(this)
+private fun Node.toJsAst(scope: JsScope, fileName: String, mapAction: JsAstMapper.(Node)->T): T =
+ JsAstMapper(scope, fileName).mapAction(this)
private fun StringReader(string: String, offset: Int): Reader {
val reader = StringReader(string)
diff --git a/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/ast/JsAstSerializer.kt b/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/ast/JsAstSerializer.kt
index 3cb0ff3a63e..c5719d78de7 100644
--- a/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/ast/JsAstSerializer.kt
+++ b/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/ast/JsAstSerializer.kt
@@ -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()
diff --git a/js/js.tests/js.tests.iml b/js/js.tests/js.tests.iml
index cc7aada55d3..30433c5086e 100644
--- a/js/js.tests/js.tests.iml
+++ b/js/js.tests/js.tests.iml
@@ -16,5 +16,6 @@
+
\ No newline at end of file
diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt
index 74d352d03db..3d4294eb8ef 100644
--- a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt
+++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt
@@ -18,6 +18,7 @@ package org.jetbrains.kotlin.js.test
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
+import com.intellij.openapi.vfs.CharsetToolkit
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiManager
@@ -35,6 +36,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
@@ -53,6 +56,7 @@ import org.jetbrains.kotlin.test.KotlinTestUtils.TestFileFactory
import org.jetbrains.kotlin.test.KotlinTestWithEnvironment
import org.jetbrains.kotlin.test.TargetBackend
import org.jetbrains.kotlin.utils.DFS
+import org.mozilla.javascript.Context
import java.io.ByteArrayOutputStream
import java.io.Closeable
import java.io.File
@@ -102,8 +106,9 @@ abstract class BasicBoxTest(
generateJavaScriptFile(file.parent, module, outputFileName, dependencies, friends, 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]!!
@@ -140,7 +145,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()) {
@@ -152,7 +157,10 @@ abstract class BasicBoxTest(
runGeneratedCode(allJsFiles, mainModuleName, testFactory.testPackage, TEST_FUNCTION, expectedResult, withModuleSystem)
- performAdditionalChecks(generatedJsFiles, outputPrefixFile, outputPostfixFile)
+ performAdditionalChecks(generatedJsFiles.map { it.first }, outputPrefixFile, outputPostfixFile)
+
+ minifyAndRun(File(File(outputDir, "min"), file.nameWithoutExtension), allJsFiles, generatedJsFiles, expectedResult,
+ mainModuleName, testFactory.testPackage, TEST_FUNCTION, withModuleSystem)
}
}
@@ -397,6 +405,59 @@ abstract class BasicBoxTest(
return JsConfig(project, configuration)
}
+ private fun minifyAndRun(
+ workDir: File, allJsFiles: List, generatedJsFiles: List>,
+ expectedResult: String, testModuleName: String, testPackage: String?, testFunction: String, withModuleSystem: Boolean
+ ) {
+ 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"
+ )
+ val allFilesToMinify = filesToMinify.values + kotlinJsInputFile + kotlinTestJsInputFile
+ val reachableNodes = DeadCodeElimination.run(allFilesToMinify, additionalReachableNodes) { }
+ if (reachableNodes.size > 1500) println("!!!")
+ println(reachableNodes.size.toString() + ": " + workDir.path)
+
+ val runList = mutableListOf()
+ runList += kotlinJsLibOutput
+ runList += kotlinTestJsLibOutput
+
+ val context = Context.enter()
+ context.languageVersion = Context.VERSION_1_8
+ context.optimizationLevel = -1
+ val scope = context.initStandardObjects()
+
+ fun applyFile(fileToRun: String) {
+ val code = FileUtil.loadFile(File(fileToRun), CharsetToolkit.UTF8, true)
+ context.evaluateString(scope, code, fileToRun, 1, null)
+ }
+
+ applyFile(DIST_DIR_JS_PATH + RhinoUtils.RHINO_POLYFILLS_RELATIVE_PATH)
+ applyFile(kotlinJsLibOutput)
+ applyFile(kotlinTestJsLibOutput)
+ context.evaluateString(scope, "function ok() {}", "setup assertions", 0, null)
+ context.evaluateString(scope, RhinoUtils.SETUP_KOTLIN_OUTPUT, "setup kotlin output", 0, null)
+
+ allJsFiles.map { filesToMinify[it]?.outputName ?: it }.forEach(::applyFile)
+
+ val checker = RhinoFunctionResultChecker(testModuleName, testPackage, testFunction, expectedResult, withModuleSystem)
+ checker.runChecks(context, scope)
+ }
+
private inner class TestFileFactoryImpl : TestFileFactory, Closeable {
var testPackage: String? = null
val tmpDir = KotlinTestUtils.tmpDir("js-tests")
diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/ast/NameResolutionTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/ast/NameResolutionTest.kt
index 91c3ea54985..294e6208190 100644
--- a/js/js.tests/test/org/jetbrains/kotlin/js/test/ast/NameResolutionTest.kt
+++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/ast/NameResolutionTest.kt
@@ -57,8 +57,8 @@ class NameResolutionTest {
val expectedCode = FileUtil.loadFile(File(expectedName))
val parserScope = JsFunctionScope(JsRootScope(JsProgram()), "")
- 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()
diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/BasicOptimizerTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/BasicOptimizerTest.kt
index 728dd928cd0..3dd1bddf964 100644
--- a/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/BasicOptimizerTest.kt
+++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/BasicOptimizerTest.kt
@@ -55,7 +55,7 @@ abstract class BasicOptimizerTest(private var basePath: String) {
private fun checkOptimizer(unoptimizedCode: String, optimizedCode: String) {
val parserScope = JsFunctionScope(JsRootScope(JsProgram()), "")
- val unoptimizedAst = parse(unoptimizedCode, errorReporter, parserScope)
+ val unoptimizedAst = parse(unoptimizedCode, errorReporter, parserScope, "")
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, "")
Assert.assertEquals(astToString(optimizedAst), astToString(unoptimizedAst))
}
diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/reference/CallExpressionTranslator.java b/js/js.translator/src/org/jetbrains/kotlin/js/translate/reference/CallExpressionTranslator.java
index 23ace4b8742..c7219edeadf 100644
--- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/reference/CallExpressionTranslator.java
+++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/reference/CallExpressionTranslator.java
@@ -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());
}
}