/* * Copyright 2009 The Closure Compiler Authors. * * 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 com.google.javascript.jscomp; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.javascript.rhino.IR; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; /** * Models an assignment that defines a variable and the removal of it. * */ class DefinitionsRemover { /** * This logic must match {@link isDefinitionNode}. * * @return an {@link Definition} object if the node contains a definition or {@code null} * otherwise. */ static Definition getDefinition(Node n, boolean isExtern) { Node parent = n.getParent(); if (parent == null) { return null; } if (NodeUtil.isVarDeclaration(n) && (isExtern || n.hasChildren())) { return new VarDefinition(n, isExtern); } else if (parent.isFunction() && parent.getFirstChild() == n) { if (!NodeUtil.isFunctionExpression(parent)) { return new NamedFunctionDefinition(parent, isExtern); } else if (!n.getString().isEmpty()) { return new FunctionExpressionDefinition(parent, isExtern); } } else if (parent.isAssign() && parent.getFirstChild() == n) { return new AssignmentDefinition(parent, isExtern); } else if (NodeUtil.isObjectLitKey(n)) { return new ObjectLiteralPropertyDefinition(parent, n, n.getFirstChild(), isExtern); } else if (parent.isParamList()) { Node function = parent.getParent(); return new FunctionArgumentDefinition(function, n, isExtern); } else if (parent.getToken() == Token.COLON && parent.getFirstChild() == n && isExtern) { Node grandparent = parent.getParent(); Preconditions.checkState(grandparent.getToken() == Token.LB); Preconditions.checkState(grandparent.getParent().getToken() == Token.LC); return new RecordTypePropertyDefinition(n); } else if (isExtern && n.isGetProp() && parent.isExprResult() && n.isQualifiedName()) { return new ExternalNameOnlyDefinition(n); } return null; } /** * This logic must match {@link getDefinition}. * * @return Whether a definition object can be created. */ static boolean isDefinitionNode(Node n) { Node parent = n.getParent(); if (parent == null) { return false; } if (NodeUtil.isVarDeclaration(n) && (n.isFromExterns() || n.hasChildren())) { return true; } else if (parent.isFunction() && parent.getFirstChild() == n) { if (!NodeUtil.isFunctionExpression(parent)) { return true; } else if (!n.getString().isEmpty()) { return true; } } else if (parent.isAssign() && parent.getFirstChild() == n) { return true; } else if (NodeUtil.isObjectLitKey(n)) { return true; } else if (parent.isParamList()) { return true; } else if (parent.getToken() == Token.COLON && parent.getFirstChild() == n && n.isFromExterns()) { Node grandparent = parent.getParent(); Preconditions.checkState(grandparent.getToken() == Token.LB); Preconditions.checkState(grandparent.getParent().getToken() == Token.LC); return true; } else if (n.isFromExterns() && parent.isExprResult() && n.isGetProp() && n.isQualifiedName()) { return true; } return false; } abstract static class Definition { private final boolean isExtern; Definition(boolean isExtern) { this.isExtern = isExtern; } /** * Removes this definition from the AST if it is not an extern. * * This method should not be called on a definition for which isExtern() * is true. */ public void remove(AbstractCompiler compiler) { if (!isExtern) { performRemove(compiler); } else { throw new IllegalStateException("Attempt to remove() an extern definition."); } } /** * Subclasses should override to remove the definition from the AST. */ protected abstract void performRemove(AbstractCompiler compiler); /** * Variable or property name represented by this definition. * For example, in the case of assignments this method would * return the NAME, GETPROP or GETELEM expression that acts as the * assignment left hand side. * * @return the L-Value associated with this definition. * The node's type is always NAME, GETPROP or GETELEM. */ public abstract Node getLValue(); /** * Value expression that acts as the right hand side of the * definition statement. */ public abstract Node getRValue(); /** * Returns true if the definition is an extern. */ public boolean isExtern() { return isExtern; } @Override public String toString() { return getLValue().getQualifiedName() + " = " + getRValue(); } } /** * Represents an name-only external definition. The definition's * RHS is missing. */ abstract static class IncompleteDefinition extends Definition { private static final ImmutableSet<Token> ALLOWED_TYPES = ImmutableSet.of(Token.NAME, Token.GETPROP, Token.GETELEM); private final Node lValue; IncompleteDefinition(Node lValue, boolean inExterns) { super(inExterns); Preconditions.checkNotNull(lValue); Preconditions.checkArgument( ALLOWED_TYPES.contains(lValue.getToken()), "Unexpected lValue type %s", lValue.getToken()); this.lValue = lValue; } @Override public Node getLValue() { return lValue; } @Override public Node getRValue() { return null; } } /** * Represents an unknown definition. */ static final class UnknownDefinition extends IncompleteDefinition { UnknownDefinition(Node lValue, boolean inExterns) { super(lValue, inExterns); } @Override public void performRemove(AbstractCompiler compiler) { throw new IllegalArgumentException("Can't remove an UnknownDefinition"); } } /** * Represents an name-only external definition. The definition's * RHS is missing. */ static final class ExternalNameOnlyDefinition extends IncompleteDefinition { ExternalNameOnlyDefinition(Node lValue) { super(lValue, true); } @Override public void performRemove(AbstractCompiler compiler) { throw new IllegalArgumentException( "Can't remove external name-only definition"); } } /** * Represents a function formal parameter. The definition's RHS is missing. */ static final class FunctionArgumentDefinition extends IncompleteDefinition { FunctionArgumentDefinition(Node function, Node argumentName, boolean inExterns) { super(argumentName, inExterns); Preconditions.checkArgument(function.isFunction()); Preconditions.checkArgument(argumentName.isName()); } @Override public void performRemove(AbstractCompiler compiler) { throw new IllegalArgumentException( "Can't remove a FunctionArgumentDefinition"); } } /** * Represents a function declaration or function expression. */ abstract static class FunctionDefinition extends Definition { protected final Node function; FunctionDefinition(Node node, boolean inExterns) { super(inExterns); Preconditions.checkArgument(node.isFunction()); function = node; } @Override public Node getLValue() { return function.getFirstChild(); } @Override public Node getRValue() { return function; } } /** * Represents a function declaration without assignment node such as * {@code function foo()}. */ static final class NamedFunctionDefinition extends FunctionDefinition { NamedFunctionDefinition(Node node, boolean inExterns) { super(node, inExterns); } @Override public void performRemove(AbstractCompiler compiler) { compiler.reportChangeToEnclosingScope(function); function.detach(); } } /** * Represents a function expression that acts as a RHS. The defined * name is only reachable from within the function. */ static final class FunctionExpressionDefinition extends FunctionDefinition { FunctionExpressionDefinition(Node node, boolean inExterns) { super(node, inExterns); Preconditions.checkArgument( NodeUtil.isFunctionExpression(node)); } @Override public void performRemove(AbstractCompiler compiler) { // replace internal name with "" function.replaceChild(function.getFirstChild(), IR.name("")); compiler.reportChangeToEnclosingScope(function.getFirstChild()); } } /** * Represents a declaration within an assignment. */ static final class AssignmentDefinition extends Definition { private final Node assignment; AssignmentDefinition(Node node, boolean inExterns) { super(inExterns); Preconditions.checkArgument(node.isAssign()); assignment = node; } @Override public void performRemove(AbstractCompiler compiler) { // A simple assignment. foo = bar() -> bar(); Node parent = assignment.getParent(); Node last = assignment.getLastChild(); assignment.removeChild(last); parent.replaceChild(assignment, last); compiler.reportChangeToEnclosingScope(parent); } @Override public Node getLValue() { return assignment.getFirstChild(); } @Override public Node getRValue() { return assignment.getLastChild(); } } /** * Represents member declarations using a record type from externs. * Example: /** @typedef {{prop: number}} *\/ var typdef; */ static final class RecordTypePropertyDefinition extends IncompleteDefinition { RecordTypePropertyDefinition(Node name) { super(IR.getprop(IR.objectlit(), name.cloneNode()), /** isExtern */ true); Preconditions.checkArgument(name.isString()); } @Override public void performRemove(AbstractCompiler compiler) { throw new UnsupportedOperationException("Can't remove RecordType def"); } } /** * Represents member declarations using a object literal. * Example: var x = { e : function() { } }; */ static final class ObjectLiteralPropertyDefinition extends Definition { private final Node literal; private final Node name; private final Node value; ObjectLiteralPropertyDefinition(Node lit, Node name, Node value, boolean isExtern) { super(isExtern); this.literal = lit; this.name = name; this.value = value; } @Override public void performRemove(AbstractCompiler compiler) { literal.removeChild(name); compiler.reportChangeToEnclosingScope(literal); } @Override public Node getLValue() { // TODO(user) revisit: object literal definitions are an example // of definitions whose LHS doesn't correspond to a node that // exists in the AST. We will have to change the return type of // getLValue sooner or later in order to provide this added // flexibility. switch (name.getToken()) { case SETTER_DEF: case GETTER_DEF: case STRING_KEY: // TODO(johnlenz): return a GETELEM for quoted strings. return IR.getprop( IR.objectlit(), IR.string(name.getString())); default: throw new IllegalStateException("unexpected"); } } @Override public Node getRValue() { return value; } } /** * Represents a VAR declaration with an assignment. */ static final class VarDefinition extends Definition { private final Node name; VarDefinition(Node node, boolean inExterns) { super(inExterns); Preconditions.checkArgument(NodeUtil.isVarDeclaration(node)); Preconditions.checkArgument(inExterns || node.hasChildren(), "VAR Declaration of %s must be assigned a value.", node.getString()); name = node; } @Override public void performRemove(AbstractCompiler compiler) { Node var = name.getParent(); Preconditions.checkState(var.getFirstChild() == var.getLastChild(), "AST should be normalized first"); Node parent = var.getParent(); Node rValue = name.removeFirstChild(); Preconditions.checkState(!NodeUtil.isLoopStructure(parent)); parent.replaceChild(var, NodeUtil.newExpr(rValue)); compiler.reportChangeToEnclosingScope(parent); } @Override public Node getLValue() { return name; } @Override public Node getRValue() { return name.getFirstChild(); } } }