com.google.javascript.jscomp.ChromePass Maven / Gradle / Ivy
/*
* Copyright 2014 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Compiler pass for Chrome-specific needs. It handles the following Chrome JS features:
*
*
* - namespace declaration using {@code cr.define()},
*
- unquoted property declaration using {@code {cr|Object}.defineProperty()}.
*
*
* For the details, see tests inside ChromePassTest.java.
*/
public class ChromePass extends AbstractPostOrderCallback implements CompilerPass {
final AbstractCompiler compiler;
private final Set createdObjects;
private static final String CR_DEFINE = "cr.define";
private static final String CR_EXPORT_PATH = "cr.exportPath";
private static final String OBJECT_DEFINE_PROPERTY = "Object.defineProperty";
private static final String CR_DEFINE_PROPERTY = "cr.defineProperty";
private static final String CR_MAKE_PUBLIC = "cr.makePublic";
private static final String CR_DEFINE_COMMON_EXPLANATION =
"It should be called like this:"
+ " cr.define('name.space', function() '{ ... return {Export: Internal}; }');";
static final DiagnosticType CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS =
DiagnosticType.error(
"JSC_CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS",
"cr.define() should have exactly 2 arguments. " + CR_DEFINE_COMMON_EXPLANATION);
static final DiagnosticType CR_EXPORT_PATH_TOO_FEW_ARGUMENTS =
DiagnosticType.error(
"JSC_CR_EXPORT_PATH_TOO_FEW_ARGUMENTS",
"cr.exportPath() should have at least 1 argument: path name.");
static final DiagnosticType CR_DEFINE_INVALID_FIRST_ARGUMENT =
DiagnosticType.error(
"JSC_CR_DEFINE_INVALID_FIRST_ARGUMENT",
"Invalid first argument for cr.define(). " + CR_DEFINE_COMMON_EXPLANATION);
static final DiagnosticType CR_DEFINE_INVALID_SECOND_ARGUMENT =
DiagnosticType.error(
"JSC_CR_DEFINE_INVALID_SECOND_ARGUMENT",
"Invalid second argument for cr.define(). " + CR_DEFINE_COMMON_EXPLANATION);
static final DiagnosticType CR_DEFINE_INVALID_RETURN_IN_FUNCTION =
DiagnosticType.error(
"JSC_CR_DEFINE_INVALID_RETURN_IN_SECOND_ARGUMENT",
"Function passed as second argument of cr.define() should return the"
+ " dictionary in its last statement. "
+ CR_DEFINE_COMMON_EXPLANATION);
static final DiagnosticType CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND =
DiagnosticType.error(
"JSC_CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND",
"Invalid cr.PropertyKind passed to cr.defineProperty(): expected ATTR,"
+ " BOOL_ATTR or JS, found \"{0}\".");
static final DiagnosticType CR_MAKE_PUBLIC_HAS_NO_JSDOC =
DiagnosticType.error(
"JSC_CR_MAKE_PUBLIC_HAS_NO_JSDOC",
"Private method exported by cr.makePublic() has no JSDoc.");
static final DiagnosticType CR_MAKE_PUBLIC_MISSED_DECLARATION =
DiagnosticType.error(
"JSC_CR_MAKE_PUBLIC_MISSED_DECLARATION",
"Method \"{1}_\" exported by cr.makePublic() on \"{0}\" has no declaration.");
static final DiagnosticType CR_MAKE_PUBLIC_INVALID_SECOND_ARGUMENT =
DiagnosticType.error(
"JSC_CR_MAKE_PUBLIC_INVALID_SECOND_ARGUMENT",
"Invalid second argument passed to cr.makePublic(): should be array of strings.");
public ChromePass(AbstractCompiler compiler) {
this.compiler = compiler;
// The global variable "cr" is declared in ui/webui/resources/js/cr.js.
this.createdObjects = new HashSet<>(Arrays.asList("cr"));
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverse(compiler, root, this);
}
@Override
public void visit(NodeTraversal t, Node node, Node parent) {
if (node.isCall()) {
Node callee = node.getFirstChild();
if (callee.matchesQualifiedName(CR_DEFINE)) {
visitNamespaceDefinition(node, parent);
} else if (callee.matchesQualifiedName(CR_EXPORT_PATH)) {
visitExportPath(node, parent);
} else if (callee.matchesQualifiedName(OBJECT_DEFINE_PROPERTY)
|| callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) {
visitPropertyDefinition(node, parent);
} else if (callee.matchesQualifiedName(CR_MAKE_PUBLIC)) {
visitMakePublic(node, parent);
}
}
}
private void visitPropertyDefinition(Node call, Node parent) {
Node callee = call.getFirstChild();
String target = call.getSecondChild().getQualifiedName();
if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY) && !target.endsWith(".prototype")) {
target += ".prototype";
}
Node property = call.getChildAtIndex(2);
Node getPropNode =
NodeUtil.newQName(compiler, target + "." + property.getString()).srcrefTree(call);
boolean isCrDefinePropertyCall = callee.matchesQualifiedName(CR_DEFINE_PROPERTY);
if (isCrDefinePropertyCall) {
// The 3rd argument (PropertyKind) is actually used at runtime, so it takes precedence.
Node propertyType = getTypeByCrPropertyKind(call.getChildAtIndex(3));
if (propertyType != null) {
setJsDocWithType(getPropNode, propertyType);
} else {
// Otherwise, if there's a @type above the cr.defineProperty() call, move it to the getter.
JSDocInfo sourceJsDocInfo = call.getJSDocInfo();
if (sourceJsDocInfo != null && sourceJsDocInfo.hasType()) {
getPropNode.setJSDocInfo(sourceJsDocInfo);
} else {
// Else, just give it a @type {?}.
setJsDocWithType(getPropNode, new Node(Token.QMARK));
}
}
// Nuke the JsDoc info above cr.defineProperty() calls so that the CheckJSDocInfo pass won't
// report any warnings.
call.setJSDocInfo(null);
} else {
setJsDocWithType(getPropNode, new Node(Token.QMARK));
}
Node definitionNode = IR.exprResult(getPropNode).srcref(parent);
parent.getParent().addChildAfter(definitionNode, parent);
compiler.reportChangeToEnclosingScope(isCrDefinePropertyCall ? call : getPropNode);
}
@Nullable
private Node getTypeByCrPropertyKind(@Nullable Node propertyKind) {
if (propertyKind == null || propertyKind.matchesQualifiedName("cr.PropertyKind.JS")) {
// This is valid, it just doesn't tell us much about the type.
return null;
}
if (propertyKind.matchesQualifiedName("cr.PropertyKind.ATTR")) {
return IR.string("string");
}
if (propertyKind.matchesQualifiedName("cr.PropertyKind.BOOL_ATTR")) {
return IR.string("boolean");
}
compiler.report(
JSError.make(
propertyKind,
CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND,
propertyKind.getQualifiedName()));
return null;
}
private static void setJsDocWithType(Node target, Node type) {
JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
builder.recordType(new JSTypeExpression(type, ""));
target.setJSDocInfo(builder.build());
}
private void visitMakePublic(Node call, Node exprResult) {
Node scope = exprResult.getParent();
String className = call.getSecondChild().getQualifiedName();
String prototype = className + ".prototype";
Node methods = call.getChildAtIndex(2);
if (methods == null || !methods.isArrayLit()) {
compiler.report(JSError.make(exprResult, CR_MAKE_PUBLIC_INVALID_SECOND_ARGUMENT));
return;
}
Set methodNames = new HashSet<>();
for (Node methodName : methods.children()) {
if (!methodName.isString()) {
compiler.report(JSError.make(methodName, CR_MAKE_PUBLIC_INVALID_SECOND_ARGUMENT));
return;
}
methodNames.add(methodName.getString());
}
for (Node child : scope.children()) {
if (isAssignmentToPrototype(child, prototype)) {
Node objectLit = child.getFirstChild().getSecondChild();
for (Node stringKey : objectLit.children()) {
String field = stringKey.getString();
maybeAddPublicDeclaration(field, methodNames, className, stringKey, scope, exprResult);
}
} else if (isAssignmentToPrototypeMethod(child, prototype)) {
Node assignNode = child.getFirstChild();
String qualifiedName = assignNode.getFirstChild().getQualifiedName();
String field = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1);
maybeAddPublicDeclaration(field, methodNames, className, assignNode, scope, exprResult);
} else if (isDummyPrototypeMethodDeclaration(child, prototype)) {
String qualifiedName = child.getFirstChild().getQualifiedName();
String field = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1);
maybeAddPublicDeclaration(
field, methodNames, className, child.getFirstChild(), scope, exprResult);
}
}
for (String missedDeclaration : methodNames) {
compiler.report(
JSError.make(
exprResult, CR_MAKE_PUBLIC_MISSED_DECLARATION, className, missedDeclaration));
}
}
private static boolean isAssignmentToPrototype(Node node, String prototype) {
Node assignNode;
return node.isExprResult()
&& (assignNode = node.getFirstChild()).isAssign()
&& assignNode.getFirstChild().matchesQualifiedName(prototype);
}
private static boolean isAssignmentToPrototypeMethod(Node node, String prototype) {
Node assignNode;
return node.isExprResult()
&& (assignNode = node.getFirstChild()).isAssign()
&& assignNode.getFirstChild().getQualifiedName().startsWith(prototype + ".");
}
private static boolean isDummyPrototypeMethodDeclaration(Node node, String prototype) {
Node getPropNode;
return node.isExprResult()
&& (getPropNode = node.getFirstChild()).isGetProp()
&& getPropNode.getQualifiedName().startsWith(prototype + ".");
}
private void maybeAddPublicDeclaration(
String field,
Set publicAPIStrings,
String className,
Node jsDocSourceNode,
Node scope,
Node exprResult) {
if (field.endsWith("_")) {
String publicName = field.substring(0, field.length() - 1);
if (publicAPIStrings.contains(publicName)) {
Node methodDeclaration = NodeUtil.newQName(compiler, className + "." + publicName);
if (jsDocSourceNode.getJSDocInfo() != null) {
methodDeclaration.setJSDocInfo(jsDocSourceNode.getJSDocInfo());
Node publicDeclaration = IR.exprResult(methodDeclaration).srcrefTree(exprResult);
scope.addChildBefore(publicDeclaration, exprResult);
compiler.reportChangeToEnclosingScope(publicDeclaration);
} else {
compiler.report(JSError.make(jsDocSourceNode, CR_MAKE_PUBLIC_HAS_NO_JSDOC));
}
publicAPIStrings.remove(publicName);
}
}
}
private void visitExportPath(Node crExportPathNode, Node parent) {
if (crExportPathNode.getChildCount() < 2) {
compiler.report(JSError.make(crExportPathNode, CR_EXPORT_PATH_TOO_FEW_ARGUMENTS));
return;
}
Node pathArg = crExportPathNode.getSecondChild();
if (pathArg.isString()) {
// TODO(dbeam): support cr.exportPath('ns').value.
createAndInsertObjectsForQualifiedName(parent, pathArg.getString());
}
}
private void createAndInsertObjectsForQualifiedName(Node scriptChild, String namespace) {
List objectsForQualifiedName = createObjectsForQualifiedName(namespace);
for (Node n : objectsForQualifiedName) {
scriptChild.getParent().addChildBefore(n, scriptChild);
}
if (!objectsForQualifiedName.isEmpty()) {
compiler.reportChangeToEnclosingScope(scriptChild);
}
}
private void visitNamespaceDefinition(Node crDefineCallNode, Node parent) {
if (crDefineCallNode.getChildCount() != 3) {
compiler.report(JSError.make(crDefineCallNode, CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS));
}
Node namespaceArg = crDefineCallNode.getSecondChild();
Node function = crDefineCallNode.getChildAtIndex(2);
if (!namespaceArg.isString()) {
compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_FIRST_ARGUMENT));
return;
}
// TODO(vitalyp): Check namespace name for validity here. It should be a valid chain of
// identifiers.
String namespace = namespaceArg.getString();
createAndInsertObjectsForQualifiedName(parent, namespace);
if (!function.isFunction()) {
compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_SECOND_ARGUMENT));
return;
}
Node returnNode;
Node objectLit;
Node functionBlock = function.getLastChild();
if ((returnNode = functionBlock.getLastChild()) == null
|| !returnNode.isReturn()
|| (objectLit = returnNode.getFirstChild()) == null
|| !objectLit.isObjectLit()) {
compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_RETURN_IN_FUNCTION));
return;
}
Map exports = objectLitToMap(objectLit);
NodeTraversal.traverse(
compiler,
functionBlock,
new RenameInternalsToExternalsCallback(namespace, exports, functionBlock));
}
private static Map objectLitToMap(Node objectLit) {
Map res = new HashMap<>();
for (Node keyNode : objectLit.children()) {
String key = keyNode.getString();
Node valueNode = keyNode.getFirstChild();
if (valueNode.isName()) {
String value = keyNode.getFirstChild().getString();
res.put(value, key);
}
}
return res;
}
/**
* For a string "a.b.c" produce the following JS IR:
*
*
*
*
* var a = a || {};
* a.b = a.b || {};
* a.b.c = a.b.c || {};
*/
private List createObjectsForQualifiedName(String namespace) {
List objects = new ArrayList<>();
String[] parts = namespace.split("\\.");
createObjectIfNew(objects, parts[0], true);
if (parts.length >= 2) {
StringBuilder currPrefix = new StringBuilder().append(parts[0]);
for (int i = 1; i < parts.length; ++i) {
currPrefix.append(".").append(parts[i]);
createObjectIfNew(objects, currPrefix.toString(), false);
}
}
return objects;
}
private void createObjectIfNew(List objects, String name, boolean needVar) {
if (!createdObjects.contains(name)) {
objects.add(createJsNode((needVar ? "var " : "") + name + " = " + name + " || {};"));
createdObjects.add(name);
}
}
private Node createJsNode(String code) {
// The parent node after parseSyntheticCode() is SCRIPT node, we need to get rid of it.
return compiler.parseSyntheticCode(code).removeFirstChild();
}
private class RenameInternalsToExternalsCallback extends AbstractPostOrderCallback {
private final String namespaceName;
private final Map exports;
private final Node namespaceBlock;
public RenameInternalsToExternalsCallback(
String namespaceName, Map exports, Node namespaceBlock) {
this.namespaceName = namespaceName;
this.exports = exports;
this.namespaceBlock = namespaceBlock;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if ((n.isFunction() || n.isClass())
&& parent == this.namespaceBlock
&& this.exports.containsKey(n.getFirstChild().getString())) {
// It's a top-level function/constructor/class definition.
//
// Change
//
// /** Some doc */
// function internalName() {}
//
// to
//
// /** Some doc */
// my.namespace.name.externalName = function internalName() {};
//
// and change
//
// /** Some doc */
// class InternalName {}
//
// to
//
// /** Some doc */
// my.namespace.name.ExternalName = class InternalName {};
//
// by looking up in this.exports for internalName to find the correspondent
// externalName.
Node clone = n.cloneTree();
if (clone.isClass()) {
Node className = clone.getFirstChild();
className.replaceWith(IR.empty().useSourceInfoFrom(className));
}
NodeUtil.markNewScopesChanged(clone, compiler);
Node exprResult =
IR.exprResult(IR.assign(buildQualifiedName(n.getFirstChild()), clone).srcref(n))
.srcref(n);
if (n.getJSDocInfo() != null) {
exprResult.getFirstChild().setJSDocInfo(n.getJSDocInfo());
clone.removeProp(Node.JSDOC_INFO_PROP);
}
this.namespaceBlock.replaceChild(n, exprResult);
NodeUtil.markFunctionsDeleted(n, compiler);
compiler.reportChangeToEnclosingScope(exprResult);
} else if (n.isName()
&& this.exports.containsKey(n.getString())
&& !parent.isFunction()
&& !parent.isClass()) {
if (NodeUtil.isNameDeclaration(parent)) {
if (parent.getParent() == this.namespaceBlock) {
// It's a top-level exported variable definition (maybe without an
// assignment).
// Change
//
// var enum = { 'one': 1, 'two': 2 };
//
// to
//
// my.namespace.name.enum = { 'one': 1, 'two': 2 };
Node varContent = n.removeFirstChild();
Node exprResult;
if (varContent == null) {
exprResult = IR.exprResult(buildQualifiedName(n)).srcref(parent);
} else {
exprResult =
IR.exprResult(IR.assign(buildQualifiedName(n), varContent).srcref(parent))
.srcref(parent);
}
if (parent.getJSDocInfo() != null) {
exprResult.getFirstChild().setJSDocInfo(parent.getJSDocInfo().clone());
}
this.namespaceBlock.replaceChild(parent, exprResult);
compiler.reportChangeToEnclosingScope(exprResult);
}
} else {
// It's a local name referencing exported entity. Change to its global name.
Node newNode = buildQualifiedName(n);
if (n.getJSDocInfo() != null) {
newNode.setJSDocInfo(n.getJSDocInfo().clone());
}
// If we alter the name of a called function, then it gets an explicit "this"
// value.
if (parent.isCall()) {
parent.putBooleanProp(Node.FREE_CALL, false);
}
parent.replaceChild(n, newNode);
compiler.reportChangeToEnclosingScope(newNode);
}
}
}
private Node buildQualifiedName(Node internalName) {
String externalName = this.exports.get(internalName.getString());
return NodeUtil.newQName(compiler, this.namespaceName + "." + externalName)
.srcrefTree(internalName);
}
}
}