closure: update from closure compiler's src and rebuild runner.jar
[chromium-blink-merge.git] / third_party / closure_compiler / runner / src / com / google / javascript / jscomp / ChromePass.java
blobd89782358003410cb798aa6b43b5ebd596ba2d24
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;
19 import java.util.Map;
20 import java.util.Set;
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>
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 " +
82 "strings.");
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"));
90 @Override
91 public void process(Node externs, Node root) {
92 NodeTraversal.traverse(compiler, root, this);
95 @Override
96 public void visit(NodeTraversal t, Node node, Node parent) {
97 if (node.isCall()) {
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.newQName(
127 compiler, target + "." + property.getString()).srcrefTree(call);
129 if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) {
130 setJsDocWithType(getPropNode, getTypeByCrPropertyKind(call.getChildAtIndex(3)));
131 } else {
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()));
152 return null;
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));
170 return changesMade;
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));
177 return changesMade;
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,
206 missedDeclaration));
209 return changesMade;
212 private boolean isAssignmentToPrototype(Node node, String prototype) {
213 Node assignNode;
214 return node.isExprResult() && (assignNode = node.getFirstChild()).isAssign() &&
215 assignNode.getFirstChild().getQualifiedName().equals(prototype);
218 private boolean isAssignmentToPrototypeMethod(Node node, String prototype) {
219 Node assignNode;
220 return node.isExprResult() && (assignNode = node.getFirstChild()).isAssign() &&
221 assignNode.getFirstChild().getQualifiedName().startsWith(prototype + ".");
224 private boolean isDummyPrototypeMethodDeclaration(Node node, String prototype) {
225 Node getPropNode;
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.newQName(compiler, className + "." + publicName);
237 if (jsDocSourceNode.getJSDocInfo() != null) {
238 methodDeclaration.setJSDocInfo(jsDocSourceNode.getJSDocInfo());
239 scope.addChildBefore(
240 IR.exprResult(methodDeclaration).srcrefTree(exprResult),
241 exprResult);
242 changesMade = true;
243 } else {
244 compiler.report(JSError.make(jsDocSourceNode, CR_MAKE_PUBLIC_HAS_NO_JSDOC));
246 publicAPIStrings.remove(publicName);
249 return changesMade;
252 private void visitExportPath(Node crExportPathNode, Node parent) {
253 if (crExportPathNode.getChildCount() != 2) {
254 compiler.report(JSError.make(crExportPathNode,
255 CR_EXPORT_PATH_WRONG_NUMBER_OF_ARGUMENTS));
256 return;
259 createAndInsertObjectsForQualifiedName(parent,
260 crExportPathNode.getChildAtIndex(1).getString());
263 private void createAndInsertObjectsForQualifiedName(Node scriptChild, String namespace) {
264 List<Node> objectsForQualifiedName = createObjectsForQualifiedName(namespace);
265 for (Node n : objectsForQualifiedName) {
266 scriptChild.getParent().addChildBefore(n, scriptChild);
270 private void visitNamespaceDefinition(Node crDefineCallNode, Node parent) {
271 if (crDefineCallNode.getChildCount() != 3) {
272 compiler.report(JSError.make(crDefineCallNode, CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS));
275 Node namespaceArg = crDefineCallNode.getChildAtIndex(1);
276 Node function = crDefineCallNode.getChildAtIndex(2);
278 if (!namespaceArg.isString()) {
279 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_FIRST_ARGUMENT));
280 return;
283 // TODO(vitalyp): Check namespace name for validity here. It should be a valid chain of
284 // identifiers.
285 String namespace = namespaceArg.getString();
287 createAndInsertObjectsForQualifiedName(parent, namespace);
289 if (!function.isFunction()) {
290 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_SECOND_ARGUMENT));
291 return;
294 Node returnNode, objectLit;
295 Node functionBlock = function.getLastChild();
296 if ((returnNode = functionBlock.getLastChild()) == null ||
297 !returnNode.isReturn() ||
298 (objectLit = returnNode.getFirstChild()) == null ||
299 !objectLit.isObjectLit()) {
300 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_RETURN_IN_FUNCTION));
301 return;
304 Map<String, String> exports = objectLitToMap(objectLit);
306 NodeTraversal.traverse(compiler, functionBlock, new RenameInternalsToExternalsCallback(
307 namespace, exports, functionBlock));
310 private Map<String, String> objectLitToMap(Node objectLit) {
311 Map<String, String> res = new HashMap<String, String>();
313 for (Node keyNode : objectLit.children()) {
314 String key = keyNode.getString();
316 // TODO(vitalyp): Can dict value be other than a simple NAME? What if NAME doesn't
317 // refer to a function/constructor?
318 String value = keyNode.getFirstChild().getString();
320 res.put(value, key);
323 return res;
327 * For a string "a.b.c" produce the following JS IR:
329 * <p><pre>
330 * var a = a || {};
331 * a.b = a.b || {};
332 * a.b.c = a.b.c || {};</pre>
334 private List<Node> createObjectsForQualifiedName(String namespace) {
335 List<Node> objects = new ArrayList<>();
336 String[] parts = namespace.split("\\.");
338 createObjectIfNew(objects, parts[0], true);
340 if (parts.length >= 2) {
341 StringBuilder currPrefix = new StringBuilder().append(parts[0]);
342 for (int i = 1; i < parts.length; ++i) {
343 currPrefix.append(".").append(parts[i]);
344 createObjectIfNew(objects, currPrefix.toString(), false);
348 return objects;
351 private void createObjectIfNew(List<Node> objects, String name, boolean needVar) {
352 if (!createdObjects.contains(name)) {
353 objects.add(createJsNode((needVar ? "var " : "") + name + " = " + name + " || {};"));
354 createdObjects.add(name);
358 private Node createJsNode(String code) {
359 // The parent node after parseSyntheticCode() is SCRIPT node, we need to get rid of it.
360 return compiler.parseSyntheticCode(code).removeFirstChild();
363 private class RenameInternalsToExternalsCallback extends AbstractPostOrderCallback {
364 private final String namespaceName;
365 private final Map<String, String> exports;
366 private final Node namespaceBlock;
368 public RenameInternalsToExternalsCallback(String namespaceName,
369 Map<String, String> exports, Node namespaceBlock) {
370 this.namespaceName = namespaceName;
371 this.exports = exports;
372 this.namespaceBlock = namespaceBlock;
375 @Override
376 public void visit(NodeTraversal t, Node n, Node parent) {
377 if (n.isFunction() && parent == this.namespaceBlock &&
378 this.exports.containsKey(n.getFirstChild().getString())) {
379 // It's a top-level function/constructor definition.
381 // Change
383 // /** Some doc */
384 // function internalName() {}
386 // to
388 // /** Some doc */
389 // my.namespace.name.externalName = function internalName() {};
391 // by looking up in this.exports for internalName to find the correspondent
392 // externalName.
393 Node functionTree = n.cloneTree();
394 Node exprResult = IR.exprResult(
395 IR.assign(buildQualifiedName(n.getFirstChild()), functionTree).srcref(n)
396 ).srcref(n);
398 if (n.getJSDocInfo() != null) {
399 exprResult.getFirstChild().setJSDocInfo(n.getJSDocInfo());
400 functionTree.removeProp(Node.JSDOC_INFO_PROP);
402 this.namespaceBlock.replaceChild(n, exprResult);
403 } else if (n.isName() && this.exports.containsKey(n.getString()) &&
404 !parent.isFunction()) {
405 if (parent.isVar()) {
406 if (parent.getParent() == this.namespaceBlock) {
407 // It's a top-level exported variable definition (maybe without an
408 // assignment).
409 // Change
411 // var enum = { 'one': 1, 'two': 2 };
413 // to
415 // my.namespace.name.enum = { 'one': 1, 'two': 2 };
416 Node varContent = n.removeFirstChild();
417 Node exprResult;
418 if (varContent == null) {
419 exprResult = IR.exprResult(buildQualifiedName(n)).srcref(parent);
420 } else {
421 exprResult = IR.exprResult(
422 IR.assign(buildQualifiedName(n), varContent).srcref(parent)
423 ).srcref(parent);
425 if (parent.getJSDocInfo() != null) {
426 exprResult.getFirstChild().setJSDocInfo(parent.getJSDocInfo().clone());
428 this.namespaceBlock.replaceChild(parent, exprResult);
430 } else {
431 // It's a local name referencing exported entity. Change to its global name.
432 Node newNode = buildQualifiedName(n);
433 if (n.getJSDocInfo() != null) {
434 newNode.setJSDocInfo(n.getJSDocInfo().clone());
437 // If we alter the name of a called function, then it gets an explicit "this"
438 // value.
439 if (parent.isCall()) {
440 parent.putBooleanProp(Node.FREE_CALL, false);
443 parent.replaceChild(n, newNode);
448 private Node buildQualifiedName(Node internalName) {
449 String externalName = this.exports.get(internalName.getString());
450 return NodeUtil.newQName(compiler, this.namespaceName + "." + externalName).srcrefTree(
451 internalName);