All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.kotlin.js.dce.Analyzer.kt Maven / Gradle / Ivy

There is a newer version: 2.1.0-RC
Show newest version
/*
 * 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.collectDefinedNames
import org.jetbrains.kotlin.js.inline.util.collectLocalVariables
import org.jetbrains.kotlin.js.translate.context.Namer

class Analyzer(private val context: Context) : JsVisitor() {
    private val processedFunctions = mutableSetOf()
    private val postponedFunctions = mutableMapOf()
    private val nodeMap = mutableMapOf()
    private val astNodesToEliminate = mutableSetOf()
    private val astNodesToSkip = mutableSetOf()
    private val invocationsToSkip = mutableSetOf()
    val moduleMapping = mutableMapOf()
    private val functionsToEnter = mutableSetOf()
    private val functionsToSkip = mutableSetOf()

    val analysisResult = object : AnalysisResult {
        override val nodeMap: Map get() = [email protected]

        override val astNodesToEliminate: Set get() = [email protected]

        override val astNodesToSkip: Set get() = [email protected]

        override val functionsToEnter: Set get() = [email protected]

        override val invocationsToSkip: Set get() = [email protected]

        override val functionsToSkip: Set get() = [email protected]
    }

    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
        when (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
                }
            }
            is JsFunction -> expression.name?.let { context.nodes[it]?.original }?.let {
                nodeMap[x] = it
                it.addFunction(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
                    }
                }

                when {
                    // Object.defineProperty()
                    context.isObjectDefineProperty(function) ->
                        handleObjectDefineProperty(x, expression.arguments.getOrNull(0), expression.arguments.getOrNull(1),
                                                   expression.arguments.getOrNull(2))

                    // Kotlin.defineModule()
                    context.isDefineModule(function) ->
                        // (just remove it)
                        astNodesToEliminate += x
                    context.isAmdDefine(function) ->
                        handleAmdDefine(expression, expression.arguments)
                }
            }
        }
    }

    private fun handleObjectDefineProperty(statement: JsStatement, target: JsExpression?, propertyName: JsExpression?,
                                           propertyDescriptor: JsExpression?) {
        if (target == null || propertyName !is JsStringLiteral || propertyDescriptor == null) return
        val targetNode = context.extractNode(target) ?: return

        val memberNode = targetNode.member(propertyName.value)
        nodeMap[statement] = memberNode
        memberNode.hasSideEffects = true

        // Object.defineProperty(instance, name, { get: value, ... })
        if (propertyDescriptor is JsObjectLiteral) {
            for (initializer in propertyDescriptor.propertyInitializers) {
                // process as if it was instance.name = value
                processAssignment(statement, JsNameRef(propertyName.value, target), initializer.valueExpr)
            }
        }
        // Object.defineProperty(instance, name, Object.getOwnPropertyDescriptor(otherInstance))
        else if (propertyDescriptor is JsInvocation) {
            val function = propertyDescriptor.qualifier
            if (context.isObjectGetOwnPropertyDescriptor(function)) {
                val source = propertyDescriptor.arguments.getOrNull(0)
                val sourcePropertyName = propertyDescriptor.arguments.getOrNull(1)
                if (source != null && sourcePropertyName is JsStringLiteral) {
                    // process as if it was instance.name = otherInstance.name
                    processAssignment(statement, JsNameRef(propertyName.value, target), JsNameRef(sourcePropertyName.value, source))
                }
            }
        }
    }

    private fun handleAmdDefine(invocation: JsInvocation, arguments: List) {
        // Handle both named and anonymous modules
        val argumentsWithoutName = when (arguments.size) {
            2 -> arguments
            3 -> arguments.drop(1)
            else -> return
        }

        val dependencies = argumentsWithoutName[0] as? JsArrayLiteral ?: return

        // Function can be either a function() { ... } or a reference to parameter out outer function which is known to take
        // function literal
        val functionRef = argumentsWithoutName[1]
        val function = when (functionRef) {
            is JsFunction -> functionRef
            is JsNameRef -> {
                if (functionRef.qualifier != null) return
                postponedFunctions[functionRef.name] ?: return
            }
            else -> return
        }

        val dependencyNodes = dependencies.expressions
            .map { it as? JsStringLiteral ?: return }
            .map { if (it.value == "exports") context.currentModule else context.globalScope.member(it.value) }

        enterFunctionWithGivenNodes(function, dependencyNodes)
        astNodesToSkip += invocation.qualifier
    }

    override fun visitBlock(x: JsBlock) {
        val newModule = moduleMapping[x]
        if (newModule != null) {
            context.currentModule = context.globalScope.member(newModule)
        }
        x.statements.forEach { accept(it) }
    }

    override fun visitIf(x: JsIf) {
        accept(x.thenStatement)
        x.elseStatement?.accept(this)
    }

    override fun visitReturn(x: JsReturn) {
        val expr = x.expression
        if (expr != null) {
            context.extractNode(expr)?.let {
                nodeMap[x] = it
            }
        }
    }

    private fun processAssignment(node: JsNode?, lhs: JsExpression, rhs: JsExpression): Node? {
        val leftNode = context.extractNode(lhs)
        val rightNode = context.extractNode(rhs)

        if (leftNode != null && rightNode != null) {
            // If both left and right expressions are fully-qualified names, alias them
            leftNode.alias(rightNode)
            return leftNode
        }
        else if (leftNode != null) {
            // lhs = foo()
            when {
                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', )
                    // where  is one of
                    //   - function() { ... }
                    //   - wrapFunction(function() { ... })
                    if (context.isDefineInlineFunction(function) && rhs.arguments.size == 2) {
                        tryExtractFunction(rhs.arguments[1])?.let { (inlineableFunction, additionalDeps) ->
                            leftNode.addFunction(inlineableFunction)
                            val defineInlineFunctionNode = context.extractNode(function)
                            if (defineInlineFunctionNode != null) {
                                leftNode.addDependency(defineInlineFunctionNode)
                            }
                            additionalDeps.forEach {
                                leftNode.addDependency(it)
                            }
                            return leftNode
                        }
                    }

                    tryExtractFunction(rhs)?.let { (functionBody, additionalDeps) ->
                        leftNode.addFunction(functionBody)
                        additionalDeps.forEach {
                            leftNode.addDependency(it)
                        }
                        return leftNode
                    }
                }
                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)
                            }
                        }
                    }
                rhs is JsFunction -> {
                    // lhs = function() { ... }
                    // During reachability tracking phase: eliminate it if lhs is unreachable, traverse function otherwise
                    leftNode.addFunction(rhs)
                    return leftNode
                }
                leftNode.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.addExpression(rhs)
                    return leftNode
                }
                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 tryExtractFunction(expression: JsExpression): Pair>? {
        when (expression) {
            is JsFunction -> return Pair(expression, emptyList())
            is JsInvocation -> {
                if (context.isWrapFunction(expression.qualifier)) {
                    (expression.arguments.getOrNull(0) as? JsFunction)?.let { wrapper ->
                        val statementsWithoutBody = wrapper.body.statements.filter { it !is JsReturn }
                        JsBlock(statementsWithoutBody).let {
                            context.addNodesForLocalVars(collectDefinedNames(it))
                            accept(it)
                        }

                        val wrapperNode = context.extractNode(expression.qualifier)?.also {
                            functionsToSkip += it
                        }
                        val body = wrapper.body.statements.filterIsInstance().first().expression as JsFunction
                        return Pair(body, listOfNotNull(wrapperNode))
                    }
                }
            }
        }

        return null
    }

    private fun handleObjectCreate(target: Node, arg: JsExpression?) {
        if (arg == null) return

        val prototypeNode = context.extractNode(arg) ?: return
        target.addDependency(prototypeNode.original)
        target.addExpression(arg)
    }

    // Handle typeof foo === 'undefined' ? {} : foo, where foo is FQN
    // Assume foo
    // This is used by UMD wrapper
    private fun extractVariableInitializedByEmptyObject(expression: JsExpression): Node? {
        if (expression !is JsConditional) return null

        val testExpr = expression.testExpression as? JsBinaryOperation ?: return null
        if (testExpr.operator != JsBinaryOperator.REF_EQ) return null

        val testExprLhs = testExpr.arg1 as? JsPrefixOperation ?: return null
        if (testExprLhs.operator != JsUnaryOperator.TYPEOF) return null
        val testExprNode = context.extractNode(testExprLhs.arg) ?: return null

        val testExprRhs = testExpr.arg2 as? JsStringLiteral ?: return null
        if (testExprRhs.value != "undefined") return null

        val thenExpr = expression.thenExpression as? JsObjectLiteral ?: return null
        if (thenExpr.propertyInitializers.isNotEmpty()) return null

        val elseNode = context.extractNode(expression.elseExpression) ?: return null

        if (testExprNode.original != elseNode.original) return null
        return testExprNode.original
    }

    // foo(), where foo is either function literal or parameter of outer function that takes function literal.
    // The latter case is required to handle UMD wrapper
    // Skip arguments during reachability tracker phase
    // Traverse function's body
    private fun enterFunction(function: JsFunction, arguments: List) {
        functionsToEnter += function
        context.addNodesForLocalVars(function.collectLocalVariables())
        context.markSpecialFunctions(function.body)

        for ((param, arg) in function.parameters.zip(arguments)) {
            if (arg is JsFunction && arg.name == null && isProperFunctionalParameter(arg.body, param)) {
                postponedFunctions[param.name] = arg
            }
            else {
                if (processAssignment(function, param.name.makeRef(), arg) != null) {
                    astNodesToSkip += arg
                }
            }
        }

        processFunction(function)
    }

    private fun enterFunctionWithGivenNodes(function: JsFunction, arguments: List) {
        functionsToEnter += function
        context.addNodesForLocalVars(function.collectLocalVariables())
        context.markSpecialFunctions(function.body)

        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
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy