com.google.javascript.jscomp.ClosureRewriteClass Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler Show documentation
Show all versions of closure-compiler Show documentation
Closure Compiler is a JavaScript optimizing compiler. It parses your
JavaScript, analyzes it, removes dead code and rewrites and minimizes
what's left. It also checks syntax, variable references, and types, and
warns about common JavaScript pitfalls. It is used in many of Google's
JavaScript apps, including Gmail, Google Web Search, Google Maps, and
Google Docs.
The newest version!
/*
* Copyright 2012 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.QualifiedName;
import com.google.javascript.rhino.Token;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jspecify.nullness.Nullable;
/**
* Rewrites "goog.defineClass" into a form that is suitable for type checking and dead code
* elimination.
*/
class ClosureRewriteClass extends AbstractPostOrderCallback implements CompilerPass {
// Errors
static final DiagnosticType GOOG_CLASS_TARGET_INVALID = DiagnosticType.error(
"JSC_GOOG_CLASS_TARGET_INVALID",
"Unsupported class definition expression.");
static final DiagnosticType GOOG_CLASS_SUPER_CLASS_NOT_VALID = DiagnosticType.error(
"JSC_GOOG_CLASS_SUPER_CLASS_NOT_VALID",
"The super class must be null or a valid name reference");
static final DiagnosticType GOOG_CLASS_DESCRIPTOR_NOT_VALID = DiagnosticType.error(
"JSC_GOOG_CLASS_DESCRIPTOR_NOT_VALID",
"The class must be defined by an object literal");
static final DiagnosticType GOOG_CLASS_CONSTRUCTOR_MISSING = DiagnosticType.error(
"JSC_GOOG_CLASS_CONSTRUCTOR_MISSING",
"The 'constructor' property is missing for the class definition");
static final DiagnosticType GOOG_CLASS_CONSTRUCTOR_NOT_VALID = DiagnosticType.error(
"JSC_GOOG_CLASS_CONSTRUCTOR_NOT_VALID",
"The 'constructor' expression must be a function literal");
static final DiagnosticType GOOG_CLASS_CONSTRUCTOR_ON_INTERFACE = DiagnosticType.error(
"JSC_GOOG_CLASS_CONSTRUCTOR_ON_INTERFACE",
"An interface definition should not have a 'constructor' property");
static final DiagnosticType GOOG_CLASS_STATICS_NOT_VALID = DiagnosticType.error(
"JSC_GOOG_CLASS_STATICS_NOT_VALID",
"The class 'statics' property must be an object or function literal");
static final DiagnosticType GOOG_CLASS_UNEXPECTED_PARAMS = DiagnosticType.error(
"JSC_GOOG_CLASS_UNEXPECTED_PARAMS",
"Too many arguments to goog.defineClass.");
static final DiagnosticType GOOG_CLASS_ES6_COMPUTED_PROP_NAMES_NOT_SUPPORTED =
DiagnosticType.error(
"JSC_GOOG_CLASS_ES6_COMPUTED_PROP_NAMES_NOT_SUPPORTED",
"Computed property names not supported in goog.defineClass.");
static final DiagnosticType GOOG_CLASS_ES6_ARROW_FUNCTION_NOT_SUPPORTED =
DiagnosticType.error(
"JSC_GOOG_CLASS_ES6_ARROW_FUNCTION_NOT_SUPPORTED",
"Arrow functions not supported in goog.defineClass. Object literal method"
+ " definition may be an alternative.");
// Warnings
static final DiagnosticType GOOG_CLASS_NG_INJECT_ON_CLASS = DiagnosticType.warning(
"JSC_GOOG_CLASS_NG_INJECT_ON_CLASS",
"@ngInject should be declared on the constructor, not on the class.");
private static final QualifiedName GOOG_DEFINE_CLASS = QualifiedName.of("goog.defineClass");
private static final QualifiedName GOOG_MODULE_GET = QualifiedName.of("goog.module.get");
private final AbstractCompiler compiler;
public ClosureRewriteClass(AbstractCompiler compiler) {
this.compiler = compiler;
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverse(compiler, root, this);
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isCall() && isGoogDefineClass(n) && !validateUsage(n)) {
compiler.report(JSError.make(n, GOOG_CLASS_TARGET_INVALID));
}
maybeRewriteClassDefinition(t, n);
}
private boolean validateUsage(Node n) {
// There are only three valid usage patterns for of goog.defineClass
// var ClassName = googDefineClass
// namespace.ClassName = googDefineClass
// and within an objectlit, used by the goog.defineClass.
Node parent = n.getParent();
switch (parent.getToken()) {
case NAME:
return true;
case ASSIGN:
return n == parent.getLastChild() && parent.getParent().isExprResult();
case STRING_KEY:
return isContainedInGoogDefineClass(parent);
default:
break;
}
return false;
}
private boolean isContainedInGoogDefineClass(Node n) {
while (n != null) {
n = n.getParent();
if (n.isCall()) {
if (isGoogDefineClass(n)) {
return true;
}
} else if (!n.isObjectLit() && !n.isStringKey()) {
break;
}
}
return false;
}
private void maybeRewriteClassDefinition(NodeTraversal t, Node n) {
if (NodeUtil.isNameDeclaration(n)) {
Node target = n.getFirstChild();
Node value = target.getFirstChild();
maybeRewriteClassDefinition(t, n, target, value);
} else if (NodeUtil.isExprAssign(n)) {
Node assign = n.getFirstChild();
Node target = assign.getFirstChild();
Node value = assign.getLastChild();
maybeRewriteClassDefinition(t, n, target, value);
}
}
private void maybeRewriteClassDefinition(
NodeTraversal t, Node n, Node target, Node value) {
if (isGoogDefineClass(value)) {
if (!target.isQualifiedName()) {
compiler.report(JSError.make(n, GOOG_CLASS_TARGET_INVALID));
}
ClassDefinition def = extractClassDefinition(target, value);
if (def != null) {
value.detach();
target.detach();
rewriteGoogDefineClass(t, n, def);
}
}
}
private static class MemberDefinition {
final JSDocInfo info;
final Node name;
final Node value;
MemberDefinition(JSDocInfo info, @Nullable Node name, Node value) {
this.info = info;
this.name = name;
this.value = value;
}
}
private static final class ClassDefinition {
final Node name;
final JSDocInfo classInfo;
final Node superClass;
final MemberDefinition constructor;
final List staticProps;
final List props;
final Node classModifier;
ClassDefinition(
Node name,
JSDocInfo classInfo,
Node superClass,
MemberDefinition constructor,
List staticProps,
List props,
Node classModifier) {
this.name = name;
this.classInfo = classInfo;
this.superClass = superClass;
this.constructor = constructor;
this.staticProps = staticProps;
this.props = props;
this.classModifier = classModifier;
}
}
/**
* Validates the class definition and if valid, destructively extracts the class definition from
* the AST.
*/
private @Nullable ClassDefinition extractClassDefinition(Node targetName, Node callNode) {
JSDocInfo classInfo = NodeUtil.getBestJSDocInfo(targetName);
// name = goog.defineClass(superClass, {...}, [modifier, ...])
Node superClass = NodeUtil.getArgumentForCallOrNew(callNode, 0);
if (superClass == null
|| (!superClass.isNull()
&& !superClass.isQualifiedName()
&& !NodeUtil.isCallTo(superClass, GOOG_MODULE_GET))) {
compiler.report(JSError.make(callNode, GOOG_CLASS_SUPER_CLASS_NOT_VALID));
return null;
}
if (NodeUtil.isNullOrUndefined(superClass) || superClass.matchesName("Object")) {
superClass = null;
}
Node description = NodeUtil.getArgumentForCallOrNew(callNode, 1);
if (!validateObjLit(description, callNode)) {
// Errors will be reported in the validate method. Keeping here clean
return null;
}
int paramCount = callNode.getChildCount() - 1;
if (paramCount > 2) {
compiler.report(JSError.make(callNode, GOOG_CLASS_UNEXPECTED_PARAMS));
return null;
}
Node constructor = extractProperty(description, "constructor");
if (classInfo != null && classInfo.isInterface()) {
if (constructor != null) {
compiler.report(JSError.make(description, GOOG_CLASS_CONSTRUCTOR_ON_INTERFACE));
return null;
}
} else if (constructor == null) {
// report missing constructor
compiler.report(JSError.make(description, GOOG_CLASS_CONSTRUCTOR_MISSING));
return null;
} else {
if (!constructor.isFunction()) {
compiler.report(JSError.make(constructor, GOOG_CLASS_CONSTRUCTOR_NOT_VALID));
}
}
if (constructor == null) {
constructor = IR.function(
IR.name("").srcref(callNode),
IR.paramList().srcref(callNode),
IR.block().srcref(callNode));
constructor.srcref(callNode);
compiler.reportChangeToChangeScope(constructor);
}
JSDocInfo info = NodeUtil.getBestJSDocInfo(constructor);
Node classModifier = null;
Node statics = null;
Node staticsProp = extractProperty(description, "statics");
if (staticsProp != null) {
if (staticsProp.isObjectLit()){
if (!validateObjLit(staticsProp, staticsProp.getParent())) {
// Errors will be reported in the validate method. Keeping here clean
return null;
}
statics = staticsProp;
} else if (staticsProp.isFunction()) {
classModifier = staticsProp;
} else {
compiler.report(
JSError.make(staticsProp, GOOG_CLASS_STATICS_NOT_VALID));
return null;
}
}
if (statics == null) {
statics = IR.objectlit();
}
// Ok, now rip apart the definition into its component pieces.
// Remove the "special" property key nodes.
maybeDetach(constructor.getParent());
maybeDetach(statics.getParent());
if (classModifier != null) {
maybeDetach(classModifier.getParent());
}
return new ClassDefinition(
targetName,
classInfo,
maybeDetach(superClass),
new MemberDefinition(info, null, maybeDetach(constructor)),
objectLitToList(maybeDetach(statics)),
objectLitToList(description),
maybeDetach(classModifier));
}
private static Node maybeDetach(Node node) {
if (node != null && node.hasParent()) {
node.detach();
}
return node;
}
/**
* @param objlit the object literal being checked.
* @param parent the parent of the object literal node
* @return false if the node is not an object literal, or if it contains any
* property that is neither unquoted plain property nor member
* function definition (ES6 feature)
*/
private boolean validateObjLit(Node objlit, Node parent) {
if (objlit == null || !objlit.isObjectLit()) {
reportErrorOnContext(parent);
return false;
}
for (Node key = objlit.getFirstChild(); key != null; key = key.getNext()) {
if (key.isMemberFunctionDef()) {
continue;
}
if (key.isComputedProp()) {
// report using computed property name
compiler.report(JSError.make(objlit,
GOOG_CLASS_ES6_COMPUTED_PROP_NAMES_NOT_SUPPORTED));
return false;
}
if (key.isStringKey() && key.getFirstChild().isArrowFunction()) {
// report using arrow function
compiler.report(JSError.make(objlit,
GOOG_CLASS_ES6_ARROW_FUNCTION_NOT_SUPPORTED));
return false;
}
if (!key.isStringKey() || key.isQuotedStringKey()) {
reportErrorOnContext(parent);
return false;
}
}
return true;
}
private void reportErrorOnContext(Node parent){
if (parent.isStringKey()){
compiler.report(JSError.make(parent, GOOG_CLASS_STATICS_NOT_VALID));
} else {
// Report error in the context that the objlit is an
// argument of goog.defineClass call.
checkState(parent.isCall());
compiler.report(JSError.make(parent, GOOG_CLASS_DESCRIPTOR_NOT_VALID));
}
}
/**
* @return The first property in the objlit that matches the key.
*/
private static @Nullable Node extractProperty(Node objlit, String keyName) {
for (Node keyNode = objlit.getFirstChild(); keyNode != null; keyNode = keyNode.getNext()) {
if (keyNode.getString().equals(keyName)) {
return keyNode.getFirstChild();
}
}
return null;
}
private static List objectLitToList(
Node objlit) {
List result = new ArrayList<>();
for (Node keyNode = objlit.getFirstChild(); keyNode != null; keyNode = keyNode.getNext()) {
Node name = keyNode;
// The span of a member function def is the whole function. The NAME node should be the
// first-first child, which will have a span for just the name of the function.
if (keyNode.isMemberFunctionDef()) {
name = keyNode.getFirstFirstChild().cloneNode();
name.setString(keyNode.getString());
}
result.add(
new MemberDefinition(
NodeUtil.getBestJSDocInfo(keyNode), name, keyNode.removeFirstChild()));
}
objlit.detachChildren();
return result;
}
private void rewriteGoogDefineClass(NodeTraversal t, Node exprRoot, final ClassDefinition cls) {
// For simplicity add everything into a block, before adding it to the AST.
Node block = IR.block();
// remove the original jsdoc info if it was attached to the value.
cls.constructor.value.setJSDocInfo(null);
if (NodeUtil.isNameDeclaration(exprRoot)) {
// example: var ctr = function(){}
Node decl =
IR.declaration(cls.name.cloneTree(), cls.constructor.value, exprRoot.getToken())
.srcref(exprRoot);
// TODO(b/138324343): remove this special case for names in module scopes, which is
// here only to unblock moving module rewriting later. this should warn instead of being
// a no-op.
JSDocInfo mergedClassInfo =
mergeJsDocFor(
cls, decl, /* includeConstructorExport= */ !exprRoot.getParent().isModuleBody());
decl.setJSDocInfo(mergedClassInfo);
block.addChildToBack(decl);
} else {
// example: ns.ctr = function(){}
Node assign =
IR.assign(cls.name.cloneTree(), cls.constructor.value)
.srcref(exprRoot)
.setJSDocInfo(cls.constructor.info);
JSDocInfo mergedClassInfo =
mergeJsDocFor(cls, exprRoot.getOnlyChild(), /* includeConstructorExport= */ true);
assign.setJSDocInfo(mergedClassInfo);
Node expr = IR.exprResult(assign).srcref(exprRoot);
block.addChildToBack(expr);
}
if (cls.superClass != null) {
// example: goog.inherits(ctr, superClass)
block.addChildToBack(
fixupSrcref(
IR.exprResult(
IR.call(
NodeUtil.newQName(compiler, "goog.inherits").srcrefTree(cls.superClass),
cls.name.cloneTree(),
cls.superClass.cloneTree())
.srcref(cls.superClass))));
}
for (MemberDefinition def : cls.staticProps) {
if (!def.value.isCast()) {
// remove the original jsdoc info if it was attached to the value.
def.value.setJSDocInfo(null);
}
// example: ctr.prop = value
block.addChildToBack(
fixupSrcref(
IR.exprResult(
fixupSrcref(
IR.assign(
IR.getprop(cls.name.cloneTree(), def.name.getString())
.srcref(def.name),
def.value))
.setJSDocInfo(def.info))));
// Handle inner class definitions.
maybeRewriteClassDefinition(t, block.getLastChild());
}
for (MemberDefinition def : cls.props) {
// remove the original jsdoc info if it was attached to the value.
def.value.setJSDocInfo(null);
// example: ctr.prototype.prop = value
Node exprResult =
IR.exprResult(
IR.assign(
NodeUtil.newQName(
compiler,
cls.name.getQualifiedName() + ".prototype." + def.name.getString()),
def.value)
.setJSDocInfo(def.info));
exprResult.srcrefTreeIfMissing(def.name);
// The length needs to be set explicitly to include the string key node and the function node.
// If we just used the length of def.name or def.value alone, then refactorings which try to
// delete the method would not work correctly.
exprResult.setLength(
def.value.getSourceOffset() + def.value.getLength() - def.name.getSourceOffset());
block.addChildToBack(exprResult);
// Handle inner class definitions.
maybeRewriteClassDefinition(t, block.getLastChild());
}
if (cls.classModifier != null) {
// Inside the modifier function, replace references to the argument
// with the class name.
// function(cls) { cls.Foo = bar; }
// becomes
// function(cls) { theClassName.Foo = bar; }
// The cls parameter is unused, but leave it there so that it
// matches the JsDoc.
// TODO(tbreisacher): Add a warning if the param is shadowed or reassigned.
Node argList = cls.classModifier.getSecondChild();
Node arg = argList.getFirstChild();
final String argName = arg.getString();
NodeTraversal.builder()
.setCompiler(compiler)
.setCallback(
(NodeTraversal unused, Node n, Node parent) -> {
if (n.isName() && n.getString().equals(argName)) {
Node newName = cls.name.cloneTree();
n.replaceWith(newName);
compiler.reportChangeToEnclosingScope(newName);
}
})
.traverse(cls.classModifier.getLastChild());
block.addChildToBack(
IR.exprResult(
fixupFreeCall(
IR.call(
cls.classModifier,
cls.name.cloneTree())
.srcref(cls.classModifier)))
.srcref(cls.classModifier));
}
Node parent = exprRoot.getParent();
Node stmts = block.removeChildren();
parent.addChildrenAfter(stmts, exprRoot);
exprRoot.detach();
// compiler.reportChangeToEnclosingScope(parent);
t.reportCodeChange();
}
private static Node fixupSrcref(Node node) {
node.srcref(node.getFirstChild());
return node;
}
private static Node fixupFreeCall(Node call) {
checkState(call.isCall());
call.putBooleanProp(Node.FREE_CALL, true);
return call;
}
/**
* @return Whether the call represents a class definition.
*/
static boolean isGoogDefineClass(Node value) {
if (value != null && value.isCall()) {
return GOOG_DEFINE_CLASS.matches(value.getFirstChild());
}
return false;
}
static final String VIRTUAL_FILE = "";
private JSTypeExpression getSuperclassFor(Node superNode) {
final String superName;
if (superNode.isQualifiedName()) {
superName = superNode.getQualifiedName();
} else {
checkState(NodeUtil.isCallTo(superNode, GOOG_MODULE_GET));
superName = superNode.getLastChild().getString();
}
return new JSTypeExpression(
new Node(Token.BANG, IR.string(superName)).srcrefTree(superNode), VIRTUAL_FILE);
}
/**
* Merges the JSDoc from the class and constructor into a single function-level JSDoc
*
* @param includeConstructorExport if false, disregards the @export JSDoc from the constructor, if
* any. This is a workaround for b/138324343.
*/
private JSDocInfo mergeJsDocFor(
ClassDefinition cls, Node associatedNode, boolean includeConstructorExport) {
// avoid null checks
JSDocInfo classInfo =
(cls.classInfo != null)
? cls.classInfo
: JSDocInfo.builder().parseDocumentation().build(true);
JSDocInfo ctorInfo =
(cls.constructor.info != null)
? cls.constructor.info
: JSDocInfo.builder().parseDocumentation().build(true);
Node superNode = cls.superClass;
// Start with a clone of the constructor info if there is one.
JSDocInfo.Builder mergedInfo =
cls.constructor.info != null
? JSDocInfo.Builder.copyFrom(ctorInfo)
: JSDocInfo.builder().parseDocumentation();
// Optionally, remove @export from the cloned constructor info.
// TODO(b/138324343): remove this case (or, even better, just delete goog.defineClass support).
if (!includeConstructorExport && ctorInfo.isExport()) {
mergedInfo.removeExport();
}
// merge block description
String blockDescription = Joiner.on("\n").skipNulls().join(
classInfo.getBlockDescription(),
ctorInfo.getBlockDescription());
if (!blockDescription.isEmpty()) {
mergedInfo.recordBlockDescription(blockDescription);
}
// merge suppressions
Set suppressions = new LinkedHashSet<>();
suppressions.addAll(classInfo.getSuppressions());
suppressions.addAll(ctorInfo.getSuppressions());
if (!suppressions.isEmpty()) {
mergedInfo.recordSuppressions(suppressions);
}
// Use class deprecation if set.
if (classInfo.isDeprecated()) {
mergedInfo.recordDeprecated();
}
String deprecationReason = null;
if (classInfo.getDeprecationReason() != null) {
deprecationReason = classInfo.getDeprecationReason();
mergedInfo.recordDeprecationReason(deprecationReason);
}
// Use class visibility if specifically set
Visibility visibility = classInfo.getVisibility();
if (visibility != null && visibility != Visibility.INHERITED) {
mergedInfo.recordVisibility(classInfo.getVisibility());
}
if (classInfo.isAbstract()) {
mergedInfo.recordAbstract();
}
if (classInfo.isConstant()) {
mergedInfo.recordConstancy();
}
if (classInfo.isExport()) {
mergedInfo.recordExport();
}
// If @ngInject is on the ctor, it's already been copied above.
if (classInfo.isNgInject()) {
compiler.report(JSError.make(associatedNode, GOOG_CLASS_NG_INJECT_ON_CLASS));
mergedInfo.recordNgInject(true);
}
if (classInfo.makesUnrestricted() || ctorInfo.makesUnrestricted()) {
mergedInfo.recordUnrestricted();
} else if (classInfo.makesDicts() || ctorInfo.makesDicts()) {
mergedInfo.recordDict();
} else {
// @struct by default
mergedInfo.recordStruct();
}
// @constructor is implied, @interface must be explicit
boolean isInterface = classInfo.isInterface() || ctorInfo.isInterface();
if (isInterface) {
if (classInfo.usesImplicitMatch() || ctorInfo.usesImplicitMatch()) {
mergedInfo.recordImplicitMatch();
} else {
mergedInfo.recordInterface();
}
List extendedInterfaces = null;
if (classInfo.getExtendedInterfacesCount() > 0) {
extendedInterfaces = classInfo.getExtendedInterfaces();
} else if (ctorInfo.getExtendedInterfacesCount() == 0
&& superNode != null) {
extendedInterfaces = ImmutableList.of(getSuperclassFor(superNode));
}
if (extendedInterfaces != null) {
for (JSTypeExpression extend : extendedInterfaces) {
mergedInfo.recordExtendedInterface(extend);
}
}
} else {
// @constructor by default
mergedInfo.recordConstructor();
if (classInfo.getBaseType() != null) {
mergedInfo.recordBaseType(classInfo.getBaseType());
} else if (superNode != null) {
// a "super" implies @extends, build a default.
JSTypeExpression baseType = getSuperclassFor(superNode);
mergedInfo.recordBaseType(baseType);
}
// @implements from the class if they exist
List interfaces = classInfo.getImplementedInterfaces();
for (JSTypeExpression implemented : interfaces) {
mergedInfo.recordImplementedInterface(implemented);
}
}
// merge @template types if they exist
ImmutableMap classTemplates = classInfo.getTemplateTypes();
ImmutableMap ctorTemplates = ctorInfo.getTemplateTypes();
for (Map.Entry entry : classTemplates.entrySet()) {
mergedInfo.recordTemplateTypeName(entry.getKey(), entry.getValue());
}
for (Map.Entry entry : ctorTemplates.entrySet()) {
mergedInfo.recordTemplateTypeName(entry.getKey(), entry.getValue());
}
return mergedInfo.build();
}
}