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
.newQName(
127 compiler
, 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
.newQName(compiler
, className
+ "." + publicName
);
237 if (jsDocSourceNode
.getJSDocInfo() != null) {
238 methodDeclaration
.setJSDocInfo(jsDocSourceNode
.getJSDocInfo());
239 scope
.addChildBefore(
240 IR
.exprResult(methodDeclaration
).srcrefTree(exprResult
),
244 compiler
.report(JSError
.make(jsDocSourceNode
, CR_MAKE_PUBLIC_HAS_NO_JSDOC
));
246 publicAPIStrings
.remove(publicName
);
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
));
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
));
283 // TODO(vitalyp): Check namespace name for validity here. It should be a valid chain of
285 String namespace
= namespaceArg
.getString();
287 createAndInsertObjectsForQualifiedName(parent
, namespace
);
289 if (!function
.isFunction()) {
290 compiler
.report(JSError
.make(namespaceArg
, CR_DEFINE_INVALID_SECOND_ARGUMENT
));
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
));
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();
327 * For a string "a.b.c" produce the following JS IR:
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);
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
;
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.
384 // function internalName() {}
389 // my.namespace.name.externalName = function internalName() {};
391 // by looking up in this.exports for internalName to find the correspondent
393 Node functionTree
= n
.cloneTree();
394 Node exprResult
= IR
.exprResult(
395 IR
.assign(buildQualifiedName(n
.getFirstChild()), functionTree
).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
411 // var enum = { 'one': 1, 'two': 2 };
415 // my.namespace.name.enum = { 'one': 1, 'two': 2 };
416 Node varContent
= n
.removeFirstChild();
418 if (varContent
== null) {
419 exprResult
= IR
.exprResult(buildQualifiedName(n
)).srcref(parent
);
421 exprResult
= IR
.exprResult(
422 IR
.assign(buildQualifiedName(n
), varContent
).srcref(parent
)
425 if (parent
.getJSDocInfo() != null) {
426 exprResult
.getFirstChild().setJSDocInfo(parent
.getJSDocInfo().clone());
428 this.namespaceBlock
.replaceChild(parent
, exprResult
);
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"
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(