ChromePass.java revision 6e8cce623b6e4fe0c9e4af605d675dd9d0338c38
1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package com.google.javascript.jscomp; 6 7import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; 8import com.google.javascript.rhino.IR; 9import com.google.javascript.rhino.JSDocInfoBuilder; 10import com.google.javascript.rhino.JSTypeExpression; 11import com.google.javascript.rhino.Node; 12import com.google.javascript.rhino.Token; 13 14import java.util.ArrayList; 15import java.util.Arrays; 16import java.util.HashMap; 17import java.util.HashSet; 18import java.util.List; 19import java.util.Map; 20import java.util.Set; 21 22/** 23 * Compiler pass for Chrome-specific needs. It handles the following Chrome JS features: 24 * <ul> 25 * <li>namespace declaration using {@code cr.define()}, 26 * <li>unquoted property declaration using {@code {cr|Object}.defineProperty()}. 27 * </ul> 28 * 29 * <p>For the details, see tests inside ChromePassTest.java. 30 */ 31public class ChromePass extends AbstractPostOrderCallback implements CompilerPass { 32 final AbstractCompiler compiler; 33 34 private Set<String> createdObjects; 35 36 private static final String CR_DEFINE = "cr.define"; 37 private static final String CR_EXPORT_PATH = "cr.exportPath"; 38 private static final String OBJECT_DEFINE_PROPERTY = "Object.defineProperty"; 39 private static final String CR_DEFINE_PROPERTY = "cr.defineProperty"; 40 41 private static final String CR_DEFINE_COMMON_EXPLANATION = "It should be called like this:" 42 + " cr.define('name.space', function() '{ ... return {Export: Internal}; }');"; 43 44 static final DiagnosticType CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS = 45 DiagnosticType.error("JSC_CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS", 46 "cr.define() should have exactly 2 arguments. " + CR_DEFINE_COMMON_EXPLANATION); 47 48 static final DiagnosticType CR_EXPORT_PATH_WRONG_NUMBER_OF_ARGUMENTS = 49 DiagnosticType.error("JSC_CR_EXPORT_PATH_WRONG_NUMBER_OF_ARGUMENTS", 50 "cr.exportPath() should have exactly 1 argument: namespace name."); 51 52 static final DiagnosticType CR_DEFINE_INVALID_FIRST_ARGUMENT = 53 DiagnosticType.error("JSC_CR_DEFINE_INVALID_FIRST_ARGUMENT", 54 "Invalid first argument for cr.define(). " + CR_DEFINE_COMMON_EXPLANATION); 55 56 static final DiagnosticType CR_DEFINE_INVALID_SECOND_ARGUMENT = 57 DiagnosticType.error("JSC_CR_DEFINE_INVALID_SECOND_ARGUMENT", 58 "Invalid second argument for cr.define(). " + CR_DEFINE_COMMON_EXPLANATION); 59 60 static final DiagnosticType CR_DEFINE_INVALID_RETURN_IN_FUNCTION = 61 DiagnosticType.error("JSC_CR_DEFINE_INVALID_RETURN_IN_SECOND_ARGUMENT", 62 "Function passed as second argument of cr.define() should return the" 63 + " dictionary in its last statement. " + CR_DEFINE_COMMON_EXPLANATION); 64 65 static final DiagnosticType CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND = 66 DiagnosticType.error("JSC_CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND", 67 "Invalid cr.PropertyKind passed to cr.defineProperty(): expected ATTR," 68 + " BOOL_ATTR or JS, found \"{0}\"."); 69 70 public ChromePass(AbstractCompiler compiler) { 71 this.compiler = compiler; 72 // The global variable "cr" is declared in ui/webui/resources/js/cr.js. 73 this.createdObjects = new HashSet<>(Arrays.asList("cr")); 74 } 75 76 @Override 77 public void process(Node externs, Node root) { 78 NodeTraversal.traverse(compiler, root, this); 79 } 80 81 @Override 82 public void visit(NodeTraversal t, Node node, Node parent) { 83 if (node.isCall()) { 84 Node callee = node.getFirstChild(); 85 if (callee.matchesQualifiedName(CR_DEFINE)) { 86 visitNamespaceDefinition(node, parent); 87 compiler.reportCodeChange(); 88 } else if (callee.matchesQualifiedName(CR_EXPORT_PATH)) { 89 visitExportPath(node, parent); 90 compiler.reportCodeChange(); 91 } else if (callee.matchesQualifiedName(OBJECT_DEFINE_PROPERTY) || 92 callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) { 93 visitPropertyDefinition(node, parent); 94 compiler.reportCodeChange(); 95 } 96 } 97 } 98 99 private void visitPropertyDefinition(Node call, Node parent) { 100 Node callee = call.getFirstChild(); 101 String target = call.getChildAtIndex(1).getQualifiedName(); 102 if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY) && !target.endsWith(".prototype")) { 103 target += ".prototype"; 104 } 105 106 Node property = call.getChildAtIndex(2); 107 108 Node getPropNode = NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), 109 target + "." + property.getString()).srcrefTree(call); 110 111 if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) { 112 setJsDocWithType(getPropNode, getTypeByCrPropertyKind(call.getChildAtIndex(3))); 113 } else { 114 setJsDocWithType(getPropNode, new Node(Token.QMARK)); 115 } 116 117 Node definitionNode = IR.exprResult(getPropNode).srcref(parent); 118 119 parent.getParent().addChildAfter(definitionNode, parent); 120 } 121 122 private Node getTypeByCrPropertyKind(Node propertyKind) { 123 if (propertyKind == null || propertyKind.matchesQualifiedName("cr.PropertyKind.JS")) { 124 return new Node(Token.QMARK); 125 } 126 if (propertyKind.matchesQualifiedName("cr.PropertyKind.ATTR")) { 127 return IR.string("string"); 128 } 129 if (propertyKind.matchesQualifiedName("cr.PropertyKind.BOOL_ATTR")) { 130 return IR.string("boolean"); 131 } 132 compiler.report(JSError.make(propertyKind, CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND, 133 propertyKind.getQualifiedName())); 134 return null; 135 } 136 137 private void setJsDocWithType(Node target, Node type) { 138 JSDocInfoBuilder builder = new JSDocInfoBuilder(false); 139 builder.recordType(new JSTypeExpression(type, "")); 140 target.setJSDocInfo(builder.build(target)); 141 } 142 143 private void visitExportPath(Node crExportPathNode, Node parent) { 144 if (crExportPathNode.getChildCount() != 2) { 145 compiler.report(JSError.make(crExportPathNode, 146 CR_EXPORT_PATH_WRONG_NUMBER_OF_ARGUMENTS)); 147 return; 148 } 149 150 createAndInsertObjectsForQualifiedName(parent, 151 crExportPathNode.getChildAtIndex(1).getString()); 152 } 153 154 private void createAndInsertObjectsForQualifiedName(Node scriptChild, String namespace) { 155 List<Node> objectsForQualifiedName = createObjectsForQualifiedName(namespace); 156 for (Node n : objectsForQualifiedName) { 157 scriptChild.getParent().addChildBefore(n, scriptChild); 158 } 159 } 160 161 private void visitNamespaceDefinition(Node crDefineCallNode, Node parent) { 162 if (crDefineCallNode.getChildCount() != 3) { 163 compiler.report(JSError.make(crDefineCallNode, CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS)); 164 } 165 166 Node namespaceArg = crDefineCallNode.getChildAtIndex(1); 167 Node function = crDefineCallNode.getChildAtIndex(2); 168 169 if (!namespaceArg.isString()) { 170 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_FIRST_ARGUMENT)); 171 return; 172 } 173 174 // TODO(vitalyp): Check namespace name for validity here. It should be a valid chain of 175 // identifiers. 176 String namespace = namespaceArg.getString(); 177 178 createAndInsertObjectsForQualifiedName(parent, namespace); 179 180 if (!function.isFunction()) { 181 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_SECOND_ARGUMENT)); 182 return; 183 } 184 185 Node returnNode, objectLit; 186 Node functionBlock = function.getLastChild(); 187 if ((returnNode = functionBlock.getLastChild()) == null || 188 !returnNode.isReturn() || 189 (objectLit = returnNode.getFirstChild()) == null || 190 !objectLit.isObjectLit()) { 191 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_RETURN_IN_FUNCTION)); 192 return; 193 } 194 195 Map<String, String> exports = objectLitToMap(objectLit); 196 197 NodeTraversal.traverse(compiler, functionBlock, new RenameInternalsToExternalsCallback( 198 namespace, exports, functionBlock)); 199 } 200 201 private Map<String, String> objectLitToMap(Node objectLit) { 202 Map<String, String> res = new HashMap<String, String>(); 203 204 for (Node keyNode : objectLit.children()) { 205 String key = keyNode.getString(); 206 207 // TODO(vitalyp): Can dict value be other than a simple NAME? What if NAME doesn't 208 // refer to a function/constructor? 209 String value = keyNode.getFirstChild().getString(); 210 211 res.put(value, key); 212 } 213 214 return res; 215 } 216 217 /** 218 * For a string "a.b.c" produce the following JS IR: 219 * 220 * <p><pre> 221 * var a = a || {}; 222 * a.b = a.b || {}; 223 * a.b.c = a.b.c || {};</pre> 224 */ 225 private List<Node> createObjectsForQualifiedName(String namespace) { 226 List<Node> objects = new ArrayList<>(); 227 String[] parts = namespace.split("\\."); 228 229 createObjectIfNew(objects, parts[0], true); 230 231 if (parts.length >= 2) { 232 StringBuilder currPrefix = new StringBuilder().append(parts[0]); 233 for (int i = 1; i < parts.length; ++i) { 234 currPrefix.append(".").append(parts[i]); 235 createObjectIfNew(objects, currPrefix.toString(), false); 236 } 237 } 238 239 return objects; 240 } 241 242 private void createObjectIfNew(List<Node> objects, String name, boolean needVar) { 243 if (!createdObjects.contains(name)) { 244 objects.add(createJsNode((needVar ? "var " : "") + name + " = " + name + " || {};")); 245 createdObjects.add(name); 246 } 247 } 248 249 private Node createJsNode(String code) { 250 // The parent node after parseSyntheticCode() is SCRIPT node, we need to get rid of it. 251 return compiler.parseSyntheticCode(code).removeFirstChild(); 252 } 253 254 private class RenameInternalsToExternalsCallback extends AbstractPostOrderCallback { 255 private final String namespaceName; 256 private final Map<String, String> exports; 257 private final Node namespaceBlock; 258 259 public RenameInternalsToExternalsCallback(String namespaceName, 260 Map<String, String> exports, Node namespaceBlock) { 261 this.namespaceName = namespaceName; 262 this.exports = exports; 263 this.namespaceBlock = namespaceBlock; 264 } 265 266 @Override 267 public void visit(NodeTraversal t, Node n, Node parent) { 268 if (n.isFunction() && parent == this.namespaceBlock && 269 this.exports.containsKey(n.getFirstChild().getString())) { 270 // It's a top-level function/constructor definition. 271 // 272 // Change 273 // 274 // /** Some doc */ 275 // function internalName() {} 276 // 277 // to 278 // 279 // /** Some doc */ 280 // my.namespace.name.externalName = function internalName() {}; 281 // 282 // by looking up in this.exports for internalName to find the correspondent 283 // externalName. 284 Node functionTree = n.cloneTree(); 285 Node exprResult = IR.exprResult( 286 IR.assign(buildQualifiedName(n.getFirstChild()), functionTree).srcref(n) 287 ).srcref(n); 288 289 if (n.getJSDocInfo() != null) { 290 exprResult.getFirstChild().setJSDocInfo(n.getJSDocInfo()); 291 functionTree.removeProp(Node.JSDOC_INFO_PROP); 292 } 293 this.namespaceBlock.replaceChild(n, exprResult); 294 } else if (n.isName() && this.exports.containsKey(n.getString()) && 295 !parent.isFunction()) { 296 if (parent.isVar()) { 297 if (parent.getParent() == this.namespaceBlock) { 298 // It's a top-level exported variable definition. 299 // Change 300 // 301 // var enum = { 'one': 1, 'two': 2 }; 302 // 303 // to 304 // 305 // my.namespace.name.enum = { 'one': 1, 'two': 2 }; 306 Node varContent = n.removeFirstChild(); 307 Node exprResult = IR.exprResult( 308 IR.assign(buildQualifiedName(n), varContent).srcref(parent) 309 ).srcref(parent); 310 311 if (parent.getJSDocInfo() != null) { 312 exprResult.getFirstChild().setJSDocInfo(parent.getJSDocInfo().clone()); 313 } 314 this.namespaceBlock.replaceChild(parent, exprResult); 315 } 316 } else { 317 // It's a local name referencing exported entity. Change to its global name. 318 Node newNode = buildQualifiedName(n); 319 if (n.getJSDocInfo() != null) { 320 newNode.setJSDocInfo(n.getJSDocInfo().clone()); 321 } 322 323 // If we alter the name of a called function, then it gets an explicit "this" 324 // value. 325 if (parent.isCall()) { 326 parent.putBooleanProp(Node.FREE_CALL, false); 327 } 328 329 parent.replaceChild(n, newNode); 330 } 331 } 332 } 333 334 private Node buildQualifiedName(Node internalName) { 335 String externalName = this.exports.get(internalName.getString()); 336 return NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), 337 this.namespaceName + "." + externalName).srcrefTree(internalName); 338 } 339 } 340 341} 342