com.google.javascript.rhino.Node Maven / Gradle / Ivy
Show all versions of com.liferay.frontend.js.minifier
/*
*
* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Rhino code, released
* May 6, 1999.
*
* The Initial Developer of the Original Code is
* Netscape Communications Corporation.
* Portions created by the Initial Developer are Copyright (C) 1997-1999
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Norris Boyd
* Roger Lawrence
* Mike McCabe
*
* Alternatively, the contents of this file may be used under the terms of
* the GNU General Public License Version 2 or later (the "GPL"), in which
* case the provisions of the GPL are applicable instead of those above. If
* you wish to allow use of your version of this file only under the terms of
* the GPL and not to allow others to use your version of this file under the
* MPL, indicate your decision by deleting the provisions above and replacing
* them with the notice and other provisions required by the GPL. If you do
* not delete the provisions above, a recipient may use your version of this
* file under either the MPL or the GPL.
*
* ***** END LICENSE BLOCK ***** */
package com.google.javascript.rhino;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.base.JSCompDoubles.isPositive;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.DoNotCall;
import com.google.javascript.jscomp.colors.Color;
import com.google.javascript.jscomp.serialization.NodeProperty;
import com.google.javascript.rhino.StaticSourceFile.SourceKind;
import com.google.javascript.rhino.jstype.JSType;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.annotation.Nullable;
/**
* This class implements the root of the intermediate representation.
*
*/
public class Node {
enum Prop {
// Is this Node within parentheses
IS_PARENTHESIZED,
// Contains non-JSDoc comment
NON_JSDOC_COMMENT,
TRAILING_NON_JSDOC_COMMENT,
// Contains a JSDocInfo object
JSDOC_INFO,
// Whether incrdecr is pre (false) or post (true)
INCRDECR,
// Set to indicate a quoted object lit key
QUOTED,
// A synthetic block. Used to make processing simpler, and does not represent a real block in
// the source.
SYNTHETIC,
// Used to indicate BLOCK that is added
ADDED_BLOCK,
// Function or constructor call side effect flags.
SIDE_EFFECT_FLAGS,
// The variable or property is constant.
// TODO(lukes): either document the differences or otherwise reconcile with CONSTANT_VAR_FLAGS
IS_CONSTANT_NAME,
// The variable creates a namespace.
IS_NAMESPACE,
// The presence of the "use strict" directive on this node.
USE_STRICT,
// ES5 distinguishes between direct and indirect calls to eval.
DIRECT_EVAL,
// A CALL without an explicit "this" value.
FREE_CALL,
// A StaticSourceFile indicating the file where this node lives.
SOURCE_FILE,
// The id of the input associated with this node.
INPUT_ID,
// For passes that work only on changed funs.
CHANGE_TIME,
// An object that's used for goog.object.reflect-style reflection.
REFLECTED_OBJECT,
// Set if class member definition is static
STATIC_MEMBER,
// Set if the node is a Generator function or member method.
GENERATOR_FN,
// Set if the node is an arrow function.
ARROW_FN,
// http://tc39.github.io/ecmascript-asyncawait/
ASYNC_FN,
// Set if a yield is a "yield all"
YIELD_ALL,
// Set if a export is a "default" export
EXPORT_DEFAULT,
// Set if an export is a "*"
EXPORT_ALL_FROM,
// A variable is inferred or declared as const meaning it is only ever assigned once at its
// declaration site.
// This is an int prop that holds a bitset of {@link ConstantVarFlags} values.
CONSTANT_VAR_FLAGS,
// Used by the ES6-to-ES3 translator.
IS_GENERATOR_MARKER,
// Used by the ES6-to-ES3 translator.
IS_GENERATOR_SAFE,
// A computed property that has the method syntax
// ( [prop]() {...} )
// rather than the property definition syntax
// ( [prop]: value ).
COMPUTED_PROP_METHOD,
// A computed property in a getter, e.g. var obj = { get [prop]() {...} };
COMPUTED_PROP_GETTER,
// A computed property in a setter, e.g. var obj = 32;
COMPUTED_PROP_SETTER,
// A computed property that's a variable, e.g. [prop]: string;
COMPUTED_PROP_VARIABLE,
// Used to attach TypeDeclarationNode ASTs to Nodes which represent a typed NAME or FUNCTION.
DECLARED_TYPE_EXPR,
// The type of an expression before the cast. This will be present only if the expression is
// casted.
TYPE_BEFORE_CAST,
// Indicates that this epxression was casted but we don't necessarily know to which type
COLOR_FROM_CAST,
// The node is an optional parameter or property in ES6 Typed syntax.
OPT_ES6_TYPED,
// Generic type list in ES6 typed syntax.
GENERIC_TYPE,
// "implements" clause in ES6 typed syntax.
IMPLEMENTS,
// This node is a TypeScript ConstructSignature
CONSTRUCT_SIGNATURE,
// TypeScript accessibility modifiers (public, protected, private)
ACCESS_MODIFIER,
// Indicates the node should not be indexed by analysis tools.
NON_INDEXABLE,
// Parse results stored on SCRIPT nodes to allow replaying parse warnings/errors when cloning
// cached ASTs.
PARSE_RESULTS,
// Indicates that a SCRIPT node is a goog.module. Remains set after the goog.module is
// desugared.
GOOG_MODULE,
// Attaches a FeatureSet to SCRIPT nodes.
FEATURE_SET,
// Indicates a TypeScript abstract method or class, for use in Migrants
IS_TYPESCRIPT_ABSTRACT,
// Indicates that a SCRIPT represents a transpiled file
TRANSPILED,
// For passes that work only on deleted funs.
DELETED,
// Indicates that the node is an alias or a name from goog.require'd module or ES6
// module. Aliases are desugared and inlined by compiler passes but we need to preserve them for
// building index.
MODULE_ALIAS,
// Mark a parameter as unused. Used to defer work from RemovedUnusedVars to OptimizeParameters.
IS_UNUSED_PARAMETER,
// Mark a property as a module export so that collase properties can act on it.
MODULE_EXPORT,
// Indicates that a property {x:x} was originally parsed as {x}.
IS_SHORTHAND_PROPERTY,
// Indicates that a SCRIPT node is or was an ES module. Remains set after the module is
// rewritten.
ES6_MODULE,
// Record the type associated with a @typedef to enable looking up typedef in the AST possible
// without saving the type scope.
TYPEDEF_TYPE,
// Indicate that a OPTCHAIN_GETPROP, OPTCHAIN_GETELEM, or OPTCHAIN_CALL is the start of an
// optional chain.
START_OF_OPT_CHAIN,
// Indicates a trailing comma in an array literal, object literal, parameter list, or argument
// list
TRAILING_COMMA,
// Indicates that a variable declaration was synthesized to provide a declaration of some
// name referenced in code but never defined, as most compiler passes expect that to be an
// invariant.
// Only present in the "synthetic externs file". Builds initialized using a
// "TypedAST filesystem" will delete any such declarations present in a different compilation
// shard
SYNTHESIZED_UNFULFILLED_NAME_DECLARATION,
}
// Avoid cloning "values" repeatedly in hot code, we save it off now.
private static final Prop[] PROP_VALUES = Prop.values();
/**
* Get the NonJSDoc comment string attached to this node.
*
* @return the information or empty string if no nonJSDoc is attached to this node
*/
public final String getNonJSDocCommentString() {
if (getProp(Prop.NON_JSDOC_COMMENT) == null) {
return "";
}
return ((NonJSDocComment) getProp(Prop.NON_JSDOC_COMMENT)).getCommentString();
}
public final NonJSDocComment getNonJSDocComment() {
return (NonJSDocComment) getProp(Prop.NON_JSDOC_COMMENT);
}
public final String getTrailingNonJSDocCommentString() {
if (getProp(Prop.TRAILING_NON_JSDOC_COMMENT) == null) {
return "";
}
return ((NonJSDocComment) getProp(Prop.TRAILING_NON_JSDOC_COMMENT)).getCommentString();
}
public final NonJSDocComment getTrailingNonJSDocComment() {
return (NonJSDocComment) getProp(Prop.TRAILING_NON_JSDOC_COMMENT);
}
/** Sets the NonJSDoc comment attached to this node. */
public final Node setNonJSDocComment(NonJSDocComment comment) {
putProp(Prop.NON_JSDOC_COMMENT, comment);
return this;
}
public final Node setTrailingNonJSDocComment(NonJSDocComment comment) {
putProp(Prop.TRAILING_NON_JSDOC_COMMENT, comment);
return this;
}
/** Sets whether this node is inside parentheses. */
public final void setIsParenthesized(boolean b) {
checkState(IR.mayBeExpression(this));
putBooleanProp(Prop.IS_PARENTHESIZED, b);
}
/** Check whether node was inside parentheses. */
public final boolean getIsParenthesized() {
return getBooleanProp(Prop.IS_PARENTHESIZED);
}
// TODO(sdh): Get rid of these by using accessor methods instead.
// These export instances of a private type, which is awkward but a side effect is that it
// prevents anyone from introducing problemmatic uses of the general-purpose accessors.
public static final Prop INCRDECR_PROP = Prop.INCRDECR;
public static final Prop QUOTED_PROP = Prop.QUOTED;
public static final Prop IS_CONSTANT_NAME = Prop.IS_CONSTANT_NAME;
public static final Prop IS_NAMESPACE = Prop.IS_NAMESPACE;
public static final Prop DIRECT_EVAL = Prop.DIRECT_EVAL;
public static final Prop FREE_CALL = Prop.FREE_CALL;
public static final Prop REFLECTED_OBJECT = Prop.REFLECTED_OBJECT;
public static final Prop STATIC_MEMBER = Prop.STATIC_MEMBER;
public static final Prop GENERATOR_FN = Prop.GENERATOR_FN;
public static final Prop YIELD_ALL = Prop.YIELD_ALL;
public static final Prop EXPORT_DEFAULT = Prop.EXPORT_DEFAULT;
public static final Prop EXPORT_ALL_FROM = Prop.EXPORT_ALL_FROM;
public static final Prop COMPUTED_PROP_METHOD = Prop.COMPUTED_PROP_METHOD;
public static final Prop COMPUTED_PROP_GETTER = Prop.COMPUTED_PROP_GETTER;
public static final Prop COMPUTED_PROP_SETTER = Prop.COMPUTED_PROP_SETTER;
public static final Prop COMPUTED_PROP_VARIABLE = Prop.COMPUTED_PROP_VARIABLE;
public static final Prop OPT_ES6_TYPED = Prop.OPT_ES6_TYPED;
public static final Prop GENERIC_TYPE_LIST = Prop.GENERIC_TYPE;
public static final Prop IMPLEMENTS = Prop.IMPLEMENTS;
public static final Prop CONSTRUCT_SIGNATURE = Prop.CONSTRUCT_SIGNATURE;
public static final Prop ACCESS_MODIFIER = Prop.ACCESS_MODIFIER;
public static final Prop PARSE_RESULTS = Prop.PARSE_RESULTS;
public static final Prop GOOG_MODULE = Prop.GOOG_MODULE;
public static final Prop FEATURE_SET = Prop.FEATURE_SET;
public static final Prop IS_TYPESCRIPT_ABSTRACT = Prop.IS_TYPESCRIPT_ABSTRACT;
public static final Prop TRANSPILED = Prop.TRANSPILED;
public static final Prop MODULE_ALIAS = Prop.MODULE_ALIAS;
public static final Prop MODULE_EXPORT = Prop.MODULE_EXPORT;
public static final Prop IS_SHORTHAND_PROPERTY = Prop.IS_SHORTHAND_PROPERTY;
public static final Prop ES6_MODULE = Prop.ES6_MODULE;
private static final class NumberNode extends Node {
private static final long serialVersionUID = 1L;
private double number;
NumberNode(double number) {
super(Token.NUMBER);
this.setDouble(number);
}
@Override
public boolean isEquivalentTo(
Node node, boolean compareType, boolean recur, boolean jsDoc, boolean sideEffect) {
return super.isEquivalentTo(node, compareType, recur, jsDoc, sideEffect)
&& (this.number == ((NumberNode) node).number); // -0.0 and NaN are forbidden.
}
@Override
NumberNode cloneNode(boolean cloneTypeExprs) {
NumberNode clone = new NumberNode(number);
copyBaseNodeFields(this, clone, cloneTypeExprs);
return clone;
}
}
private static final class BigIntNode extends Node {
private static final long serialVersionUID = 1L;
private BigInteger bigint;
BigIntNode(BigInteger bigint) {
super(Token.BIGINT);
setBigInt(bigint);
}
@Override
public boolean isEquivalentTo(
Node node, boolean compareType, boolean recur, boolean jsDoc, boolean sideEffect) {
return super.isEquivalentTo(node, compareType, recur, jsDoc, sideEffect)
&& getBigInt().equals(node.getBigInt());
}
@Override
BigIntNode cloneNode(boolean cloneTypeExprs) {
BigIntNode clone = new BigIntNode(bigint);
copyBaseNodeFields(this, clone, cloneTypeExprs);
return clone;
}
}
private static final class StringNode extends Node {
private static final long serialVersionUID = 1L;
private String str;
// Only for cloneNode
private StringNode(Token token) {
super(token);
}
StringNode(Token token, String str) {
super(token);
setString(str);
}
@Override
public boolean isEquivalentTo(
Node node, boolean compareType, boolean recur, boolean jsDoc, boolean sideEffect) {
return super.isEquivalentTo(node, compareType, recur, jsDoc, sideEffect)
&& RhinoStringPool.uncheckedEquals(this.str, ((StringNode) node).str);
}
@Override
StringNode cloneNode(boolean cloneTypeExprs) {
StringNode clone = new StringNode(this.getToken());
copyBaseNodeFields(this, clone, cloneTypeExprs);
clone.str = this.str;
return clone;
}
}
private static final class TemplateLiteralSubstringNode extends Node {
/**
* The cooked version of the template literal substring.
*
* Null iff the raw string contains an uncookable escape sequence.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#es2018_revision_of_illegal_escape_sequences
*/
@Nullable private final String cooked;
/** The raw version of the template literal substring, is not null */
private final String raw;
TemplateLiteralSubstringNode(@Nullable String cooked, String raw) {
super(Token.TEMPLATELIT_STRING);
// RhinoStringPool is null-hostile.
this.cooked = (cooked == null) ? null : RhinoStringPool.addOrGet(cooked);
this.raw = RhinoStringPool.addOrGet(raw);
}
@Override
public boolean isEquivalentTo(
Node node, boolean compareType, boolean recur, boolean jsDoc, boolean sideEffect) {
if (!super.isEquivalentTo(node, compareType, recur, jsDoc, sideEffect)) {
return false;
}
TemplateLiteralSubstringNode castNode = (TemplateLiteralSubstringNode) node;
return RhinoStringPool.uncheckedEquals(this.raw, castNode.raw)
&& RhinoStringPool.uncheckedEquals(this.cooked, castNode.cooked);
}
@Override
TemplateLiteralSubstringNode cloneNode(boolean cloneTypeExprs) {
TemplateLiteralSubstringNode clone = new TemplateLiteralSubstringNode(this.cooked, this.raw);
copyBaseNodeFields(this, clone, cloneTypeExprs);
return clone;
}
}
private abstract static class PropListItem {
final @Nullable PropListItem next;
final byte propType;
PropListItem(byte propType, @Nullable PropListItem next) {
this.propType = propType;
this.next = next;
}
public abstract int getIntValue();
public abstract Object getObjectValue();
public abstract PropListItem chain(@Nullable PropListItem next);
}
// A base class for Object storing props
private static final class ObjectPropListItem extends PropListItem {
private final Object objectValue;
ObjectPropListItem(byte propType, Object objectValue, @Nullable PropListItem next) {
super(propType, next);
this.objectValue = checkNotNull(objectValue);
}
@Override
public int getIntValue() {
throw new UnsupportedOperationException();
}
@Override
public Object getObjectValue() {
return objectValue;
}
@Override
public String toString() {
return String.valueOf(objectValue);
}
@Override
public PropListItem chain(@Nullable PropListItem next) {
return new ObjectPropListItem(propType, objectValue, next);
}
}
// A base class for int storing props
private static final class IntPropListItem extends PropListItem {
final int intValue;
IntPropListItem(byte propType, int intValue, @Nullable PropListItem next) {
super(propType, next);
this.intValue = intValue;
checkState(this.intValue != 0);
}
@Override
public int getIntValue() {
return intValue;
}
@Override
public Object getObjectValue() {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
return String.valueOf(intValue);
}
@Override
public PropListItem chain(@Nullable PropListItem next) {
return new IntPropListItem(propType, intValue, next);
}
}
public Node(Token token) {
this.token = token;
}
public Node(Token token, Node child) {
this(token);
this.first = child;
child.checkDetached();
// child.next remains null;
child.previous = child;
child.parent = this;
}
public Node(Token token, Node left, Node right) {
this(token);
this.first = left;
left.checkDetached();
left.next = right;
left.previous = right;
left.parent = this;
right.checkDetached();
// right.next remains null;
right.previous = left;
right.parent = this;
}
public Node(Token token, Node left, Node mid, Node right) {
this(token);
this.first = left;
left.checkDetached();
left.next = mid;
left.previous = right;
left.parent = this;
mid.checkDetached();
mid.next = right;
mid.previous = left;
mid.parent = this;
right.checkDetached();
// right.next remains null;
right.previous = mid;
right.parent = this;
}
public static Node newNumber(double number) {
return new NumberNode(number);
}
public static Node newBigInt(BigInteger bigint) {
return new BigIntNode(bigint);
}
public static Node newString(String str) {
return new StringNode(Token.STRINGLIT, str);
}
public static Node newString(Token token, String str) {
return new StringNode(token, str);
}
public static Node newTemplateLitString(String cooked, String raw) {
return new TemplateLiteralSubstringNode(cooked, raw);
}
public final Token getToken() {
return token;
}
public final void setToken(Token token) {
this.token = token;
}
public final boolean hasChildren() {
return first != null;
}
public final Node getOnlyChild() {
checkState(hasOneChild());
return first;
}
@Nullable
public final Node getFirstChild() {
return first;
}
/**
* Get the first child of the first child. This method assumes that the first child exists.
*
* @return The first child of the first child.
*/
@Nullable
public final Node getFirstFirstChild() {
return first.first;
}
@Nullable
public final Node getSecondChild() {
return first.next;
}
@Nullable
public final Node getLastChild() {
return first != null ? first.previous : null;
}
@Nullable
public final Node getNext() {
return next;
}
@Nullable
public final Node getPrevious() {
return this == parent.first ? null : previous;
}
/**
* Gets the ith child, note that this is O(N) where N is the number of children.
*
* @param i The index
* @return The ith child
*/
public final Node getChildAtIndex(int i) {
Node n = first;
while (i > 0) {
n = n.next;
i--;
}
return n;
}
/**
* Gets the index of a child, note that this is O(N) where N is the number of children.
*
* @param child The child
* @return The index of the child
*/
public final int getIndexOfChild(Node child) {
Node n = first;
int i = 0;
while (n != null) {
if (child == n) {
return i;
}
n = n.next;
i++;
}
return -1;
}
public final void addChildToFront(Node child) {
checkArgument(child.parent == null);
checkArgument(child.next == null);
checkArgument(child.previous == null);
child.parent = this;
child.next = first;
if (first == null) {
// NOTE: child.next remains null
child.previous = child;
} else {
Node last = first.previous;
// NOTE: last.next remains null
child.previous = last;
child.next = first;
first.previous = child;
}
first = child;
}
public final void addChildToBack(Node child) {
checkArgument(
child.parent == null,
"Cannot add already-owned child node.\nChild: %s\nExisting parent: %s\nNew parent: %s",
child,
child.parent,
this);
checkArgument(child.next == null);
checkArgument(child.previous == null);
if (first == null) {
// NOTE: child.next remains null
child.previous = child;
first = child;
} else {
Node last = first.previous;
last.next = child;
// NOTE: child.next remains null
child.previous = last;
first.previous = child;
}
child.parent = this;
}
/**
* Add all children to the front of this node.
*
* @param children first of a list of sibling nodes who have no parent. NOTE: Usually you would
* get this argument from a removeChildren() call. A single detached node will not work
* because its sibling pointers will not be correctly initialized.
*/
public final void addChildrenToFront(@Nullable Node children) {
if (children == null) {
return; // removeChildren() returns null when there are none
}
// NOTE: If there is only one sibling, its previous pointer must point to itself.
// Null indicates a fully detached node.
checkNotNull(children.previous, children);
for (Node child = children; child != null; child = child.next) {
checkArgument(child.parent == null);
child.parent = this;
}
Node lastSib = children.previous;
if (first != null) {
Node last = first.previous;
// NOTE: last.next remains null
children.previous = last;
lastSib.next = first;
first.previous = lastSib;
}
first = children;
}
public final void addChildrenToBack(Node children) {
addChildrenAfter(children, getLastChild());
}
public final void insertAfter(Node existing) {
existing.checkAttached();
this.checkDetached();
final Node existingParent = existing.parent;
final Node existingNext = existing.next;
this.parent = existingParent;
existing.next = this;
this.previous = existing;
if (existingNext == null) {
existingParent.first.previous = this;
// this.next remains null
} else {
existingNext.previous = this;
this.next = existingNext;
}
}
public final void insertBefore(Node existing) {
existing.checkAttached();
this.checkDetached();
final Node existingParent = existing.parent;
final Node existingPrevious = existing.previous;
this.parent = existingParent;
this.next = existing;
existing.previous = this;
this.previous = existingPrevious;
if (existingPrevious.next == null) {
existingParent.first = this;
// existingPrevious.next remains null
} else {
// existingParent.first remains existing
existingPrevious.next = this;
}
}
/**
* Add all children after 'node'. If 'node' is null, add them to the front of this node.
*
* @param children first of a list of sibling nodes who have no parent. NOTE: Usually you would
* get this argument from a removeChildren() call. A single detached node will not work
* because its sibling pointers will not be correctly initialized.
*/
public final void addChildrenAfter(@Nullable Node children, @Nullable Node node) {
if (children == null) {
return; // removeChildren() returns null when there are none
}
checkArgument(node == null || node.parent == this);
// NOTE: If there is only one sibling, its previous pointer must point to itself.
// Null indicates a fully detached node.
checkNotNull(children.previous, children);
if (node == null) {
addChildrenToFront(children);
return;
}
for (Node child = children; child != null; child = child.next) {
checkArgument(child.parent == null);
child.parent = this;
}
Node lastSibling = children.previous;
Node nodeAfter = node.next;
lastSibling.next = nodeAfter;
if (nodeAfter == null) {
first.previous = lastSibling;
} else {
nodeAfter.previous = lastSibling;
}
node.next = children;
children.previous = node;
}
/** Swaps `replacement` and its subtree into the position of `this`. */
public final void replaceWith(Node replacement) {
this.checkAttached();
replacement.checkDetached();
final Node existingParent = this.parent;
final Node existingNext = this.next;
final Node existingPrevious = this.previous;
// Copy over important information.
// TODO(nickreid): Stop doing this. It's totally unexpected.
replacement.srcrefIfMissing(this);
// The sequence below also has to work when `this` is an only child, ehich can cause many of the
// variables to point to the same object.
this.parent = null;
replacement.parent = existingParent;
this.previous = null;
replacement.previous = existingPrevious;
if (existingPrevious.next == null) {
existingParent.first = replacement;
// existingPrevious.next remains null
} else {
// existingParent.first is unchanged;
existingPrevious.next = replacement;
}
if (existingNext == null) {
// this.next remains null;
existingParent.first.previous = replacement;
// replacement.next remains null
} else {
this.next = null;
existingNext.previous = replacement;
replacement.next = existingNext;
}
}
/** Removes this node from its parent, but retains its subtree. */
public final Node detach() {
this.checkAttached();
final Node existingParent = this.parent;
final Node existingNext = this.next;
final Node existingPrevious = this.previous;
// The sequence below also has to work when `this` is an only child or has a single sibling,
// which can cause many of the variables to point to the same object.
this.parent = null;
if (existingNext == null) {
// this.next remains null;
existingParent.first.previous = existingPrevious;
} else {
this.next = null;
existingNext.previous = existingPrevious;
}
this.previous = null;
if (existingPrevious.next == null) {
existingParent.first = existingNext;
// existingPrevious.next remains null
} else {
// existingParent.first is unchanged;
existingPrevious.next = existingNext;
}
return this;
}
private final void checkAttached() {
checkState(this.parent != null, "Has no parent: %s", this);
}
private final void checkDetached() {
checkState(this.parent == null, "Has parent: %s", this);
checkState(this.next == null, "Has next: %s", this);
checkState(this.previous == null, "Has previous: %s", this);
}
/**
* Removes the first child of Node. Equivalent to: node.removeChild(node.getFirstChild());
*
* @return The removed Node.
*/
@Nullable
public final Node removeFirstChild() {
Node child = first;
if (child != null) {
child.detach();
}
return child;
}
/** @return A Node that is the head of the list of children. */
@Nullable
public final Node removeChildren() {
Node children = first;
for (Node child = first; child != null; child = child.next) {
child.parent = null;
}
first = null;
return children;
}
/** Removes all children from this node and isolates the children from each other. */
public final void detachChildren() {
for (Node child = first; child != null; ) {
Node nextChild = child.next;
child.parent = null;
child.next = null;
child.previous = null;
child = nextChild;
}
first = null;
}
@VisibleForTesting
@Nullable
final PropListItem lookupProperty(Prop prop) {
byte propType = (byte) prop.ordinal();
PropListItem x = propListHead;
while (x != null && propType != x.propType) {
x = x.next;
}
return x;
}
/**
* Clone the properties from the provided node without copying the property object. The receiving
* node may not have any existing properties.
*
* @param other The node to clone properties from.
* @return this node.
*/
public final Node clonePropsFrom(Node other) {
checkState(this.propListHead == null, "Node has existing properties.");
this.propListHead = other.propListHead;
return this;
}
/**
* Checks for invalid or missing properties and feeds error messages for any violations to the
* given `Consumer`.
*
*
We use a `Consumer` to avoid the cost of building a usually-empty list every time this
* method is called.
*/
public void validateProperties(Consumer violationMessageConsumer) {
if (propListHead == null) {
// TODO(bradfordcsmith): Fix the bugs that prevent enabling this validation.
//
// In particular:
//
// 1. CoverageInstrumentationPass and BranchCoverageInstrumentationCallback
// create a bunch of nodes without valid source reference info, and it isn't obvious what
// source reference info they should be using.
//
// 2. b/186056977 covers an issue blocking correcting a violation in `VarCheck`
//
// 3. Fixing a violation in DeclaredGlobalExternsOnWindow makes an unexpected change to
// the pre-computed TypedAst for the runtime libraries.
//
// if (token != Token.ROOT) {
// violationMessageConsumer.accept("non-ROOT has no properties");
// }
return;
}
if (token == Token.ROOT) {
// ROOT tokens should never have properties
violationMessageConsumer.accept("ROOT has properties");
}
for (PropListItem propListItem = propListHead;
propListItem != null;
propListItem = propListItem.next) {
final Prop prop = PROP_VALUES[propListItem.propType];
// Catch it if the definition of Prop ever changes so that the ordinals don't line up.
checkState(prop.ordinal() == propListItem.propType, "ordinal doesn't match: %s", prop);
// TODO(bradfordcsmith): This is not yet an exhaustive list of validations.
// Other validations should be added as it is found useful to have them.
// Some property validation is done independently in `AstValidator` and could possibly be
// moved here.
//
// This method was added in response to a bug that created an invalid IS_PARENTHESIZED
// property that was discovered by a check in `deserializeProperties()`, so initially
// this method was created to cover the checks previously done there.
switch (prop) {
case IS_PARENTHESIZED:
if (!IR.mayBeExpression(this)) {
violationMessageConsumer.accept("non-expression is parenthesized");
}
break;
case ARROW_FN:
if (!isFunction()) {
violationMessageConsumer.accept("invalid ARROW_FN prop");
}
break;
case ASYNC_FN:
if (!isFunction()) {
violationMessageConsumer.accept("invalid ASYNC_FN prop");
}
break;
case SYNTHETIC:
if (!isBlock()) {
violationMessageConsumer.accept("invalid SYNTHETIC prop");
}
break;
case COLOR_FROM_CAST:
if (getColor() == null) {
violationMessageConsumer.accept("COLOR_FROM_CAST with no Color");
}
break;
case START_OF_OPT_CHAIN:
if (!(isOptChainCall() || isOptChainGetElem() || isOptChainGetProp())) {
violationMessageConsumer.accept("START_OF_OPT_CHAIN on non-optional Node");
}
break;
case CONSTANT_VAR_FLAGS:
if (!(isName() || isImportStar())) {
violationMessageConsumer.accept("invalid CONST_VAR_FLAGS");
}
break;
case SYNTHESIZED_UNFULFILLED_NAME_DECLARATION:
// note: we could relax this restriction if VarCheck needed to generate other forms of
// synthetic externs
if (!isVar() || !hasOneChild() || !getFirstChild().isName()) {
violationMessageConsumer.accept(
"Expected all synthetic unfulfilled declarations to be `var `");
}
break;
default:
// No validation is currently done for other properties
break;
}
}
}
/**
* @param item The item to inspect
* @param prop The property to look for
* @return The replacement list if the property was removed, or 'item' otherwise.
*/
@Nullable
private static PropListItem rebuildListWithoutProp(@Nullable PropListItem item, Prop prop) {
if (item == null) {
return null;
} else if (item.propType == prop.ordinal()) {
return item.next;
} else {
PropListItem result = rebuildListWithoutProp(item.next, prop);
return (result == item.next) ? item : item.chain(result);
}
}
@Nullable
public final Object getProp(Prop propType) {
PropListItem item = lookupProperty(propType);
if (item == null) {
return null;
}
return item.getObjectValue();
}
public final boolean getBooleanProp(Prop propType) {
return getIntProp(propType) != 0;
}
/** Returns the integer value for the property, or 0 if the property is not defined. */
private int getIntProp(Prop propType) {
PropListItem item = lookupProperty(propType);
if (item == null) {
return 0;
}
return item.getIntValue();
}
public final void putProp(Prop prop, @Nullable Object value) {
this.propListHead = rebuildListWithoutProp(this.propListHead, prop);
if (value != null) {
this.propListHead = new ObjectPropListItem((byte) prop.ordinal(), value, this.propListHead);
}
}
public final void putBooleanProp(Prop propType, boolean value) {
putIntProp(propType, value ? 1 : 0);
}
public final void putIntProp(Prop prop, int value) {
this.propListHead = rebuildListWithoutProp(this.propListHead, prop);
if (value != 0) {
this.propListHead = new IntPropListItem((byte) prop.ordinal(), value, this.propListHead);
}
}
public final EnumSet serializeProperties() {
EnumSet propSet = EnumSet.noneOf(NodeProperty.class);
for (PropListItem propListItem = this.propListHead;
propListItem != null;
propListItem = propListItem.next) {
Prop prop = PROP_VALUES[propListItem.propType];
switch (prop) {
case TYPE_BEFORE_CAST:
propSet.add(NodeProperty.COLOR_FROM_CAST);
break;
case CONSTANT_VAR_FLAGS:
int intVal = propListItem.getIntValue();
if (anyBitSet(intVal, ConstantVarFlags.INFERRED)) {
propSet.add(NodeProperty.IS_INFERRED_CONSTANT);
}
if (anyBitSet(intVal, ConstantVarFlags.DECLARED)) {
propSet.add(NodeProperty.IS_DECLARED_CONSTANT);
}
break;
default:
if (propListItem instanceof Node.IntPropListItem) {
NodeProperty nodeProperty = PropTranslator.serialize(prop);
if (nodeProperty != null) {
propSet.add(nodeProperty);
}
}
break;
}
}
return propSet;
}
public final void deserializeProperties(List serializedNodeBooleanPropertyList) {
if (this.isRoot()) {
checkState(this.propListHead == null, this.propListHead);
} else {
checkState(this.propListHead.propType == Prop.SOURCE_FILE.ordinal(), this.propListHead);
}
EnumSet propSet = EnumSet.noneOf(NodeProperty.class);
for (int i = 0; i < serializedNodeBooleanPropertyList.size(); i++) {
NodeProperty nodeProperty = serializedNodeBooleanPropertyList.get(i);
checkState(propSet.add(nodeProperty), "Found multiple node property: %s", nodeProperty);
}
if (propSet.contains(NodeProperty.IS_DECLARED_CONSTANT)
|| propSet.contains(NodeProperty.IS_INFERRED_CONSTANT)) {
int newConstantVarFlags =
(propSet.remove(NodeProperty.IS_DECLARED_CONSTANT) ? ConstantVarFlags.DECLARED : 0)
| (propSet.remove(NodeProperty.IS_INFERRED_CONSTANT) ? ConstantVarFlags.INFERRED : 0);
this.propListHead =
new IntPropListItem(
(byte) Prop.CONSTANT_VAR_FLAGS.ordinal(), newConstantVarFlags, this.propListHead);
}
for (NodeProperty nodeProperty : propSet) {
Prop prop = PropTranslator.deserialize(nodeProperty);
this.propListHead = new IntPropListItem((byte) prop.ordinal(), 1, this.propListHead);
}
// Make sure the deserialized properties are valid.
validateProperties(
// NOTE: errorMessage will never be null, but just passing `false` to `checkState()`
// triggers warning messages from some code analysis tools.
errorMessage ->
checkState(errorMessage != null, "deserialize error: %s: %s", errorMessage, this));
}
/** Sets the syntactical type specified on this node. */
public final void setDeclaredTypeExpression(Node typeExpression) {
putProp(Prop.DECLARED_TYPE_EXPR, typeExpression);
}
/**
* Returns the syntactical type specified on this node. Not to be confused with {@link
* #getJSType()} which returns the compiler-inferred type.
*/
@Nullable
public final Node getDeclaredTypeExpression() {
return (Node) getProp(Prop.DECLARED_TYPE_EXPR);
}
/** Sets the type of this node before casting. */
public final void setJSTypeBeforeCast(JSType type) {
putProp(Prop.TYPE_BEFORE_CAST, type);
}
/**
* Returns the type of this node before casting. This annotation will only exist on the first
* child of a CAST node after type checking.
*/
@Nullable
public final JSType getJSTypeBeforeCast() {
return (JSType) getProp(Prop.TYPE_BEFORE_CAST);
}
/**
* Indicate that this node's color comes from a type assertion. Only set when colors are present;
* when JSTypes are on the AST we instead preserve the actual JSType before the type assertion.
*/
public final void setColorFromTypeCast() {
checkState(getColor() != null, "Only use on nodes with colors present");
putBooleanProp(Prop.COLOR_FROM_CAST, true);
}
/**
* Indicates that this node's color comes from a type assertion. Only set when colors are present.
*/
public final boolean isColorFromTypeCast() {
return getBooleanProp(Prop.COLOR_FROM_CAST);
}
// Gets all the property types, in sorted order.
private byte[] getSortedPropTypes() {
int count = 0;
for (PropListItem x = propListHead; x != null; x = x.next) {
count++;
}
byte[] keys = new byte[count];
for (PropListItem x = propListHead; x != null; x = x.next) {
count--;
keys[count] = x.propType;
}
Arrays.sort(keys);
return keys;
}
public final double getDouble() {
return ((NumberNode) this).number;
}
public final void setDouble(double x) {
checkState(!Double.isNaN(x), x);
checkState(isPositive(x), x);
((NumberNode) this).number = x;
}
public final BigInteger getBigInt() {
return ((BigIntNode) this).bigint;
}
public final void setBigInt(BigInteger number) {
checkNotNull(number);
checkState(number.signum() >= 0, number);
((BigIntNode) this).bigint = number;
}
public final String getString() {
return ((StringNode) this).str;
}
public final void setString(String str) {
((StringNode) this).str = RhinoStringPool.addOrGet(str); // RhinoStringPool is null-hostile.
}
public final String getRawString() {
return ((TemplateLiteralSubstringNode) this).raw;
}
@Nullable
public final String getCookedString() {
return ((TemplateLiteralSubstringNode) this).cooked;
}
@Override
public final String toString() {
return toString(true, true, true);
}
public final String toString(boolean printSource, boolean printAnnotations, boolean printType) {
StringBuilder sb = new StringBuilder();
toString(sb, printSource, printAnnotations, printType);
return sb.toString();
}
private void toString(
StringBuilder sb, boolean printSource, boolean printAnnotations, boolean printType) {
sb.append(token);
if (this instanceof StringNode) {
sb.append(' ');
sb.append(getString());
} else if (token == Token.FUNCTION) {
sb.append(' ');
// In the case of JsDoc trees, the first child is often not a string
// which causes exceptions to be thrown when calling toString or
// toStringTree.
if (first == null || first.token != Token.NAME) {
sb.append("");
} else {
sb.append(first.getString());
}
} else if (token == Token.NUMBER) {
sb.append(' ');
sb.append(getDouble());
}
if (printSource) {
int lineno = getLineno();
if (lineno != -1) {
sb.append(' ');
sb.append(lineno);
sb.append(':');
sb.append(getCharno());
sb.append(' ');
}
if (length != 0) {
sb.append(" [length: ");
sb.append(length);
sb.append(']');
}
}
if (printAnnotations) {
byte[] keys = getSortedPropTypes();
for (int i = 0; i < keys.length; i++) {
Prop type = PROP_VALUES[keys[i]];
PropListItem x = lookupProperty(type);
sb.append(" [");
sb.append(Ascii.toLowerCase(String.valueOf(type)));
sb.append(": ");
sb.append(x);
sb.append(']');
}
if (this.originalName != null) {
sb.append(" [original_name: ");
sb.append(this.originalName);
sb.append(']');
}
}
if (printType && jstypeOrColor != null) {
String typeString = jstypeOrColor.toString();
if (typeString != null) {
sb.append(" : ");
sb.append(typeString);
}
}
}
@CheckReturnValue
public final String toStringTree() {
return toStringTreeImpl();
}
private String toStringTreeImpl() {
try {
StringBuilder s = new StringBuilder();
appendStringTree(s);
return s.toString();
} catch (IOException e) {
throw new RuntimeException("Should not happen\n" + e);
}
}
public final void appendStringTree(Appendable appendable) throws IOException {
toStringTreeHelper(this, 0, appendable);
}
private static void toStringTreeHelper(Node n, int level, Appendable sb) throws IOException {
for (int i = 0; i != level; ++i) {
sb.append(" ");
}
sb.append(n.toString());
sb.append('\n');
for (Node cursor = n.first; cursor != null; cursor = cursor.next) {
toStringTreeHelper(cursor, level + 1, sb);
}
}
private transient Token token; // Type of the token of the node; NAME for example
@Nullable private transient Node next; // next sibling, a linked list
@Nullable private transient Node previous; // previous sibling, a circular linked list
@Nullable private transient Node first; // first element of a linked list of children
@Nullable private transient Node parent;
// We get the last child as first.previous. But last.next is null, not first.
/**
* Source position of this node. The position is encoded with the column number in the low 12 bits
* of the integer, and the line number in the rest. Create some handy constants so we can change
* this size if we want.
*/
private transient int linenoCharno = -1;
/** The length of the code represented by the node. */
private transient int length;
@Nullable private transient Object jstypeOrColor;
@Nullable private transient String originalName;
/**
* Linked list of properties. Since vast majority of nodes would have no more than 2 properties,
* linked list saves memory and provides fast lookup. If this does not holds, propListHead can be
* replaced by UintMap.
*/
@Nullable private transient PropListItem propListHead;
// ==========================================================================
// Source position management
public final void setStaticSourceFileFrom(Node other) {
// Make sure source file prop nodes are not duplicated.
if (other.propListHead != null
&& (this.propListHead == null
|| (this.propListHead.propType == Prop.SOURCE_FILE.ordinal()
&& this.propListHead.next == null))) {
// Either the node has only Prop.SOURCE_FILE as a property or has not properties.
PropListItem tail = other.propListHead;
while (tail.next != null) {
tail = tail.next;
}
if (tail.propType == Prop.SOURCE_FILE.ordinal()) {
propListHead = tail;
return;
}
}
setStaticSourceFile(other.getStaticSourceFile());
}
public final Node setStaticSourceFile(@Nullable StaticSourceFile file) {
this.putProp(Prop.SOURCE_FILE, file);
return this;
}
/** Sets the source file to a non-extern file of the given name. */
public final void setSourceFileForTesting(String name) {
this.putProp(Prop.SOURCE_FILE, new SimpleSourceFile(name, SourceKind.STRONG));
}
// TODO(johnlenz): make this final
@Nullable
public String getSourceFileName() {
StaticSourceFile file = getStaticSourceFile();
return file == null ? null : file.getName();
}
/** Returns the source file associated with this input. */
@Nullable
public StaticSourceFile getStaticSourceFile() {
return ((StaticSourceFile) this.getProp(Prop.SOURCE_FILE));
}
/** @param inputId */
public void setInputId(InputId inputId) {
this.putProp(Prop.INPUT_ID, inputId);
}
/** @return The Id of the CompilerInput associated with this Node. */
@Nullable
public InputId getInputId() {
return ((InputId) this.getProp(Prop.INPUT_ID));
}
/**
* The original name of this node, if the node has been renamed.
*
* Do not use original name to make optimization decisions. The original intent was to preserve
* some naming for lightly optimized code to put into source maps. It is not rigorously defined
* and is not a suitable replacement for a canonical identifier. "Original name" is not associated
* with any scope and easily transfers to unrelated values. Its existance and use beyond its
* original purpose has delayed creating useful more precise alternatives.
*
* @deprecated "original name" is poorly defined.
*/
@Nullable
@Deprecated
public final String getOriginalName() {
return this.originalName;
}
public final void setOriginalName(String s) {
this.originalName = (s == null) ? null : RhinoStringPool.addOrGet(s);
}
/** Whether this node should be indexed by static analysis / code indexing tools. */
public final boolean isIndexable() {
return !this.getBooleanProp(Prop.NON_INDEXABLE);
}
public final void makeNonIndexable() {
this.putBooleanProp(Prop.NON_INDEXABLE, true);
}
public final void makeNonIndexableRecursive() {
this.makeNonIndexable();
for (Node child = this.first; child != null; child = child.getNext()) {
child.makeNonIndexableRecursive();
}
}
public final boolean isFromExterns() {
StaticSourceFile file = getStaticSourceFile();
return file == null ? false : file.isExtern();
}
public final int getLength() {
return this.length;
}
public final void setLength(int length) {
this.length = length;
}
public final int getLineno() {
if (this.linenoCharno == -1) {
return -1;
} else {
return this.linenoCharno >>> CHARNO_BITS;
}
}
// Returns the 0-based column number
public final int getCharno() {
if (this.linenoCharno == -1) {
return -1;
} else {
return this.linenoCharno & MAX_COLUMN_NUMBER;
}
}
public final String getLocation() {
return this.getSourceFileName() + ":" + this.getLineno() + ":" + this.getCharno();
}
// TODO(johnlenz): make this final
public int getSourceOffset() {
StaticSourceFile file = getStaticSourceFile();
if (file == null) {
return -1;
}
int lineno = getLineno();
if (lineno == -1) {
return -1;
}
return file.getLineOffset(lineno) + getCharno();
}
public final int getSourcePosition() {
return linenoCharno;
}
/**
* CHARNO_BITS represents how many of the lower-order bits of linenoCharno are reserved for
* storing the column number. Bits above these store the line number. This gives us decent
* position information for everything except files already passed through a minimizer, where
* lines might be longer than 4096 characters.
*/
private static final int CHARNO_BITS = 12;
/**
* MAX_COLUMN_NUMBER represents the maximum column number that can be represented. JSCompiler's
* modifications to Rhino cause all tokens located beyond the maximum column to MAX_COLUMN_NUMBER.
*/
public static final int MAX_COLUMN_NUMBER = (1 << CHARNO_BITS) - 1;
/**
* Merges the line number and character number in one integer.
*
*
The charno takes the first 12 bits and the line number takes the rest. If the charno is
* greater than (2^12)-1 it is adjusted to (2^12)-1
*/
public final Node setLinenoCharno(int lineno, int charno) {
if (lineno < 0 || charno < 0) {
this.linenoCharno = -1;
return this;
}
if (charno > MAX_COLUMN_NUMBER) {
charno = MAX_COLUMN_NUMBER;
}
this.linenoCharno = (lineno << CHARNO_BITS) | charno;
return this;
}
// ==========================================================================
// Iteration
/**
* Return an iterable object that iterates over this node's children. The iterator does not
* support the optional operation {@link Iterator#remove()}.
*
*
To iterate over a node's children, one can write
*
*
Node n = ...;
* for (Node child : n.children()) { ...
*
* NOTE: Do not use 'children' for recursive descent of the AST. The overhead of using iterators
* rather then getFirstChild()/getNext() is very significant. We have deprecated it as it is easy
* to misuse.
*/
@Deprecated
public final Iterable children() {
if (first == null) {
return Collections.emptySet();
} else {
return new SiblingNodeIterable(first);
}
}
/** @see Node#children() */
private static final class SiblingNodeIterable implements Iterable {
private final Node start;
SiblingNodeIterable(Node start) {
this.start = start;
}
@Override
public Iterator iterator() {
return new SiblingNodeIterator(start);
}
}
/** @see Node#children() */
private static final class SiblingNodeIterator implements Iterator {
@Nullable private Node current;
SiblingNodeIterator(Node start) {
this.current = start;
}
@Override
public boolean hasNext() {
return current != null;
}
@Override
public Node next() {
if (current == null) {
throw new NoSuchElementException();
}
Node n = current;
current = current.getNext();
return n;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
// ==========================================================================
// Accessors
@Nullable
final PropListItem getPropListHeadForTesting() {
return propListHead;
}
final void setPropListHead(@Nullable PropListItem propListHead) {
this.propListHead = propListHead;
}
@Nullable
public final Node getParent() {
return parent;
}
public final boolean hasParent() {
return parent != null;
}
@Nullable
public final Node getGrandparent() {
return parent == null ? null : parent.parent;
}
/**
* Gets the ancestor node relative to this.
*
* @param level 0 = this, 1 = the parent, etc.
*/
@Nullable
public final Node getAncestor(int level) {
checkArgument(level >= 0);
Node node = this;
while (node != null && level-- > 0) {
node = node.getParent();
}
return node;
}
/** @return True if this Node is {@code node} or a descendant of {@code node}. */
public final boolean isDescendantOf(Node node) {
for (Node n = this; n != null; n = n.parent) {
if (n == node) {
return true;
}
}
return false;
}
public final boolean isOnlyChildOf(Node possibleParent) {
return possibleParent == getParent() && getPrevious() == null && getNext() == null;
}
public final boolean isFirstChildOf(Node possibleParent) {
return possibleParent == getParent() && getPrevious() == null;
}
public final boolean isSecondChildOf(Node possibleParent) {
Node previousNode = getPrevious();
return previousNode != null && previousNode.isFirstChildOf(possibleParent);
}
/** Iterates all of the node's ancestors excluding itself. */
public final AncestorIterable getAncestors() {
return new AncestorIterable(this.getParent());
}
/** Iterator to go up the ancestor tree. */
public static final class AncestorIterable implements Iterable {
@Nullable private Node cur;
/** @param cur The node to start. */
AncestorIterable(@Nullable Node cur) {
this.cur = cur;
}
@Override
public Iterator iterator() {
return new Iterator() {
@Override
public boolean hasNext() {
return cur != null;
}
@Override
public Node next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
Node n = cur;
cur = cur.getParent();
return n;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}
/**
* Check for one child more efficiently than by iterating over all the children as is done with
* Node.getChildCount().
*
* @return Whether the node has exactly one child.
*/
public final boolean hasOneChild() {
return first != null && first.next == null;
}
/**
* Check for two children more efficiently than {@code getChildCount() == 2}
*
* @return Whether the node has exactly two children.
*/
public final boolean hasTwoChildren() {
return first != null && first.next != null && first.next == getLastChild();
}
/**
* Check for zero or one child more efficiently than by iterating over all the children as is done
* with Node.getChildCount().
*
* @return Whether the node has no children or exactly one child.
*/
public final boolean hasZeroOrOneChild() {
return first == getLastChild();
}
/**
* Check for more than one child more efficiently than by iterating over all the children as is
* done with Node.getChildCount().
*
* @return Whether the node more than one child.
*/
public final boolean hasMoreThanOneChild() {
return first != null && first.next != null;
}
/**
* Check for has exactly the number of specified children.
*
* @return Whether the node has exactly the number of children specified.
*/
public final boolean hasXChildren(int x) {
int c = 0;
for (Node n = first; n != null && c <= x; n = n.next) {
c++;
}
return c == x;
}
public final int getChildCount() {
int c = 0;
for (Node n = first; n != null; n = n.next) {
c++;
}
return c;
}
// Intended for testing and verification only.
public final boolean hasChild(Node child) {
for (Node n = first; n != null; n = n.next) {
if (child == n) {
return true;
}
}
return false;
}
/** Checks equivalence without going into child nodes */
public final boolean isEquivalentToShallow(Node node) {
return isEquivalentTo(node, false, false, false, false);
}
/** Returns true if this node is equivalent semantically to another including side effects. */
public final boolean isEquivalentWithSideEffectsTo(Node node) {
return isEquivalentTo(node, false, true, false, true);
}
/** Returns true if this node is equivalent semantically to another including side effects. */
public final boolean isEquivalentWithSideEffectsToShallow(Node node) {
return isEquivalentTo(node, false, false, false, true);
}
/**
* Returns true if this node is equivalent semantically to another and the types are equivalent.
*/
public final boolean isEquivalentToTyped(Node node) {
return isEquivalentTo(node, true, true, true, false);
}
/** Returns true if this node is equivalent semantically to another */
public final boolean isEquivalentTo(Node node) {
return isEquivalentTo(node, false, true, false, false);
}
/**
* @param compareType Whether to compare the JSTypes of the nodes.
* @param recurse Whether to compare the children of the current node. If not, only the count of
* the children are compared.
* @param jsDoc Whether to check that the JsDoc of the nodes are equivalent.
* @return Whether this node is equivalent semantically to the provided node.
*/
final boolean isEquivalentTo(Node node, boolean compareType, boolean recurse, boolean jsDoc) {
return isEquivalentTo(node, compareType, recurse, jsDoc, false);
}
/**
* @param compareType Whether to compare the JSTypes of the nodes.
* @param recurse Whether to compare the children of the current node. If not, only the count of
* the children are compared.
* @param jsDoc Whether to check that the JsDoc of the nodes are equivalent.
* @param sideEffect Whether to check that the side-effect flags of the nodes are equivalent.
* @return Whether this node is equivalent semantically to the provided node.
*/
public boolean isEquivalentTo(
Node node, boolean compareType, boolean recurse, boolean jsDoc, boolean sideEffect) {
if (token != node.token
|| getChildCount() != node.getChildCount()
|| this.getClass() != node.getClass()) {
return false;
}
if (compareType && !Objects.equals(this.jstypeOrColor, node.jstypeOrColor)) {
return false;
}
if (jsDoc && !JSDocInfo.areEquivalent(getJSDocInfo(), node.getJSDocInfo())) {
return false;
}
Node thisDte = this.getDeclaredTypeExpression();
Node thatDte = node.getDeclaredTypeExpression();
if (thisDte == thatDte) {
// Do nothing
} else if (thisDte == null || thatDte == null) {
return false;
} else if (!thisDte.isEquivalentTo(thatDte, compareType, recurse, jsDoc)) {
return false;
}
EnumSet propSet = EnumSet.noneOf(Prop.class);
for (PropListItem propListItem = this.propListHead;
propListItem != null;
propListItem = propListItem.next) {
Prop prop = PROP_VALUES[propListItem.propType];
propSet.add(prop);
}
for (PropListItem propListItem = node.propListHead;
propListItem != null;
propListItem = propListItem.next) {
Prop prop = PROP_VALUES[propListItem.propType];
propSet.add(prop);
}
for (Prop prop : propSet) {
if (PROP_MAP_FOR_EQUALITY_KEYS.contains(prop)) {
Function getter = PROP_MAP_FOR_EQUALITY.get(prop);
if (!Objects.equals(getter.apply(this), getter.apply(node))) {
return false;
}
}
}
if (sideEffect) {
if (this.getSideEffectFlags() != node.getSideEffectFlags()) {
return false;
}
if (this.isUnusedParameter() != node.isUnusedParameter()) {
return false;
}
}
if (recurse) {
for (Node n = first, n2 = node.first; n != null; n = n.next, n2 = n2.next) {
if (!n.isEquivalentTo(n2, compareType, recurse, jsDoc, sideEffect)) {
return false;
}
}
}
return true;
}
/**
* Accessors for {@link Node} properties that should also be compared when comparing nodes for
* equality.
*
* We'd prefer to list the props that should be ignored rather than the ones that should be
* checked, but that was too difficult initially. In general these properties are ones that show
* up as keywords / symbols which don't have their own nodes.
*
*
Accessor functions are used rather than {@link Prop}s to encode the correct way of reading
* the prop.
*/
private static final ImmutableMap> PROP_MAP_FOR_EQUALITY =
new ImmutableMap.Builder>()
.put(Prop.ARROW_FN, Node::isArrowFunction)
.put(Prop.ASYNC_FN, Node::isAsyncFunction)
.put(Prop.GENERATOR_FN, Node::isGeneratorFunction)
.put(Prop.START_OF_OPT_CHAIN, Node::isOptionalChainStart)
.put(Prop.STATIC_MEMBER, Node::isStaticMember)
.put(Prop.YIELD_ALL, Node::isYieldAll)
.put(Prop.EXPORT_DEFAULT, (n) -> n.getIntProp(Prop.EXPORT_DEFAULT))
.put(Prop.EXPORT_ALL_FROM, (n) -> n.getIntProp(Prop.EXPORT_ALL_FROM))
.put(Prop.INCRDECR, (n) -> n.getIntProp(Prop.INCRDECR))
.put(Prop.QUOTED, (n) -> n.getIntProp(Prop.QUOTED))
.put(Prop.FREE_CALL, (n) -> n.getBooleanProp(Prop.FREE_CALL))
.put(Prop.COMPUTED_PROP_METHOD, (n) -> n.getBooleanProp(Prop.COMPUTED_PROP_METHOD))
.put(Prop.COMPUTED_PROP_GETTER, (n) -> n.getBooleanProp(Prop.COMPUTED_PROP_GETTER))
.put(Prop.COMPUTED_PROP_SETTER, (n) -> n.getBooleanProp(Prop.COMPUTED_PROP_SETTER))
.buildOrThrow();
/** Used for faster Map.containsKey() lookups in PROP_MAP_FOR_EQUALITY */
private static final EnumSet PROP_MAP_FOR_EQUALITY_KEYS =
EnumSet.copyOf(PROP_MAP_FOR_EQUALITY.keySet());
/**
* This function takes a set of GETPROP nodes and produces a string that is each property
* separated by dots. If the node ultimately under the left sub-tree is not a simple name, this is
* not a valid qualified name.
*
* @return a null if this is not a qualified name, or a dot-separated string of the name and
* properties.
*/
@Nullable
public final String getQualifiedName() {
switch (token) {
case NAME:
String name = getString();
return name.isEmpty() ? null : name;
case GETPROP:
StringBuilder builder = getQualifiedNameForGetProp(0);
return builder != null ? builder.toString() : null;
case THIS:
return "this";
case SUPER:
return "super";
default:
return null;
}
}
@Nullable
public final QualifiedName getQualifiedNameObject() {
return isQualifiedName() ? new QualifiedName.NodeQname(this) : null;
}
/**
* Helper method for {@link #getQualifiedName} to handle GETPROP nodes.
*
* @param reserve The number of characters of space to reserve in the StringBuilder
* @return {@code null} if this is not a qualified name or a StringBuilder if it is a complex
* qualified name.
*/
@Nullable
private StringBuilder getQualifiedNameForGetProp(int reserve) {
String propName = this.getString();
reserve += 1 + propName.length(); // +1 for the '.'
StringBuilder builder;
if (first.isGetProp()) {
builder = first.getQualifiedNameForGetProp(reserve);
if (builder == null) {
return null;
}
} else {
String left = first.getQualifiedName();
if (left == null) {
return null;
}
builder = new StringBuilder(left.length() + reserve);
builder.append(left);
}
builder.append('.').append(propName);
return builder;
}
/**
* This function takes a set of GETPROP nodes and produces a string that is each property
* separated by dots. If the node ultimately under the left sub-tree is not a simple name, this is
* not a valid qualified name. This method returns the original name of each segment rather than
* the renamed version.
*
* @return a null if this is not a qualified name, or a dot-separated string of the name and
* properties.
* @deprecated "original name" is poorly defined. See #getOriginalName
*/
@Nullable
@Deprecated
public final String getOriginalQualifiedName() {
if (token == Token.NAME) {
String name = getOriginalName();
if (name == null) {
name = getString();
}
return name.isEmpty() ? null : name;
} else if (token == Token.GETPROP) {
String left = getFirstChild().getOriginalQualifiedName();
if (left == null) {
return null;
}
String right = this.getOriginalName();
if (right == null) {
right = this.getString();
}
return left + "." + right;
} else if (token == Token.THIS) {
return "this";
} else if (token == Token.SUPER) {
return "super";
} else {
return null;
}
}
/**
* Returns whether a node corresponds to a simple or a qualified name, such as x
or
* a.b.c
or this.a
.
*/
public final boolean isQualifiedName() {
switch (this.getToken()) {
case NAME:
return !getString().isEmpty();
case THIS:
case SUPER:
return true;
case GETPROP:
return getFirstChild().isQualifiedName();
case MEMBER_FUNCTION_DEF:
// These are explicitly *not* qualified name components.
default:
return false;
}
}
/**
* Returns whether a node matches a simple name, such as x
, returns false if this is
* not a NAME node.
*/
public final boolean matchesName(String name) {
if (token != Token.NAME) {
return false;
}
String internalString = getString();
return !internalString.isEmpty() && name.equals(internalString);
}
/**
* Check that if two NAME node match, returns false if either node is not a NAME node. As a empty
* string is not considered a valid Name (it is an AST placeholder), empty strings are never
* considered to be matches.
*/
public final boolean matchesName(Node n) {
if (token != Token.NAME || n.token != Token.NAME) {
return false;
}
// ==, rather than equal as it is interned in setString
String internalString = getString();
return !internalString.isEmpty()
&& RhinoStringPool.uncheckedEquals(internalString, n.getString());
}
/**
* Returns whether a node matches a simple or a qualified name, such as x
or
* a.b.c
or this.a
.
*/
public final boolean matchesQualifiedName(String name) {
return matchesQualifiedName(name, name.length());
}
/**
* Returns whether a node matches a simple or a qualified name, such as x
or
* a.b.c
or this.a
.
*/
private boolean matchesQualifiedName(String qname, int endIndex) {
int start = qname.lastIndexOf('.', endIndex - 1) + 1;
switch (this.getToken()) {
case NAME:
case IMPORT_STAR:
String name = getString();
return start == 0 && !name.isEmpty() && name.length() == endIndex && qname.startsWith(name);
case THIS:
return start == 0 && 4 == endIndex && qname.startsWith("this");
case SUPER:
return start == 0 && 5 == endIndex && qname.startsWith("super");
case GETPROP:
String prop = this.getString();
return start > 1
&& prop.length() == endIndex - start
&& prop.regionMatches(0, qname, start, endIndex - start)
&& getFirstChild().matchesQualifiedName(qname, start - 1);
case MEMBER_FUNCTION_DEF:
// These are explicitly *not* qualified name components.
default:
return false;
}
}
/**
* Returns whether a node matches a simple or a qualified name, such as x
or
* a.b.c
or this.a
.
*/
public final boolean matchesQualifiedName(Node n) {
if (n.token != token) {
return false;
}
switch (token) {
case NAME:
return this.matchesName(n);
case THIS:
case SUPER:
return true;
case GETPROP:
return RhinoStringPool.uncheckedEquals(this.getString(), n.getString())
&& getFirstChild().matchesQualifiedName(n.getFirstChild());
case MEMBER_FUNCTION_DEF:
// These are explicitly *not* qualified name components.
default:
return false;
}
}
/**
* Returns whether a node corresponds to a simple or a qualified name without a "this" reference,
* such as a.b.c
, but not this.a
.
*/
public final boolean isUnscopedQualifiedName() {
switch (this.getToken()) {
case NAME:
return !getString().isEmpty();
case GETPROP:
return getFirstChild().isUnscopedQualifiedName();
default:
return false;
}
}
public final boolean isValidAssignmentTarget() {
switch (this.getToken()) {
case NAME:
case GETPROP:
case GETELEM:
case ARRAY_PATTERN:
case OBJECT_PATTERN:
return true;
default:
return false;
}
}
@DoNotCall
@GwtIncompatible
@Override
public final Object clone() {
throw new UnsupportedOperationException("Did you mean cloneNode?");
}
/** @return A detached clone of the Node, specifically excluding its children. */
@CheckReturnValue
public final Node cloneNode() {
return cloneNode(false);
}
/** @return A detached clone of the Node, specifically excluding its children. */
@CheckReturnValue
Node cloneNode(boolean cloneTypeExprs) {
Node clone = new Node(token);
copyBaseNodeFields(this, clone, cloneTypeExprs);
return clone;
}
private static void copyBaseNodeFields(Node source, Node dest, boolean cloneTypeExprs) {
dest.linenoCharno = source.linenoCharno;
dest.length = source.length;
dest.jstypeOrColor = source.jstypeOrColor;
dest.originalName = source.originalName;
dest.propListHead = source.propListHead;
// TODO(johnlenz): Remove this once JSTypeExpression are immutable
if (cloneTypeExprs) {
JSDocInfo info = source.getJSDocInfo();
if (info != null) {
dest.setJSDocInfo(info.clone(true));
}
}
}
/** @return A detached clone of the Node and all its children. */
@CheckReturnValue
public final Node cloneTree() {
return cloneTree(false);
}
@CheckReturnValue
public final Node cloneTree(boolean cloneTypeExprs) {
Node result = cloneNode(cloneTypeExprs);
Node firstChild = null;
Node lastChild = null;
if (this.hasChildren()) {
for (Node n2 = getFirstChild(); n2 != null; n2 = n2.next) {
Node n2clone = n2.cloneTree(cloneTypeExprs);
n2clone.parent = result;
if (firstChild == null) {
firstChild = n2clone;
lastChild = firstChild;
} else {
lastChild.next = n2clone;
n2clone.previous = lastChild;
lastChild = n2clone;
}
}
firstChild.previous = lastChild;
lastChild.next = null;
result.first = firstChild;
}
return result;
}
/** Copy the source info from `other` onto `this`. */
public final Node srcref(Node other) {
setStaticSourceFileFrom(other);
this.originalName = other.originalName;
linenoCharno = other.linenoCharno;
length = other.length;
return this;
}
/** For all Nodes in the subtree of `this`, copy the source info from `other`. */
public final Node srcrefTree(Node other) {
this.srcref(other);
for (Node child = first; child != null; child = child.next) {
child.srcrefTree(other);
}
return this;
}
/** Iff source info is not set on `this`, copy the source info from `other`. */
public final Node srcrefIfMissing(Node other) {
if (getStaticSourceFile() == null) {
setStaticSourceFileFrom(other);
linenoCharno = other.linenoCharno;
length = other.length;
}
// TODO(lharker): should this be inside the above if condition?
// If the node already has a source file, it seems strange to
// go ahead and set the original name anyway.
if (this.originalName == null) {
this.originalName = other.originalName;
}
return this;
}
/**
* For all Nodes in the subtree of `this`, iff source info is not set, copy the source info from
* `other`.
*/
public final Node srcrefTreeIfMissing(Node other) {
this.srcrefIfMissing(other);
for (Node child = first; child != null; child = child.next) {
child.srcrefTreeIfMissing(other);
}
return this;
}
// ==========================================================================
// Custom annotations
/**
* Returns the compiler inferred type on this node. Not to be confused with {@link
* #getDeclaredTypeExpression()} which returns the syntactically specified type.
*/
@Nullable
public final JSType getJSType() {
return (this.jstypeOrColor instanceof JSType) ? (JSType) this.jstypeOrColor : null;
}
/** Returns the compiled inferred type on this node, or throws an NPE if there isn't one. */
public final JSType getJSTypeRequired() {
return checkNotNull(this.getJSType(), "no jstypeOrColor: %s", this);
}
public final Node setJSType(@Nullable JSType x) {
checkState(this.jstypeOrColor == null || this.jstypeOrColor instanceof JSType, this);
this.jstypeOrColor = x;
return this;
}
/**
* Returns the compiled inferred type on this node. Not to be confused with {@link
* #getDeclaredTypeExpression()} which returns the syntactically specified type.
*/
@Nullable
public final Color getColor() {
return (this.jstypeOrColor instanceof Color) ? (Color) this.jstypeOrColor : null;
}
public final Node setColor(@Nullable Color x) {
checkState(this.jstypeOrColor == null || this.jstypeOrColor instanceof Color, this);
this.jstypeOrColor = x;
return this;
}
/** Copies a nodes JSType or Color (if present) */
public final Node copyTypeFrom(Node other) {
this.jstypeOrColor = other.jstypeOrColor;
return this;
}
/**
* Get the {@link JSDocInfo} attached to this node.
*
* @return the information or {@code null} if no JSDoc is attached to this node
*/
@Nullable
public final JSDocInfo getJSDocInfo() {
return (JSDocInfo) getProp(Prop.JSDOC_INFO);
}
/** Sets the {@link JSDocInfo} attached to this node. */
public final Node setJSDocInfo(JSDocInfo info) {
putProp(Prop.JSDOC_INFO, info);
return this;
}
/** This node was last changed at {@code time} */
public final void setChangeTime(int time) {
putIntProp(Prop.CHANGE_TIME, time);
}
/** Returns the time of the last change for this node */
public final int getChangeTime() {
return getIntProp(Prop.CHANGE_TIME);
}
public final void setDeleted(boolean deleted) {
putBooleanProp(Prop.DELETED, deleted);
}
public final boolean isDeleted() {
return getBooleanProp(Prop.DELETED);
}
/** If this node represents a typedef declaration, the associated JSType */
public final void setTypedefTypeProp(JSType type) {
putProp(Prop.TYPEDEF_TYPE, type);
}
/** If this node represents a typedef declaration, the associated JSType */
public final JSType getTypedefTypeProp() {
return (JSType) getProp(Prop.TYPEDEF_TYPE);
}
/** @param unused Whether a parameter was function to be unused. Set by RemoveUnusedVars */
public final void setUnusedParameter(boolean unused) {
putBooleanProp(Prop.IS_UNUSED_PARAMETER, unused);
}
/** @return Whether a parameter was function to be unused. Set by RemoveUnusedVars */
public final boolean isUnusedParameter() {
return getBooleanProp(Prop.IS_UNUSED_PARAMETER);
}
/** Sets the isShorthandProperty annotation. */
public final void setShorthandProperty(boolean shorthand) {
putBooleanProp(Prop.IS_SHORTHAND_PROPERTY, shorthand);
}
/** Whether this {x:x} property was originally parsed as {x}. */
public final boolean isShorthandProperty() {
return getBooleanProp(Prop.IS_SHORTHAND_PROPERTY);
}
/** Returns whether this node is an optional node in the ES6 Typed syntax. */
public final boolean isOptionalEs6Typed() {
return getBooleanProp(Prop.OPT_ES6_TYPED);
}
/** Sets whether this is a synthetic block that should not be considered a real source block. */
public final void setIsSyntheticBlock(boolean val) {
checkState(token == Token.BLOCK);
putBooleanProp(Prop.SYNTHETIC, val);
}
/**
* Returns whether this is a synthetic block that should not be considered a real source block.
*/
public final boolean isSyntheticBlock() {
return getBooleanProp(Prop.SYNTHETIC);
}
public final void setIsSynthesizedUnfulfilledNameDeclaration(boolean val) {
checkState(
token == Token.VAR && hasOneChild() && getFirstChild().isName(),
// we could relax this restriction if VarCheck wanted to generate other forms of synthetic
// externs
"Expected all synthetic unfulfilled declarations to be `var `, found %s",
this);
putBooleanProp(Prop.SYNTHESIZED_UNFULFILLED_NAME_DECLARATION, val);
}
public final boolean isSynthesizedUnfulfilledNameDeclaration() {
return getBooleanProp(Prop.SYNTHESIZED_UNFULFILLED_NAME_DECLARATION);
}
/** Sets whether this node contained the "use strict" directive. */
public final void setUseStrict(boolean x) {
this.putBooleanProp(Prop.USE_STRICT, x);
}
/** Returns whether this node contained the "use strict" directive. */
public final boolean isUseStrict() {
return this.getBooleanProp(Prop.USE_STRICT);
}
/**
* Sets whether this is an added block that should not be considered a real source block. Eg: In
* "if (true) x;", the "x;" is put under an added block in the AST.
*/
public final void setIsAddedBlock(boolean val) {
putBooleanProp(Prop.ADDED_BLOCK, val);
}
/** Returns whether this is an added block that should not be considered a real source block. */
public final boolean isAddedBlock() {
return getBooleanProp(Prop.ADDED_BLOCK);
}
/**
* Sets whether this node is a static member node. This method is meaningful only on {@link
* Token#GETTER_DEF}, {@link Token#SETTER_DEF} or {@link Token#MEMBER_FUNCTION_DEF} nodes
* contained within {@link Token#CLASS}.
*/
public final void setStaticMember(boolean isStatic) {
putBooleanProp(Prop.STATIC_MEMBER, isStatic);
}
/**
* Returns whether this node is a static member node. This method is meaningful only on {@link
* Token#GETTER_DEF}, {@link Token#SETTER_DEF} or {@link Token#MEMBER_FUNCTION_DEF} nodes
* contained within {@link Token#CLASS}.
*/
public final boolean isStaticMember() {
return getBooleanProp(Prop.STATIC_MEMBER);
}
/**
* Sets whether this node is a generator node. This method is meaningful only on {@link
* Token#FUNCTION} or {@link Token#MEMBER_FUNCTION_DEF} nodes.
*/
public final void setIsGeneratorFunction(boolean isGenerator) {
putBooleanProp(Prop.GENERATOR_FN, isGenerator);
}
/** Returns whether this node is a generator function node. */
public final boolean isGeneratorFunction() {
return getBooleanProp(Prop.GENERATOR_FN);
}
/**
* Sets whether this node subtree contains YIELD nodes.
*
* It's used in the translation of generators.
*/
public final void setGeneratorMarker(boolean isGeneratorMarker) {
putBooleanProp(Prop.IS_GENERATOR_MARKER, isGeneratorMarker);
}
/**
* Returns whether this node was marked as containing YIELD nodes.
*
*
It's used in the translation of generators.
*/
public final boolean isGeneratorMarker() {
return getBooleanProp(Prop.IS_GENERATOR_MARKER);
}
/** @see #isGeneratorSafe() */
public final void setGeneratorSafe(boolean isGeneratorSafe) {
putBooleanProp(Prop.IS_GENERATOR_SAFE, isGeneratorSafe);
}
/**
* Used when translating ES6 generators. If this returns true, this Node was generated by the
* compiler, and it is safe to copy this node to the transpiled output with no further changes.
*/
public final boolean isGeneratorSafe() {
return getBooleanProp(Prop.IS_GENERATOR_SAFE);
}
/**
* Sets whether this node is the start of an optional chain. This method is meaningful only on
* {@link Token#OPTCHAIN_GETELEM}, {@link Token#OPTCHAIN_GETPROP}, {@link Token#OPTCHAIN_CALL}
*/
public final void setIsOptionalChainStart(boolean isOptionalChainStart) {
checkState(
!isOptionalChainStart || isOptChainGetElem() || isOptChainGetProp() || isOptChainCall(),
"cannot make a non-optional node the start of an optional chain.");
putBooleanProp(Prop.START_OF_OPT_CHAIN, isOptionalChainStart);
}
/** Returns whether this node is an optional chaining node. */
public final boolean isOptionalChainStart() {
return getBooleanProp(Prop.START_OF_OPT_CHAIN);
}
/**
* Sets whether this node is a arrow function node. This method is meaningful only on {@link
* Token#FUNCTION}
*/
public final void setIsArrowFunction(boolean isArrow) {
checkState(isFunction());
putBooleanProp(Prop.ARROW_FN, isArrow);
}
/** Returns whether this node is a arrow function node. */
public final boolean isArrowFunction() {
return isFunction() && getBooleanProp(Prop.ARROW_FN);
}
/**
* Sets whether this node is an async function node. This method is meaningful only on {@link
* Token#FUNCTION}
*/
public void setIsAsyncFunction(boolean isAsync) {
checkState(isFunction());
putBooleanProp(Prop.ASYNC_FN, isAsync);
}
/** Returns whether this is an async function node. */
public final boolean isAsyncFunction() {
return isFunction() && getBooleanProp(Prop.ASYNC_FN);
}
/** Returns whether this is an async generator function node. */
public final boolean isAsyncGeneratorFunction() {
return isAsyncFunction() && isGeneratorFunction();
}
/**
* Sets whether this node is a generator node. This method is meaningful only on {@link
* Token#FUNCTION} or {@link Token#MEMBER_FUNCTION_DEF} nodes.
*/
public final void setYieldAll(boolean isGenerator) {
putBooleanProp(Prop.YIELD_ALL, isGenerator);
}
/**
* Returns whether this node is a generator node. This method is meaningful only on {@link
* Token#FUNCTION} or {@link Token#MEMBER_FUNCTION_DEF} nodes.
*/
public final boolean isYieldAll() {
return getBooleanProp(Prop.YIELD_ALL);
}
/** Indicates that there was a trailing comma in this list */
public final void setTrailingComma(boolean hasTrailingComma) {
putBooleanProp(Prop.TRAILING_COMMA, hasTrailingComma);
}
/** Returns true if there was a trailing comma in the orginal code */
public final boolean hasTrailingComma() {
return getBooleanProp(Prop.TRAILING_COMMA);
}
/**
* Marks this function or constructor call's side effect flags. This property is only meaningful
* for {@link Token#CALL} and {@link Token#NEW} nodes.
*/
public final void setSideEffectFlags(int flags) {
checkState(
this.isCall() || this.isOptChainCall() || this.isNew() || this.isTaggedTemplateLit(),
"Side-effect flags can only be set on invocation nodes; got %s",
this);
// We invert the flags before setting because we also invert the them when getting; they go
// full circle.
//
// We apply the mask after inversion so that if all flags are being set (all 1s), it's
// equivalent to storing a 0, the default value of an int-prop, which has no memory cost.
putIntProp(Prop.SIDE_EFFECT_FLAGS, ~flags & SideEffectFlags.USED_BITS_MASK);
}
public final void setSideEffectFlags(SideEffectFlags flags) {
setSideEffectFlags(flags.valueOf());
}
/** Returns the side effects flags for this node. */
public final int getSideEffectFlags() {
// Int props default to 0, but we want the default for side-effect flags to be all 1s.
// Therefore, we invert the value returned here. This is correct for non-defaults because we
// also invert when setting the flags.
return ~getIntProp(Prop.SIDE_EFFECT_FLAGS) & SideEffectFlags.USED_BITS_MASK;
}
/**
* A helper class for getting and setting invocation side-effect flags.
*
*
The following values are of interest:
*
*
* - Is global state mutated? ({@code MUTATES_GLOBAL_STATE})
*
- Is the receiver (`this`) mutated? ({@code MUTATES_THIS})
*
- Are any arguments mutated? ({@code MUTATES_ARGUMENTS})
*
- Does the call throw an error? ({@code THROWS})
*
*
* @author [email protected] (John Lenz)
*/
public static final class SideEffectFlags {
public static final int MUTATES_GLOBAL_STATE = 1;
public static final int MUTATES_THIS = 2;
public static final int MUTATES_ARGUMENTS = 4;
public static final int THROWS = 8;
private static final int USED_BITS_MASK = (1 << 4) - 1;
public static final int NO_SIDE_EFFECTS = 0;
public static final int ALL_SIDE_EFFECTS =
MUTATES_GLOBAL_STATE | MUTATES_THIS | MUTATES_ARGUMENTS | THROWS;
// A bitfield indicating the flag statuses. All used bits set to 1 means "global state changes".
// A value of 0 means "no side effects"
private int value = ALL_SIDE_EFFECTS;
public SideEffectFlags() {}
public SideEffectFlags(int value) {
this.value = value;
}
public int valueOf() {
return value;
}
/** All side-effect occur and the returned results are non-local. */
public SideEffectFlags setAllFlags() {
value = ALL_SIDE_EFFECTS;
return this;
}
/** No side-effects occur */
public SideEffectFlags clearAllFlags() {
value = NO_SIDE_EFFECTS;
return this;
}
public SideEffectFlags setMutatesGlobalState() {
// Modify global means everything must be assumed to be modified.
value |= MUTATES_GLOBAL_STATE | MUTATES_ARGUMENTS | MUTATES_THIS;
return this;
}
public SideEffectFlags setThrows() {
value |= THROWS;
return this;
}
public SideEffectFlags setMutatesThis() {
value |= MUTATES_THIS;
return this;
}
public SideEffectFlags setMutatesArguments() {
value |= MUTATES_ARGUMENTS;
return this;
}
@Override
@DoNotCall // For debugging only.
public String toString() {
StringBuilder builder = new StringBuilder("Side effects: ");
if ((value & MUTATES_THIS) != 0) {
builder.append("this ");
}
if ((value & MUTATES_GLOBAL_STATE) != 0) {
builder.append("global ");
}
if ((value & THROWS) != 0) {
builder.append("throw ");
}
if ((value & MUTATES_ARGUMENTS) != 0) {
builder.append("args ");
}
return builder.toString();
}
}
/** Returns whether the only side-effect is "modifies this" or there are no side effects. */
public final boolean isOnlyModifiesThisCall() {
// TODO(nickreid): Delete this; check if MUTATES_THIS is actually set. This was left in to
// maintain existing behaviour but it makes the name of this method misleading.
int sideEffectsBesidesMutatesThis = getSideEffectFlags() & ~SideEffectFlags.MUTATES_THIS;
return sideEffectsBesidesMutatesThis == SideEffectFlags.NO_SIDE_EFFECTS;
}
/** Returns whether the only side-effect is "modifies arguments" or there are no side effects. */
public final boolean isOnlyModifiesArgumentsCall() {
// TODO(nickreid): Delete this; check if MUTATES_ARGUMENTS is actually set. This was left in to
// maintain existing behaviour but it makes the name of this method misleading.
int sideEffectsBesidesMutatesArguments =
getSideEffectFlags() & ~SideEffectFlags.MUTATES_ARGUMENTS;
return sideEffectsBesidesMutatesArguments == SideEffectFlags.NO_SIDE_EFFECTS;
}
/** Returns true if this node is a function or constructor call that has no side effects. */
public final boolean isNoSideEffectsCall() {
return getSideEffectFlags() == SideEffectFlags.NO_SIDE_EFFECTS;
}
/** Returns true if this is a new/call that may mutate its arguments. */
public final boolean mayMutateArguments() {
return allBitsSet(getSideEffectFlags(), SideEffectFlags.MUTATES_ARGUMENTS);
}
/** Returns true if this is a new/call that may mutate global state or throw. */
public final boolean mayMutateGlobalStateOrThrow() {
return anyBitSet(
getSideEffectFlags(), SideEffectFlags.MUTATES_GLOBAL_STATE | SideEffectFlags.THROWS);
}
/** Returns true iff all the set bits in {@code mask} are also set in {@code value}. */
private static boolean allBitsSet(int value, int mask) {
return (value & mask) == mask;
}
/** Returns true iff any the bit set in {@code mask} is also set in {@code value}. */
private static boolean anyBitSet(int value, int mask) {
return (value & mask) != 0;
}
/**
* Constants for the {@link Prop#CONSTANT_VAR_FLAGS} bit set property.
*
*
* - {@link ConstantVarFlags#DECLARED} means the name was declared using annotation or syntax
* indicating it must be constant
*
- {@link ConstantVarFlags#INFERRED} means the compiler can see that it is assigned exactly
* once, whether or not it was declared to be constant.
*
*
* Either, both, or neither may be set for any name.
*/
private static final class ConstantVarFlags {
// each constant should be a distinct power of 2.
static final int DECLARED = 1;
static final int INFERRED = 2;
private ConstantVarFlags() {}
}
private final int getConstantVarFlags() {
return getIntProp(Prop.CONSTANT_VAR_FLAGS);
}
private final void setConstantVarFlag(int flag, boolean value) {
int flags = getConstantVarFlags();
if (value) {
flags = flags | flag;
} else {
flags = flags & ~flag;
}
putIntProp(Prop.CONSTANT_VAR_FLAGS, flags);
}
/**
* Returns whether this variable is declared as a constant.
*
*
The compiler considers a variable to be declared if:
*
*
* - it is declared with the {@code const} keyword, or
*
- It is declared with a jsdoc {@code @const} annotation, or
*
- The current coding convention considers it to be a constant.
*
*
* Only valid to call on a {@linkplain #isName name} node.
*/
public final boolean isDeclaredConstantVar() {
checkState(
isName() || isImportStar(),
"Should only be called on name or import * nodes. Found %s",
this);
return anyBitSet(getConstantVarFlags(), ConstantVarFlags.DECLARED);
}
/**
* Sets this variable to be a declared constant.
*
*
See {@link #isDeclaredConstantVar} for the rules.
*/
public final void setDeclaredConstantVar(boolean value) {
checkState(
isName() || isImportStar(),
"Should only be called on name or import * nodes. Found %s",
this);
setConstantVarFlag(ConstantVarFlags.DECLARED, value);
}
/**
* Returns whether this variable is inferred to be constant.
*
*
The compiler infers a variable to be a constant if:
*
*
* - It is assigned at its declaration site, and
*
- It is never reassigned during its lifetime, and
*
- It is not defined by an extern.
*
*
* Only valid to call on a {@linkplain #isName name} node.
*/
public final boolean isInferredConstantVar() {
checkState(
isName() || isImportStar(),
"Should only be called on name or import * nodes. Found %s",
this);
return anyBitSet(getConstantVarFlags(), ConstantVarFlags.INFERRED);
}
/**
* Sets this variable to be an inferred constant. *
*
*
See {@link #isInferredConstantVar} for the rules.
*/
public final void setInferredConstantVar(boolean value) {
checkState(
isName() || isImportStar(),
"Should only be called on name or import * nodes. Found %s",
this);
setConstantVarFlag(ConstantVarFlags.INFERRED, value);
}
public final boolean isQuotedString() {
return (this instanceof StringNode) && this.getBooleanProp(Prop.QUOTED);
}
public final void setQuotedString() {
checkState(this instanceof StringNode, this);
this.putBooleanProp(Prop.QUOTED, true);
}
/*** AST type check methods ***/
public final boolean isAdd() {
return this.token == Token.ADD;
}
public final boolean isSub() {
return this.token == Token.SUB;
}
public final boolean isAnd() {
return this.token == Token.AND;
}
public final boolean isAssignAnd() {
return this.token == Token.ASSIGN_AND;
}
public final boolean isArrayLit() {
return this.token == Token.ARRAYLIT;
}
public final boolean isArrayPattern() {
return this.token == Token.ARRAY_PATTERN;
}
public final boolean isAssign() {
return this.token == Token.ASSIGN;
}
public final boolean isAssignAdd() {
return this.token == Token.ASSIGN_ADD;
}
public final boolean isNormalBlock() {
return isBlock();
}
public final boolean isBlock() {
return this.token == Token.BLOCK;
}
public final boolean isRoot() {
return this.token == Token.ROOT;
}
public final boolean isAwait() {
return this.token == Token.AWAIT;
}
public final boolean isBigInt() {
return this.token == Token.BIGINT;
}
public final boolean isBitNot() {
return this.token == Token.BITNOT;
}
public final boolean isBreak() {
return this.token == Token.BREAK;
}
public final boolean isCall() {
return this.token == Token.CALL;
}
public final boolean isCase() {
return this.token == Token.CASE;
}
public final boolean isCast() {
return this.token == Token.CAST;
}
public final boolean isCatch() {
return this.token == Token.CATCH;
}
public final boolean isClass() {
return this.token == Token.CLASS;
}
public final boolean isClassMembers() {
return this.token == Token.CLASS_MEMBERS;
}
public final boolean isComma() {
return this.token == Token.COMMA;
}
public final boolean isComputedProp() {
return this.token == Token.COMPUTED_PROP;
}
public final boolean isContinue() {
return this.token == Token.CONTINUE;
}
public final boolean isConst() {
return this.token == Token.CONST;
}
public final boolean isDebugger() {
return this.token == Token.DEBUGGER;
}
public final boolean isDec() {
return this.token == Token.DEC;
}
public final boolean isDefaultCase() {
return this.token == Token.DEFAULT_CASE;
}
public final boolean isDefaultValue() {
return this.token == Token.DEFAULT_VALUE;
}
public final boolean isDelProp() {
return this.token == Token.DELPROP;
}
public final boolean isDestructuringLhs() {
return this.token == Token.DESTRUCTURING_LHS;
}
public final boolean isDestructuringPattern() {
return isObjectPattern() || isArrayPattern();
}
public final boolean isDo() {
return this.token == Token.DO;
}
public final boolean isEmpty() {
return this.token == Token.EMPTY;
}
public final boolean isExponent() {
return this.token == Token.EXPONENT;
}
public final boolean isAssignExponent() {
return this.token == Token.ASSIGN_EXPONENT;
}
public final boolean isExport() {
return this.token == Token.EXPORT;
}
public final boolean isExportSpec() {
return this.token == Token.EXPORT_SPEC;
}
public final boolean isExportSpecs() {
return this.token == Token.EXPORT_SPECS;
}
public final boolean isExprResult() {
return this.token == Token.EXPR_RESULT;
}
public final boolean isFalse() {
return this.token == Token.FALSE;
}
public final boolean isVanillaFor() {
return this.token == Token.FOR;
}
public final boolean isForIn() {
return this.token == Token.FOR_IN;
}
public final boolean isForOf() {
return this.token == Token.FOR_OF;
}
public final boolean isForAwaitOf() {
return this.token == Token.FOR_AWAIT_OF;
}
public final boolean isFunction() {
return this.token == Token.FUNCTION;
}
public final boolean isGetterDef() {
return this.token == Token.GETTER_DEF;
}
public final boolean isGetElem() {
return this.token == Token.GETELEM;
}
public final boolean isGetProp() {
return this.token == Token.GETPROP;
}
public final boolean isHook() {
return this.token == Token.HOOK;
}
public final boolean isIf() {
return this.token == Token.IF;
}
public final boolean isImport() {
return this.token == Token.IMPORT;
}
public final boolean isImportMeta() {
return this.token == Token.IMPORT_META;
}
public final boolean isImportStar() {
return this.token == Token.IMPORT_STAR;
}
public final boolean isImportSpec() {
return this.token == Token.IMPORT_SPEC;
}
public final boolean isImportSpecs() {
return this.token == Token.IMPORT_SPECS;
}
public final boolean isIn() {
return this.token == Token.IN;
}
public final boolean isInc() {
return this.token == Token.INC;
}
public final boolean isInstanceOf() {
return this.token == Token.INSTANCEOF;
}
public final boolean isInterfaceMembers() {
return this.token == Token.INTERFACE_MEMBERS;
}
public final boolean isRecordType() {
return this.token == Token.RECORD_TYPE;
}
public final boolean isCallSignature() {
return this.token == Token.CALL_SIGNATURE;
}
public final boolean isIndexSignature() {
return this.token == Token.INDEX_SIGNATURE;
}
public final boolean isLabel() {
return this.token == Token.LABEL;
}
public final boolean isLabelName() {
return this.token == Token.LABEL_NAME;
}
public final boolean isLet() {
return this.token == Token.LET;
}
public final boolean isMemberFunctionDef() {
return this.token == Token.MEMBER_FUNCTION_DEF;
}
public final boolean isMemberVariableDef() {
return this.token == Token.MEMBER_VARIABLE_DEF;
}
public final boolean isMemberFieldDef() {
return this.token == Token.MEMBER_FIELD_DEF;
}
public final boolean isComputedFieldDef() {
return this.token == Token.COMPUTED_FIELD_DEF;
}
public final boolean isModuleBody() {
return this.token == Token.MODULE_BODY;
}
public final boolean isName() {
return this.token == Token.NAME;
}
public final boolean isNE() {
return this.token == Token.NE;
}
public final boolean isSHNE() {
return this.token == Token.SHNE;
}
public final boolean isEQ() {
return this.token == Token.EQ;
}
public final boolean isSHEQ() {
return this.token == Token.SHEQ;
}
public final boolean isNeg() {
return this.token == Token.NEG;
}
public final boolean isNew() {
return this.token == Token.NEW;
}
public final boolean isNot() {
return this.token == Token.NOT;
}
public final boolean isNull() {
return this.token == Token.NULL;
}
public final boolean isNullishCoalesce() {
return this.token == Token.COALESCE;
}
public final boolean isAssignNullishCoalesce() {
return this.token == Token.ASSIGN_COALESCE;
}
public final boolean isNumber() {
return this.token == Token.NUMBER;
}
public final boolean isObjectLit() {
return this.token == Token.OBJECTLIT;
}
public final boolean isObjectPattern() {
return this.token == Token.OBJECT_PATTERN;
}
public final boolean isOptChainCall() {
return this.token == Token.OPTCHAIN_CALL;
}
public final boolean isOptChainGetElem() {
return this.token == Token.OPTCHAIN_GETELEM;
}
public final boolean isOptChainGetProp() {
return this.token == Token.OPTCHAIN_GETPROP;
}
public final boolean isOr() {
return this.token == Token.OR;
}
public final boolean isAssignOr() {
return this.token == Token.ASSIGN_OR;
}
public final boolean isParamList() {
return this.token == Token.PARAM_LIST;
}
public final boolean isRegExp() {
return this.token == Token.REGEXP;
}
public final boolean isRest() {
return this.token == Token.ITER_REST || this.token == Token.OBJECT_REST;
}
public final boolean isObjectRest() {
return this.token == Token.OBJECT_REST;
}
public final boolean isReturn() {
return this.token == Token.RETURN;
}
public final boolean isScript() {
return this.token == Token.SCRIPT;
}
public final boolean isSetterDef() {
return this.token == Token.SETTER_DEF;
}
public final boolean isSpread() {
return this.token == Token.ITER_SPREAD || this.token == Token.OBJECT_SPREAD;
}
public final boolean isString() {
return this.token == Token.STRINGLIT;
}
public final boolean isStringKey() {
return this.token == Token.STRING_KEY;
}
public final boolean isStringLit() {
return this.token == Token.STRINGLIT;
}
public final boolean isSuper() {
return this.token == Token.SUPER;
}
public final boolean isSwitch() {
return this.token == Token.SWITCH;
}
public final boolean isTaggedTemplateLit() {
return this.token == Token.TAGGED_TEMPLATELIT;
}
public final boolean isTemplateLit() {
return this.token == Token.TEMPLATELIT;
}
public final boolean isTemplateLitString() {
return this.token == Token.TEMPLATELIT_STRING;
}
public final boolean isTemplateLitSub() {
return this.token == Token.TEMPLATELIT_SUB;
}
public final boolean isThis() {
return this.token == Token.THIS;
}
public final boolean isThrow() {
return this.token == Token.THROW;
}
public final boolean isTrue() {
return this.token == Token.TRUE;
}
public final boolean isTry() {
return this.token == Token.TRY;
}
public final boolean isTypeOf() {
return this.token == Token.TYPEOF;
}
public final boolean isVar() {
return this.token == Token.VAR;
}
public final boolean isVoid() {
return this.token == Token.VOID;
}
public final boolean isWhile() {
return this.token == Token.WHILE;
}
public final boolean isWith() {
return this.token == Token.WITH;
}
public final boolean isYield() {
return this.token == Token.YIELD;
}
public final boolean isDeclare() {
return this.token == Token.DECLARE;
}
}