All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.ChromePass Maven / Gradle / Ivy

Go to download

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. This binary checks for style issues such as incorrect or missing JSDoc usage, and missing goog.require() statements. It does not do more advanced checks such as typechecking.

There is a newer version: v20200830
Show newest version
/*
 * 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.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;

/**
 * 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); if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) { setJsDocWithType(getPropNode, getTypeByCrPropertyKind(call.getChildAtIndex(3))); } else { setJsDocWithType(getPropNode, new Node(Token.QMARK)); } Node definitionNode = IR.exprResult(getPropNode).srcref(parent); parent.getParent().addChildAfter(definitionNode, parent); compiler.reportChangeToEnclosingScope(definitionNode); } private Node getTypeByCrPropertyKind(Node propertyKind) { if (propertyKind == null || propertyKind.matchesQualifiedName("cr.PropertyKind.JS")) { return new Node(Token.QMARK); } 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().getQualifiedName().equals(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() && parent == this.namespaceBlock && this.exports.containsKey(n.getFirstChild().getString())) { // It's a top-level function/constructor definition. // // Change // // /** Some doc */ // function internalName() {} // // to // // /** Some doc */ // my.namespace.name.externalName = function internalName() {}; // // by looking up in this.exports for internalName to find the correspondent // externalName. Node functionTree = n.cloneTree(); Node exprResult = IR.exprResult(IR.assign(buildQualifiedName(n.getFirstChild()), functionTree).srcref(n)) .srcref(n); if (n.getJSDocInfo() != null) { exprResult.getFirstChild().setJSDocInfo(n.getJSDocInfo()); functionTree.removeProp(Node.JSDOC_INFO_PROP); } this.namespaceBlock.replaceChild(n, exprResult); compiler.reportChangeToEnclosingScope(exprResult); } else if (n.isName() && this.exports.containsKey(n.getString()) && !parent.isFunction()) { if (parent.isVar()) { 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); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy