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.

There is a newer version: v20240317
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.JSDocInfo;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.QualifiedName;
import com.google.javascript.rhino.StaticSourceFile.SourceKind;
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 org.jspecify.nullness.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 OBJECT_DEFINE_PROPERTY = "Object.defineProperty"; private static final String CR_DEFINE_PROPERTY = "cr.defineProperty"; private static final String VIRTUAL_FILE = ""; private static final Node VIRTUAL_NODE = IR.empty().setStaticSourceFile(SourceFile.fromCode(VIRTUAL_FILE, "", SourceKind.EXTERN)); private static final QualifiedName CR_PROPERTYKIND_JS = QualifiedName.of("cr.PropertyKind.JS"); private static final QualifiedName CR_PROPERTYKIND_ATTR = QualifiedName.of("cr.PropertyKind.ATTR"); private static final QualifiedName CR_PROPERTYKIND_BOOL_ATTR = QualifiedName.of("cr.PropertyKind.BOOL_ATTR"); 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_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_TOO_FEW_ARGUMENTS = DiagnosticType.error( "JSC_CR_DEFINE_PROPERTY_TOO_FEW_ARGUMENTS", "cr.defineProperty() requires at least 2 arguments."); 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}\"."); 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(OBJECT_DEFINE_PROPERTY) || callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) { visitPropertyDefinition(node, parent); } } } private void visitPropertyDefinition(Node call, Node parent) { Node callee = call.getFirstChild(); boolean isCrDefinePropertyCall = callee.matchesQualifiedName(CR_DEFINE_PROPERTY); if (isCrDefinePropertyCall) { if (call.getChildCount() < 3) { compiler.report(JSError.make(call, CR_DEFINE_PROPERTY_TOO_FEW_ARGUMENTS)); return; } } else if (call.getChildCount() < 4) { // Note: Object.defineProperty() with less than 3 args throws at runtime. // We could also report some type of error here, but it seems odd to do this from the // ChromePass as there's nothing particularly Chrome-specific about Object.defineProperty() // with too few args. return; } String target = call.getSecondChild().getQualifiedName(); if (isCrDefinePropertyCall && !target.endsWith(".prototype")) { target += ".prototype"; } Node property = call.getChildAtIndex(2); Node getPropNode = NodeUtil.newQName(compiler, target + "." + property.getString()).srcrefTree(call); 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); definitionNode.insertAfter(parent); compiler.reportChangeToEnclosingScope(isCrDefinePropertyCall ? call : getPropNode); } private @Nullable Node getTypeByCrPropertyKind(@Nullable Node propertyKind) { if (propertyKind == null || CR_PROPERTYKIND_JS.matches(propertyKind)) { // This is valid, it just doesn't tell us much about the type. return null; } if (CR_PROPERTYKIND_ATTR.matches(propertyKind)) { return IR.string("string"); } if (CR_PROPERTYKIND_BOOL_ATTR.matches(propertyKind)) { 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) { JSDocInfo.Builder builder = JSDocInfo.builder(); builder.recordType(new JSTypeExpression(type.srcrefTree(VIRTUAL_NODE), VIRTUAL_FILE)); target.setJSDocInfo(builder.build()); } private void createAndInsertObjectsForQualifiedName(Node scriptChild, String namespace) { List objectsForQualifiedName = createObjectsForQualifiedName(namespace); for (Node n : objectsForQualifiedName) { n.srcrefTree(scriptChild).insertBefore(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.isStringLit()) { 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.getFirstChild(); keyNode != null; keyNode = keyNode.getNext()) { 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)) { return; } createdObjects.add(name); Node v = NodeUtil.newQName(this.compiler, name); Node lhs = IR.or(v.cloneTree(), IR.objectlit()); objects.add(needVar ? IR.var(v, lhs) : IR.exprResult(IR.assign(v, lhs))); } 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().srcref(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.setJSDocInfo(null); } n.replaceWith(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()); } parent.replaceWith(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); } n.replaceWith(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