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.
5 package com.google.javascript.jscomp;
7 import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
8 import com.google.javascript.rhino.IR;
9 import com.google.javascript.rhino.JSDocInfoBuilder;
10 import com.google.javascript.rhino.JSTypeExpression;
11 import com.google.javascript.rhino.Node;
12 import com.google.javascript.rhino.Token;
14 import java.util.ArrayList;
15 import java.util.Arrays;
16 import java.util.HashMap;
17 import java.util.HashSet;
18 import java.util.List;
23 * Compiler pass for Chrome-specific needs. It handles the following Chrome JS features:
25 * <li>namespace declaration using {@code cr.define()},
26 * <li>unquoted property declaration using {@code {cr|Object}.defineProperty()}.
29 * <p>For the details, see tests inside ChromePassTest.java.
31 public class ChromePass extends AbstractPostOrderCallback implements CompilerPass {
32 final AbstractCompiler compiler;
34 private Set<String> createdObjects;
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 private static final String CR_MAKE_PUBLIC = "cr.makePublic";
42 private static final String CR_DEFINE_COMMON_EXPLANATION = "It should be called like this:"
43 + " cr.define('name.space', function() '{ ... return {Export: Internal}; }');";
45 static final DiagnosticType CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS =
46 DiagnosticType.error("JSC_CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS",
47 "cr.define() should have exactly 2 arguments. " + CR_DEFINE_COMMON_EXPLANATION);
49 static final DiagnosticType CR_EXPORT_PATH_WRONG_NUMBER_OF_ARGUMENTS =
50 DiagnosticType.error("JSC_CR_EXPORT_PATH_WRONG_NUMBER_OF_ARGUMENTS",
51 "cr.exportPath() should have exactly 1 argument: namespace name.");
53 static final DiagnosticType CR_DEFINE_INVALID_FIRST_ARGUMENT =
54 DiagnosticType.error("JSC_CR_DEFINE_INVALID_FIRST_ARGUMENT",
55 "Invalid first argument for cr.define(). " + CR_DEFINE_COMMON_EXPLANATION);
57 static final DiagnosticType CR_DEFINE_INVALID_SECOND_ARGUMENT =
58 DiagnosticType.error("JSC_CR_DEFINE_INVALID_SECOND_ARGUMENT",
59 "Invalid second argument for cr.define(). " + CR_DEFINE_COMMON_EXPLANATION);
61 static final DiagnosticType CR_DEFINE_INVALID_RETURN_IN_FUNCTION =
62 DiagnosticType.error("JSC_CR_DEFINE_INVALID_RETURN_IN_SECOND_ARGUMENT",
63 "Function passed as second argument of cr.define() should return the"
64 + " dictionary in its last statement. " + CR_DEFINE_COMMON_EXPLANATION);
66 static final DiagnosticType CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND =
67 DiagnosticType.error("JSC_CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND",
68 "Invalid cr.PropertyKind passed to cr.defineProperty(): expected ATTR,"
69 + " BOOL_ATTR or JS, found \"{0}\".");
71 static final DiagnosticType CR_MAKE_PUBLIC_HAS_NO_JSDOC =
72 DiagnosticType.error("JSC_CR_MAKE_PUBLIC_HAS_NO_JSDOC",
73 "Private method exported by cr.makePublic() has no JSDoc.");
75 static final DiagnosticType CR_MAKE_PUBLIC_MISSED_DECLARATION =
76 DiagnosticType.error("JSC_CR_MAKE_PUBLIC_MISSED_DECLARATION",
77 "Method \"{1}_\" exported by cr.makePublic() on \"{0}\" has no declaration.");
79 static final DiagnosticType CR_MAKE_PUBLIC_INVALID_SECOND_ARGUMENT =
80 DiagnosticType.error("JSC_CR_MAKE_PUBLIC_INVALID_SECOND_ARGUMENT",
81 "Invalid second argument passed to cr.makePublic(): should be array of " +
84 public ChromePass(AbstractCompiler compiler) {
85 this.compiler = compiler;
86 // The global variable "cr" is declared in ui/webui/resources/js/cr.js.
87 this.createdObjects = new HashSet<>(Arrays.asList("cr"));
91 public void process(Node externs, Node root) {
92 NodeTraversal.traverse(compiler, root, this);
96 public void visit(NodeTraversal t, Node node, Node parent) {
98 Node callee = node.getFirstChild();
99 if (callee.matchesQualifiedName(CR_DEFINE)) {
100 visitNamespaceDefinition(node, parent);
101 compiler.reportCodeChange();
102 } else if (callee.matchesQualifiedName(CR_EXPORT_PATH)) {
103 visitExportPath(node, parent);
104 compiler.reportCodeChange();
105 } else if (callee.matchesQualifiedName(OBJECT_DEFINE_PROPERTY) ||
106 callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) {
107 visitPropertyDefinition(node, parent);
108 compiler.reportCodeChange();
109 } else if (callee.matchesQualifiedName(CR_MAKE_PUBLIC)) {
110 if (visitMakePublic(node, parent)) {
111 compiler.reportCodeChange();
117 private void visitPropertyDefinition(Node call, Node parent) {
118 Node callee = call.getFirstChild();
119 String target = call.getChildAtIndex(1).getQualifiedName();
120 if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY) && !target.endsWith(".prototype")) {
121 target += ".prototype";
124 Node property = call.getChildAtIndex(2);
126 Node getPropNode = NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(),
127 target + "." + property.getString()).srcrefTree(call);
129 if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) {
130 setJsDocWithType(getPropNode, getTypeByCrPropertyKind(call.getChildAtIndex(3)));
132 setJsDocWithType(getPropNode, new Node(Token.QMARK));
135 Node definitionNode = IR.exprResult(getPropNode).srcref(parent);
137 parent.getParent().addChildAfter(definitionNode, parent);
140 private Node getTypeByCrPropertyKind(Node propertyKind) {
141 if (propertyKind == null || propertyKind.matchesQualifiedName("cr.PropertyKind.JS")) {
142 return new Node(Token.QMARK);
144 if (propertyKind.matchesQualifiedName("cr.PropertyKind.ATTR")) {
145 return IR.string("string");
147 if (propertyKind.matchesQualifiedName("cr.PropertyKind.BOOL_ATTR")) {
148 return IR.string("boolean");
150 compiler.report(JSError.make(propertyKind, CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND,
151 propertyKind.getQualifiedName()));
155 private void setJsDocWithType(Node target, Node type) {
156 JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
157 builder.recordType(new JSTypeExpression(type, ""));
158 target.setJSDocInfo(builder.build(target));
161 private boolean visitMakePublic(Node call, Node exprResult) {
162 boolean changesMade = false;
163 Node scope = exprResult.getParent();
164 String className = call.getChildAtIndex(1).getQualifiedName();
165 String prototype = className + ".prototype";
166 Node methods = call.getChildAtIndex(2);
168 if (methods == null || !methods.isArrayLit()) {
169 compiler.report(JSError.make(exprResult, CR_MAKE_PUBLIC_INVALID_SECOND_ARGUMENT));
173 Set<String> methodNames = new HashSet<>();
174 for (Node methodName: methods.children()) {
175 if (!methodName.isString()) {
176 compiler.report(JSError.make(methodName, CR_MAKE_PUBLIC_INVALID_SECOND_ARGUMENT));
179 methodNames.add(methodName.getString());
182 for (Node child: scope.children()) {
183 if (isAssignmentToPrototype(child, prototype)) {
184 Node objectLit = child.getFirstChild().getChildAtIndex(1);
185 for (Node stringKey : objectLit.children()) {
186 String field = stringKey.getString();
187 changesMade |= maybeAddPublicDeclaration(field, methodNames, className,
188 stringKey, scope, exprResult);
190 } else if (isAssignmentToPrototypeMethod(child, prototype)) {
191 Node assignNode = child.getFirstChild();
192 String qualifiedName = assignNode.getFirstChild().getQualifiedName();
193 String field = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1);
194 changesMade |= maybeAddPublicDeclaration(field, methodNames, className,
195 assignNode, scope, exprResult);
196 } else if (isDummyPrototypeMethodDeclaration(child, prototype)) {
197 String qualifiedName = child.getFirstChild().getQualifiedName();
198 String field = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1);
199 changesMade |= maybeAddPublicDeclaration(field, methodNames, className,
200 child.getFirstChild(), scope, exprResult);
204 for (String missedDeclaration : methodNames) {
205 compiler.report(JSError.make(exprResult, CR_MAKE_PUBLIC_MISSED_DECLARATION, className,
212 private boolean isAssignmentToPrototype(Node node, String prototype) {
214 return node.isExprResult() && (assignNode = node.getFirstChild()).isAssign() &&
215 assignNode.getFirstChild().getQualifiedName().equals(prototype);
218 private boolean isAssignmentToPrototypeMethod(Node node, String prototype) {
220 return node.isExprResult() && (assignNode = node.getFirstChild()).isAssign() &&
221 assignNode.getFirstChild().getQualifiedName().startsWith(prototype + ".");
224 private boolean isDummyPrototypeMethodDeclaration(Node node, String prototype) {
226 return node.isExprResult() && (getPropNode = node.getFirstChild()).isGetProp() &&
227 getPropNode.getQualifiedName().startsWith(prototype + ".");
230 private boolean maybeAddPublicDeclaration(String field, Set<String> publicAPIStrings,
231 String className, Node jsDocSourceNode, Node scope, Node exprResult) {
232 boolean changesMade = false;
233 if (field.endsWith("_")) {
234 String publicName = field.substring(0, field.length() - 1);
235 if (publicAPIStrings.contains(publicName)) {
236 Node methodDeclaration = NodeUtil.newQualifiedNameNode(
237 compiler.getCodingConvention(), className + "." + publicName);
238 if (jsDocSourceNode.getJSDocInfo() != null) {
239 methodDeclaration.setJSDocInfo(jsDocSourceNode.getJSDocInfo());
240 scope.addChildBefore(
241 IR.exprResult(methodDeclaration).srcrefTree(exprResult),
245 compiler.report(JSError.make(jsDocSourceNode, CR_MAKE_PUBLIC_HAS_NO_JSDOC));
247 publicAPIStrings.remove(publicName);
253 private void visitExportPath(Node crExportPathNode, Node parent) {
254 if (crExportPathNode.getChildCount() != 2) {
255 compiler.report(JSError.make(crExportPathNode,
256 CR_EXPORT_PATH_WRONG_NUMBER_OF_ARGUMENTS));
260 createAndInsertObjectsForQualifiedName(parent,
261 crExportPathNode.getChildAtIndex(1).getString());
264 private void createAndInsertObjectsForQualifiedName(Node scriptChild, String namespace) {
265 List<Node> objectsForQualifiedName = createObjectsForQualifiedName(namespace);
266 for (Node n : objectsForQualifiedName) {
267 scriptChild.getParent().addChildBefore(n, scriptChild);
271 private void visitNamespaceDefinition(Node crDefineCallNode, Node parent) {
272 if (crDefineCallNode.getChildCount() != 3) {
273 compiler.report(JSError.make(crDefineCallNode, CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS));
276 Node namespaceArg = crDefineCallNode.getChildAtIndex(1);
277 Node function = crDefineCallNode.getChildAtIndex(2);
279 if (!namespaceArg.isString()) {
280 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_FIRST_ARGUMENT));
284 // TODO(vitalyp): Check namespace name for validity here. It should be a valid chain of
286 String namespace = namespaceArg.getString();
288 createAndInsertObjectsForQualifiedName(parent, namespace);
290 if (!function.isFunction()) {
291 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_SECOND_ARGUMENT));
295 Node returnNode, objectLit;
296 Node functionBlock = function.getLastChild();
297 if ((returnNode = functionBlock.getLastChild()) == null ||
298 !returnNode.isReturn() ||
299 (objectLit = returnNode.getFirstChild()) == null ||
300 !objectLit.isObjectLit()) {
301 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_RETURN_IN_FUNCTION));
305 Map<String, String> exports = objectLitToMap(objectLit);
307 NodeTraversal.traverse(compiler, functionBlock, new RenameInternalsToExternalsCallback(
308 namespace, exports, functionBlock));
311 private Map<String, String> objectLitToMap(Node objectLit) {
312 Map<String, String> res = new HashMap<String, String>();
314 for (Node keyNode : objectLit.children()) {
315 String key = keyNode.getString();
317 // TODO(vitalyp): Can dict value be other than a simple NAME? What if NAME doesn't
318 // refer to a function/constructor?
319 String value = keyNode.getFirstChild().getString();
328 * For a string "a.b.c" produce the following JS IR:
333 * a.b.c = a.b.c || {};</pre>
335 private List<Node> createObjectsForQualifiedName(String namespace) {
336 List<Node> objects = new ArrayList<>();
337 String[] parts = namespace.split("\\.");
339 createObjectIfNew(objects, parts[0], true);
341 if (parts.length >= 2) {
342 StringBuilder currPrefix = new StringBuilder().append(parts[0]);
343 for (int i = 1; i < parts.length; ++i) {
344 currPrefix.append(".").append(parts[i]);
345 createObjectIfNew(objects, currPrefix.toString(), false);
352 private void createObjectIfNew(List<Node> objects, String name, boolean needVar) {
353 if (!createdObjects.contains(name)) {
354 objects.add(createJsNode((needVar ? "var " : "") + name + " = " + name + " || {};"));
355 createdObjects.add(name);
359 private Node createJsNode(String code) {
360 // The parent node after parseSyntheticCode() is SCRIPT node, we need to get rid of it.
361 return compiler.parseSyntheticCode(code).removeFirstChild();
364 private class RenameInternalsToExternalsCallback extends AbstractPostOrderCallback {
365 private final String namespaceName;
366 private final Map<String, String> exports;
367 private final Node namespaceBlock;
369 public RenameInternalsToExternalsCallback(String namespaceName,
370 Map<String, String> exports, Node namespaceBlock) {
371 this.namespaceName = namespaceName;
372 this.exports = exports;
373 this.namespaceBlock = namespaceBlock;
377 public void visit(NodeTraversal t, Node n, Node parent) {
378 if (n.isFunction() && parent == this.namespaceBlock &&
379 this.exports.containsKey(n.getFirstChild().getString())) {
380 // It's a top-level function/constructor definition.
385 // function internalName() {}
390 // my.namespace.name.externalName = function internalName() {};
392 // by looking up in this.exports for internalName to find the correspondent
394 Node functionTree = n.cloneTree();
395 Node exprResult = IR.exprResult(
396 IR.assign(buildQualifiedName(n.getFirstChild()), functionTree).srcref(n)
399 if (n.getJSDocInfo() != null) {
400 exprResult.getFirstChild().setJSDocInfo(n.getJSDocInfo());
401 functionTree.removeProp(Node.JSDOC_INFO_PROP);
403 this.namespaceBlock.replaceChild(n, exprResult);
404 } else if (n.isName() && this.exports.containsKey(n.getString()) &&
405 !parent.isFunction()) {
406 if (parent.isVar()) {
407 if (parent.getParent() == this.namespaceBlock) {
408 // It's a top-level exported variable definition (maybe without an
412 // var enum = { 'one': 1, 'two': 2 };
416 // my.namespace.name.enum = { 'one': 1, 'two': 2 };
417 Node varContent = n.removeFirstChild();
419 if (varContent == null) {
420 exprResult = IR.exprResult(buildQualifiedName(n)).srcref(parent);
422 exprResult = IR.exprResult(
423 IR.assign(buildQualifiedName(n), varContent).srcref(parent)
426 if (parent.getJSDocInfo() != null) {
427 exprResult.getFirstChild().setJSDocInfo(parent.getJSDocInfo().clone());
429 this.namespaceBlock.replaceChild(parent, exprResult);
432 // It's a local name referencing exported entity. Change to its global name.
433 Node newNode = buildQualifiedName(n);
434 if (n.getJSDocInfo() != null) {
435 newNode.setJSDocInfo(n.getJSDocInfo().clone());
438 // If we alter the name of a called function, then it gets an explicit "this"
440 if (parent.isCall()) {
441 parent.putBooleanProp(Node.FREE_CALL, false);
444 parent.replaceChild(n, newNode);
449 private Node buildQualifiedName(Node internalName) {
450 String externalName = this.exports.get(internalName.getString());
451 return NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(),
452 this.namespaceName + "." + externalName).srcrefTree(internalName);