com.google.javascript.jscomp.ijs.PotentialDeclaration Maven / Gradle / Ivy
/*
* Copyright 2017 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.ijs;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.NodeUtil;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import javax.annotation.Nullable;
/**
* Encapsulates something that could be a declaration.
*
* This includes:
* var/let/const declarations,
* function/class declarations,
* method declarations,
* assignments,
* goog.define calls,
* and even valueless property accesses (e.g. `/** @type {number} * / Foo.prototype.bar`)
*/
abstract class PotentialDeclaration {
// The fully qualified name of the declaration.
private final String fullyQualifiedName;
// The LHS node of the declaration.
private final Node lhs;
// The RHS node of the declaration, if it exists.
private final @Nullable Node rhs;
private PotentialDeclaration(String fullyQualifiedName, Node lhs, @Nullable Node rhs) {
this.fullyQualifiedName = checkNotNull(fullyQualifiedName);
this.lhs = checkNotNull(lhs);
this.rhs = rhs;
}
static PotentialDeclaration fromName(Node nameNode) {
checkArgument(nameNode.isQualifiedName(), nameNode);
Node rhs = NodeUtil.getRValueOfLValue(nameNode);
if (ClassUtil.isThisProp(nameNode)) {
String name = ClassUtil.getPrototypeNameOfThisProp(nameNode);
return new ThisPropDeclaration(name, nameNode, rhs);
}
return new NameDeclaration(nameNode.getQualifiedName(), nameNode, rhs);
}
static PotentialDeclaration fromMethod(Node functionNode) {
checkArgument(ClassUtil.isClassMethod(functionNode));
String name = ClassUtil.getFullyQualifiedNameOfMethod(functionNode);
return new MethodDeclaration(name, functionNode);
}
static PotentialDeclaration fromStringKey(Node stringKeyNode) {
checkArgument(stringKeyNode.isStringKey());
checkArgument(stringKeyNode.getParent().isObjectLit());
String name = "this." + stringKeyNode.getString();
if (stringKeyNode.getString().equals("properties")) {
JSDocInfo objLitJsDoc = NodeUtil.getBestJSDocInfo(stringKeyNode.getParent());
if (objLitJsDoc != null && objLitJsDoc.isPolymerBehavior()) {
return new PolymerBehaviorPropertiesDeclaration(name, stringKeyNode);
}
}
return new StringKeyDeclaration(name, stringKeyNode);
}
static PotentialDeclaration fromDefine(Node callNode) {
checkArgument(NodeUtil.isCallTo(callNode, "goog.define"));
return DefineDeclaration.from(callNode);
}
static PotentialDeclaration fromAlias(Node nameNode) {
checkArgument(nameNode.isQualifiedName(), nameNode);
return new AliasDeclaration(nameNode.getQualifiedName(), nameNode);
}
String getFullyQualifiedName() {
return fullyQualifiedName;
}
Node getLhs() {
return lhs;
}
@Nullable
Node getRhs() {
return rhs;
}
@Nullable
JSDocInfo getJsDoc() {
return NodeUtil.getBestJSDocInfo(lhs);
}
boolean isDetached() {
for (Node current = lhs; current != null; current = current.getParent()) {
if (current.isScript()) {
return false;
}
}
return true;
}
Node getRemovableNode() {
return NodeUtil.getEnclosingStatement(lhs);
}
/**
* Remove this "potential declaration" completely.
* Usually, this is because the same symbol has already been declared in this file.
*/
final void remove(AbstractCompiler compiler) {
if (isDetached()) {
return;
}
Node statement = getRemovableNode();
NodeUtil.deleteNode(statement, compiler);
statement.removeChildren();
}
/**
* Simplify this declaration to only include what's necessary for typing.
* Usually, this means removing the RHS and leaving a type annotation.
*/
abstract void simplify(AbstractCompiler compiler);
/**
* A potential declaration that has a fully qualified name to describe it.
* This includes things like:
* var/let/const/function/class declarations,
* assignments to a fully qualified name,
* and goog.module exports
* This is the most common type of potential declaration.
*/
private static class NameDeclaration extends PotentialDeclaration {
NameDeclaration(String fullyQualifiedName, Node lhs, Node rhs) {
super(fullyQualifiedName, lhs, rhs);
}
private void simplifyNamespace(AbstractCompiler compiler) {
if (getRhs().isOr()) {
Node objLit = getRhs().getLastChild().detach();
getRhs().replaceWith(objLit);
compiler.reportChangeToEnclosingScope(getLhs());
}
}
private void simplifySymbol(AbstractCompiler compiler) {
checkArgument(NodeUtil.isCallTo(getRhs(), "Symbol"));
Node callNode = getRhs();
while (callNode.hasMoreThanOneChild()) {
NodeUtil.deleteNode(callNode.getLastChild(), compiler);
}
}
@Override
void simplify(AbstractCompiler compiler) {
if (getRhs() == null || shouldPreserve()) {
return;
}
Node nameNode = getLhs();
JSDocInfo jsdoc = getJsDoc();
if (jsdoc != null && jsdoc.hasEnumParameterType()) {
super.simplifyEnumValues(compiler);
return;
}
if (NodeUtil.isNamespaceDecl(nameNode)) {
simplifyNamespace(compiler);
return;
}
if (nameNode.matchesName("exports")) {
// Replace the RHS of a default goog.module export with Unknown
replaceRhsWithUnknown(getRhs());
compiler.reportChangeToEnclosingScope(nameNode);
return;
}
if (NodeUtil.isCallTo(getRhs(), "Symbol")) {
simplifySymbol(compiler);
return;
}
if (getLhs().getParent().isConst()) {
jsdoc = JsdocUtil.markConstant(jsdoc);
}
// Just completely remove the RHS, and replace with a getprop.
Node newStatement =
NodeUtil.newQNameDeclaration(compiler, nameNode.getQualifiedName(), null, jsdoc);
newStatement.useSourceInfoIfMissingFromForTree(nameNode);
Node oldStatement = getRemovableNode();
NodeUtil.deleteChildren(oldStatement, compiler);
if (oldStatement.isExport()) {
oldStatement.addChildToBack(newStatement);
} else {
oldStatement.replaceWith(newStatement);
}
compiler.reportChangeToEnclosingScope(newStatement);
}
private static void replaceRhsWithUnknown(Node rhs) {
rhs.replaceWith(IR.cast(IR.number(0), JsdocUtil.getQmarkTypeJSDoc()).srcrefTree(rhs));
}
@Override
boolean shouldPreserve() {
Node rhs = getRhs();
Node nameNode = getLhs();
JSDocInfo jsdoc = getJsDoc();
boolean isExport = isExportLhs(nameNode);
return super.shouldPreserve()
|| isImportRhs(rhs)
|| (isExport && rhs != null && (rhs.isQualifiedName() || rhs.isObjectLit()))
|| (jsdoc != null && jsdoc.isConstructor() && rhs != null && rhs.isQualifiedName())
|| (rhs != null
&& rhs.isObjectLit()
&& !rhs.hasChildren()
&& (jsdoc == null || !JsdocUtil.hasAnnotatedType(jsdoc)))
|| (rhs != null && NodeUtil.isCallTo(rhs, "Polymer"))
|| isPolymerBehaviorAliasOrArray();
}
/**
* Polymer Behaviors can take 3 forms:
*
* {@code
* 1) /** @polymerBehavior *\/ export const MyBehavior = { ... };
* 2) /** @polymerBehavior *\/ export const MyBehaviorAlias = MyBehavior;
* 3) /** @polymerBehavior *\/ export const MyBehaviorArray = [Behavior1, Behavior2];
* }
*
* Form #1 will be simplified by PolymerBehaviorPropertiesDeclaration. Forms #2 and #3 need to
* be preserved here as-is so that the PolymerPass can follow the name references. Other forms
* annotated with @polymerBehavior are invalid and can be simplified or removed like any other
* variable.
*/
boolean isPolymerBehaviorAliasOrArray() {
JSDocInfo jsdoc = getJsDoc();
Node rhs = getRhs();
return jsdoc != null
&& jsdoc.isPolymerBehavior()
&& rhs != null
&& (rhs.isName() || rhs.isArrayLit());
}
}
/**
* A declaration of a property on `this` inside a constructor.
*/
private static class ThisPropDeclaration extends PotentialDeclaration {
private final Node insertionPoint;
ThisPropDeclaration(String fullyQualifiedName, Node lhs, Node rhs) {
super(fullyQualifiedName, lhs, rhs);
Node thisPropDefinition = NodeUtil.getEnclosingStatement(lhs);
this.insertionPoint = NodeUtil.getEnclosingStatement(thisPropDefinition.getParent());
}
@Override
void simplify(AbstractCompiler compiler) {
if (shouldPreserve()) {
return;
}
// Just completely remove the RHS, if present, and replace with a getprop.
Node newStatement =
NodeUtil.newQNameDeclaration(compiler, getFullyQualifiedName(), null, getJsDoc());
newStatement.useSourceInfoIfMissingFromForTree(getLhs());
NodeUtil.deleteNode(getRemovableNode(), compiler);
if (insertionPoint.getParent() != null) {
insertionPoint.getParent().addChildAfter(newStatement, insertionPoint);
compiler.reportChangeToEnclosingScope(newStatement);
}
}
}
/**
* A declaration declared by a call to `goog.define`. Note that a let, const, or var declaration
* annotated with @define in its JSDoc and no 'goog.define' would be a NameDeclaration instead.
*/
private static class DefineDeclaration extends PotentialDeclaration {
DefineDeclaration(String qualifiedName, Node lhs, Node rhs) {
super(qualifiedName, lhs, rhs);
}
@Override
void simplify(AbstractCompiler compiler) {
JSDocInfo info = getJsDoc();
if (info != null && info.getType() != null) {
Node newRhs = makeEmptyValueNode(info.getType());
if (newRhs != null) {
getRhs().replaceWith(newRhs);
compiler.reportChangeToEnclosingScope(newRhs);
return;
}
}
NodeUtil.deleteNode(getRemovableNode(), compiler);
}
static DefineDeclaration from(Node callNode) {
// Match a few different forms, depending on the call node's parent:
// 1. EXPR_RESULT: goog.define('foo', 1);
// 2. ASSIGN: a.b = goog.define('c', 2);
// 3. NAME: var x = goog.define('d', 3);
switch (callNode.getParent().getToken()) {
case EXPR_RESULT:
return new DefineDeclaration(
callNode.getSecondChild().getString(), callNode, callNode.getLastChild());
case ASSIGN:
Node previous = callNode.getPrevious();
return new DefineDeclaration(
previous.getQualifiedName(), previous, callNode.getLastChild());
case NAME:
Node parent = callNode.getParent();
return new DefineDeclaration(parent.getString(), parent, callNode.getLastChild());
default:
throw new IllegalStateException("Unexpected parent: " + callNode.getParent().getToken());
}
}
static Node makeEmptyValueNode(JSTypeExpression type) {
Node n = type.getRoot();
while (n != null && !n.isString() && !n.isName()) {
n = n.getFirstChild();
}
switch (n != null ? n.getString() : "") {
case "boolean":
return new Node(Token.FALSE);
case "number":
return Node.newNumber(0);
case "string":
return Node.newString("");
default:
return null;
}
}
}
/**
* A declaration of a method defined using the ES6 method syntax or goog.defineClass. Note that
* a method defined as an assignment to a prototype property would be a NameDeclaration instead.
*/
private static class MethodDeclaration extends PotentialDeclaration {
MethodDeclaration(String name, Node functionNode) {
super(name, functionNode.getParent(), functionNode);
}
@Override
void simplify(AbstractCompiler compiler) {}
@Override
Node getRemovableNode() {
return getLhs();
}
}
private static class StringKeyDeclaration extends PotentialDeclaration {
StringKeyDeclaration(String name, Node stringKeyNode) {
super(name, stringKeyNode, stringKeyNode.getLastChild());
}
@Override
void simplify(AbstractCompiler compiler) {
if (shouldPreserve()) {
return;
}
JSDocInfo jsdoc = getJsDoc();
if (jsdoc != null && jsdoc.hasEnumParameterType()) {
super.simplifyEnumValues(compiler);
return;
}
Node key = getLhs();
removeStringKeyValue(key);
compiler.reportChangeToEnclosingScope(key);
if (jsdoc == null
|| !jsdoc.containsDeclaration()
|| isConstToBeInferred()) {
key.setJSDocInfo(JsdocUtil.getUnusableTypeJSDoc(jsdoc));
}
}
@Override
boolean shouldPreserve() {
return super.isDetached() || super.shouldPreserve() || !isInNamespace();
}
private boolean isInNamespace() {
Node stringKey = getLhs();
Node objLit = stringKey.getParent();
Node lvalue = NodeUtil.getBestLValue(objLit);
if (lvalue == null) {
return false;
}
JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(lvalue);
return !isExportLhs(lvalue)
&& !JsdocUtil.hasAnnotatedType(jsdoc)
&& NodeUtil.isNamespaceDecl(lvalue);
}
@Override
Node getRemovableNode() {
return getLhs();
}
}
/**
* Polymer Behaviors are mixin-like objects used in Polymer 1 for multiple inheritance. They are
* also supported by Polymer 2 and 3 for backwards-compatibility, though their use is discouraged
* in favor of regular JavaScript mixins.
*
* Example:
*
*
{@code
* \/** @polymerBehavior *\/
* export const MyBehavior = {
* properties: {
* foo: String,
* bar: {
* type: Number,
* value: 123
* }
* },
* baz: function() {}
* };
* }
*
* For incremental compilation, it is important that the "properties" object is preserved,
* because the PolymerPass injects the properties declared there onto the prototypes of the
* Polymer elements that apply that behavior. Note that method signatures are already preserved so
* don't need additional handling here.
*/
private static class PolymerBehaviorPropertiesDeclaration extends PotentialDeclaration {
PolymerBehaviorPropertiesDeclaration(String name, Node stringKeyNode) {
super(name, stringKeyNode, stringKeyNode.getLastChild());
}
@Override
void simplify(AbstractCompiler compiler) {
if (isDetached()) {
return;
}
Node propertiesObject = getRhs();
if (!propertiesObject.isObjectLit() || !propertiesObject.hasChildren()) {
return;
}
for (Node propKey : propertiesObject.children()) {
Node propDef = propKey.getOnlyChild();
// A property definition is either a function reference (e.g. String, Number), or another
// object literal. If it's an object literal, only the "type" sub-property matters for type
// checking, so we can delete everything else (which may include e.g. a "value" sub-property
// with a function expression).
if (propDef.isObjectLit()) {
for (Node subProp : propDef.children()) {
if (!subProp.getString().equals("type")) {
NodeUtil.deleteNode(subProp, compiler);
}
}
}
}
}
@Override
boolean shouldPreserve() {
return true;
}
@Override
Node getRemovableNode() {
return getLhs();
}
}
private static class AliasDeclaration extends PotentialDeclaration {
/**
* @param name The alias name being declared.
* @param lhs The NAME node that represents the name of the individual alias.
*/
AliasDeclaration(String name, Node lhs) {
super(name, lhs, null);
}
@Override
void simplify(AbstractCompiler compiler) {
// Does not simplify
}
/**
* If the declaration is a destructuring declaration: 1) If the lhs's destructuring pattern
* parent has only one child, e.g. const {Foo} = x; returns the enclosing statement to remove
* the entire statement. 2) If the parent has more than one children, e.g. const {Foo, Bar} = x;
* returns the lhs so that when Foo is removed, const {Foo, Bar} = x; becomes const {Bar} = x;
* Otherwise, returns the enclosing statement.
*/
@Override
Node getRemovableNode() {
Node lhs = getLhs();
if (lhs.getParent().isArrayPattern() && lhs.getParent().hasMoreThanOneChild()) {
return lhs;
}
if (lhs.getGrandparent().isObjectPattern() && lhs.getGrandparent().hasMoreThanOneChild()) {
return lhs.getParent();
}
return NodeUtil.getEnclosingStatement(lhs);
}
@Override
boolean isDefiniteDeclaration() {
return true;
}
@Override
boolean shouldPreserve() {
return true;
}
}
/** Remove values from enums */
private void simplifyEnumValues(AbstractCompiler compiler) {
if (getRhs().isObjectLit() && getRhs().hasChildren()) {
for (Node key : getRhs().children()) {
removeStringKeyValue(key);
}
compiler.reportChangeToEnclosingScope(getRhs());
}
}
boolean isDefiniteDeclaration() {
Node parent = getLhs().getParent();
switch (parent.getToken()) {
case VAR:
case LET:
case CONST:
case CLASS:
case FUNCTION:
return true;
default:
return isExportLhs(getLhs())
|| (getJsDoc() != null && getJsDoc().containsDeclaration())
|| (getRhs() != null && PotentialDeclaration.isTypedRhs(getRhs()));
}
}
boolean shouldPreserve() {
return getRhs() != null && isTypedRhs(getRhs());
}
boolean isConstToBeInferred() {
return isConstToBeInferred(getLhs());
}
static boolean isConstToBeInferred(Node nameNode) {
JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(nameNode);
boolean isConst =
nameNode.getParent().isConst()
|| isExportLhs(nameNode)
|| (jsdoc != null && jsdoc.isConstant());
return isConst
&& !JsdocUtil.hasAnnotatedType(jsdoc)
&& !NodeUtil.isNamespaceDecl(nameNode);
}
private static boolean isTypedRhs(Node rhs) {
return rhs.isFunction()
|| rhs.isClass()
|| NodeUtil.isCallTo(rhs, "goog.defineClass")
|| (rhs.isQualifiedName() && rhs.matchesQualifiedName("goog.abstractMethod"))
|| (rhs.isQualifiedName() && rhs.matchesQualifiedName("goog.nullFunction"));
}
private static boolean isExportLhs(Node lhs) {
return (lhs.isName() && lhs.matchesName("exports"))
|| (lhs.isGetProp() && lhs.getFirstChild().matchesName("exports"))
|| lhs.matchesQualifiedName("module.exports");
}
static boolean isImportRhs(@Nullable Node rhs) {
if (rhs == null || !rhs.isCall()) {
return false;
}
Node callee = rhs.getFirstChild();
return callee.matchesQualifiedName("goog.require")
|| callee.matchesQualifiedName("goog.requireType")
|| callee.matchesQualifiedName("goog.forwardDeclare")
|| callee.matchesName("require");
}
static boolean isAliasDeclaration(Node lhs, @Nullable Node rhs) {
return !ClassUtil.isThisProp(lhs)
&& isConstToBeInferred(lhs)
&& rhs != null
&& rhs.isQualifiedName();
}
private static void removeStringKeyValue(Node stringKey) {
Node value = stringKey.getOnlyChild();
Node replacementValue = IR.number(0).srcrefTree(value);
stringKey.replaceChild(value, replacementValue);
}
}