Prototyping DCE tool for JS
This commit is contained in:
Generated
+1
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user