be8da2eaa45fe5f55389941158795b06da76a36e
[platform/framework/web/crosswalk.git] / src / third_party / closure_compiler / runner / src / com / google / javascript / jscomp / ChromePass.java
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
5 package com.google.javascript.jscomp;
6
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;
13
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;
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  */
31 public 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     private static final String CR_MAKE_PUBLIC = "cr.makePublic";
41
42     private static final String CR_DEFINE_COMMON_EXPLANATION = "It should be called like this:"
43             + " cr.define('name.space', function() '{ ... return {Export: Internal}; }');";
44
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);
48
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.");
52
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);
56
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);
60
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);
65
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}\".");
70
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.");
74
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.");
78
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.");
83
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"));
88     }
89
90     @Override
91     public void process(Node externs, Node root) {
92         NodeTraversal.traverse(compiler, root, this);
93     }
94
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();
112                 }
113             }
114         }
115     }
116
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";
122         }
123
124         Node property = call.getChildAtIndex(2);
125
126         Node getPropNode = NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(),
127                 target + "." + property.getString()).srcrefTree(call);
128
129         if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) {
130             setJsDocWithType(getPropNode, getTypeByCrPropertyKind(call.getChildAtIndex(3)));
131         } else {
132             setJsDocWithType(getPropNode, new Node(Token.QMARK));
133         }
134
135         Node definitionNode = IR.exprResult(getPropNode).srcref(parent);
136
137         parent.getParent().addChildAfter(definitionNode, parent);
138     }
139
140     private Node getTypeByCrPropertyKind(Node propertyKind) {
141         if (propertyKind == null || propertyKind.matchesQualifiedName("cr.PropertyKind.JS")) {
142             return new Node(Token.QMARK);
143         }
144         if (propertyKind.matchesQualifiedName("cr.PropertyKind.ATTR")) {
145             return IR.string("string");
146         }
147         if (propertyKind.matchesQualifiedName("cr.PropertyKind.BOOL_ATTR")) {
148             return IR.string("boolean");
149         }
150         compiler.report(JSError.make(propertyKind, CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND,
151                 propertyKind.getQualifiedName()));
152         return null;
153     }
154
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));
159     }
160
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);
167
168         if (methods == null || !methods.isArrayLit()) {
169             compiler.report(JSError.make(exprResult, CR_MAKE_PUBLIC_INVALID_SECOND_ARGUMENT));
170             return changesMade;
171         }
172
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;
178             }
179             methodNames.add(methodName.getString());
180         }
181
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);
189                 }
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);
201             }
202         }
203
204         for (String missedDeclaration : methodNames) {
205             compiler.report(JSError.make(exprResult, CR_MAKE_PUBLIC_MISSED_DECLARATION, className,
206                     missedDeclaration));
207         }
208
209         return changesMade;
210     }
211
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);
216     }
217
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 + ".");
222     }
223
224     private boolean isDummyPrototypeMethodDeclaration(Node node, String prototype) {
225         Node getPropNode;
226         return node.isExprResult() && (getPropNode = node.getFirstChild()).isGetProp() &&
227                 getPropNode.getQualifiedName().startsWith(prototype + ".");
228     }
229
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),
242                             exprResult);
243                     changesMade = true;
244                 } else {
245                     compiler.report(JSError.make(jsDocSourceNode, CR_MAKE_PUBLIC_HAS_NO_JSDOC));
246                 }
247                 publicAPIStrings.remove(publicName);
248             }
249         }
250         return changesMade;
251     }
252
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));
257             return;
258         }
259
260         createAndInsertObjectsForQualifiedName(parent,
261                 crExportPathNode.getChildAtIndex(1).getString());
262     }
263
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);
268         }
269     }
270
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));
274         }
275
276         Node namespaceArg = crDefineCallNode.getChildAtIndex(1);
277         Node function = crDefineCallNode.getChildAtIndex(2);
278
279         if (!namespaceArg.isString()) {
280             compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_FIRST_ARGUMENT));
281             return;
282         }
283
284         // TODO(vitalyp): Check namespace name for validity here. It should be a valid chain of
285         // identifiers.
286         String namespace = namespaceArg.getString();
287
288         createAndInsertObjectsForQualifiedName(parent, namespace);
289
290         if (!function.isFunction()) {
291             compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_SECOND_ARGUMENT));
292             return;
293         }
294
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));
302             return;
303         }
304
305         Map<String, String> exports = objectLitToMap(objectLit);
306
307         NodeTraversal.traverse(compiler, functionBlock, new RenameInternalsToExternalsCallback(
308                 namespace, exports, functionBlock));
309     }
310
311     private Map<String, String> objectLitToMap(Node objectLit) {
312         Map<String, String> res = new HashMap<String, String>();
313
314         for (Node keyNode : objectLit.children()) {
315             String key = keyNode.getString();
316
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();
320
321             res.put(value, key);
322         }
323
324         return res;
325     }
326
327     /**
328      * For a string "a.b.c" produce the following JS IR:
329      *
330      * <p><pre>
331      * var a = a || {};
332      * a.b = a.b || {};
333      * a.b.c = a.b.c || {};</pre>
334      */
335     private List<Node> createObjectsForQualifiedName(String namespace) {
336         List<Node> objects = new ArrayList<>();
337         String[] parts = namespace.split("\\.");
338
339         createObjectIfNew(objects, parts[0], true);
340
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);
346             }
347         }
348
349         return objects;
350     }
351
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);
356         }
357     }
358
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();
362     }
363
364     private class RenameInternalsToExternalsCallback extends AbstractPostOrderCallback {
365         private final String namespaceName;
366         private final Map<String, String> exports;
367         private final Node namespaceBlock;
368
369         public RenameInternalsToExternalsCallback(String namespaceName,
370                 Map<String, String> exports, Node namespaceBlock) {
371             this.namespaceName = namespaceName;
372             this.exports = exports;
373             this.namespaceBlock = namespaceBlock;
374         }
375
376         @Override
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.
381                 //
382                 // Change
383                 //
384                 //   /** Some doc */
385                 //   function internalName() {}
386                 //
387                 // to
388                 //
389                 //   /** Some doc */
390                 //   my.namespace.name.externalName = function internalName() {};
391                 //
392                 // by looking up in this.exports for internalName to find the correspondent
393                 // externalName.
394                 Node functionTree = n.cloneTree();
395                 Node exprResult = IR.exprResult(
396                             IR.assign(buildQualifiedName(n.getFirstChild()), functionTree).srcref(n)
397                         ).srcref(n);
398
399                 if (n.getJSDocInfo() != null) {
400                     exprResult.getFirstChild().setJSDocInfo(n.getJSDocInfo());
401                     functionTree.removeProp(Node.JSDOC_INFO_PROP);
402                 }
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
409                         // assignment).
410                         // Change
411                         //
412                         //   var enum = { 'one': 1, 'two': 2 };
413                         //
414                         // to
415                         //
416                         //   my.namespace.name.enum = { 'one': 1, 'two': 2 };
417                         Node varContent = n.removeFirstChild();
418                         Node exprResult;
419                         if (varContent == null) {
420                             exprResult = IR.exprResult(buildQualifiedName(n)).srcref(parent);
421                         } else {
422                             exprResult = IR.exprResult(
423                                         IR.assign(buildQualifiedName(n), varContent).srcref(parent)
424                                     ).srcref(parent);
425                         }
426                         if (parent.getJSDocInfo() != null) {
427                             exprResult.getFirstChild().setJSDocInfo(parent.getJSDocInfo().clone());
428                         }
429                         this.namespaceBlock.replaceChild(parent, exprResult);
430                     }
431                 } else {
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());
436                     }
437
438                     // If we alter the name of a called function, then it gets an explicit "this"
439                     // value.
440                     if (parent.isCall()) {
441                         parent.putBooleanProp(Node.FREE_CALL, false);
442                     }
443
444                     parent.replaceChild(n, newNode);
445                 }
446             }
447         }
448
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);
453         }
454     }
455 }