Prototyping DCE tool for JS

This commit is contained in:
Alexey Andreev
2017-03-31 20:03:41 +03:00
parent d617b1d869
commit 9e89213d66
26 changed files with 1445 additions and 22 deletions
+1
View File
@@ -68,6 +68,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" />
+3
View File
@@ -112,6 +112,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>
@@ -152,6 +153,7 @@
<include name="js.inliner/**"/>
<include name="js.parser/**"/>
<include name="js.serializer/**"/>
<include name="js.dce/**"/>
</patternset>
<path id="compilerSources.path">
@@ -265,6 +267,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>
+1
View File
@@ -24,5 +24,6 @@
<orderEntry type="module" module-name="builtins-serializer" />
<orderEntry type="module" module-name="frontend.script" />
<orderEntry type="module" module-name="javac-wrapper" />
<orderEntry type="module" module-name="js.dce" />
</component>
</module>
@@ -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<String>) {
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)
}
}
+16
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>
@@ -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>
}
@@ -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<JsFunction>()
private val postponedFunctions = mutableMapOf<JsName, JsFunction>()
private val nodeMap = mutableMapOf<JsNode, Node>()
private val astNodesToEliminate = mutableSetOf<JsNode>()
private val astNodesToSkip = mutableSetOf<JsNode>()
private val invocationsToSkip = mutableSetOf<JsInvocation>()
val moduleMapping = mutableMapOf<JsStatement, String>()
private val functionsToEnter = mutableSetOf<JsFunction>()
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.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<Node>) {
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
}
}
@@ -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<JsName, Node>()
var thisNode: Node? = globalScope
val namesOfLocalVars = mutableSetOf<JsName>()
fun addNodesForLocalVars(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 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<Node>()
private val expressionsImpl = mutableSetOf<JsExpression>()
private val functionsImpl = mutableSetOf<JsFunction>()
private var hasSideEffectsImpl = false
private var reachableImpl = false
private var declarationReachableImpl = false
private val membersImpl = mutableMapOf<String, Node>()
private val usedByAstNodesImpl = mutableSetOf<JsNode>()
private var rank = 0
val dependencies: MutableSet<Node> get() = original.dependenciesImpl
val expressions: MutableSet<JsExpression> get() = original.expressionsImpl
val functions: MutableSet<JsFunction> 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<JsNode> get() = original.usedByAstNodesImpl
val memberNames: MutableSet<String> 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<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) {
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<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)
}
@@ -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<JsBlock, String>()
private val reachableNames = mutableSetOf<String>()
var reachableNodes = setOf<Node>()
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<InputFile>,
rootReachableNames: Set<String>,
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")
}
}
}
}
@@ -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<Context.Node>)
@@ -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
}
@@ -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)
@@ -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.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<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--
}
}
@@ -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)
@@ -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<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)
}
}
@@ -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 ?: "<unknown file>")
if (statements.isEmpty()) {
context.trace.report(ErrorsJs.JSCODE_NO_JAVASCRIPT_PRODUCED.on(argument))
@@ -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<String, ModuleInfo>()
@@ -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,
@@ -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<String, JsName>()
init {
currentScope += collectDefinedNames(this@fixForwardNameReferences).associateBy { it.ident }
}
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) {}
})
}
@@ -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 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;
}
}
@@ -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<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, 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 <T> Node.toJsAst(scope: JsScope, mapAction: JsAstMapper.(Node)->T): T =
JsAstMapper(scope).mapAction(this)
private fun <T> 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)
@@ -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()
+1
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>
@@ -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<String>, generatedJsFiles: List<Pair<String, TestModule>>,
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<String>()
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<TestModule, TestFile>, Closeable {
var testPackage: String? = null
val tmpDir = KotlinTestUtils.tmpDir("js-tests")
@@ -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>()
@@ -55,7 +55,7 @@ abstract class BasicOptimizerTest(private var basePath: String) {
private fun checkOptimizer(unoptimizedCode: String, optimizedCode: String) {
val parserScope = JsFunctionScope(JsRootScope(JsProgram()), "<js fun>")
val unoptimizedAst = parse(unoptimizedCode, errorReporter, parserScope)
val unoptimizedAst = parse(unoptimizedCode, errorReporter, parserScope, "<unknown file>")
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, "<unknown file>")
Assert.assertEquals(astToString(optimizedAst), astToString(unoptimizedAst))
}
@@ -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());
}
}