Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(887)

Side by Side Diff: third_party/closure_compiler/runner/src/com/google/javascript/jscomp/ChromePass.java

Issue 460163002: Handle property definition by {cr|Object}.defineProperty() in compiler pass (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@C_compiler_pass
Patch Set: rebase onto master Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2014 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 package com.google.javascript.jscomp; 5 package com.google.javascript.jscomp;
6 6
7 import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; 7 import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
8 import com.google.javascript.rhino.IR; 8 import com.google.javascript.rhino.IR;
9 import com.google.javascript.rhino.JSDocInfoBuilder;
10 import com.google.javascript.rhino.JSTypeExpression;
9 import com.google.javascript.rhino.Node; 11 import com.google.javascript.rhino.Node;
12 import com.google.javascript.rhino.Token;
10 13
11 import java.util.ArrayList; 14 import java.util.ArrayList;
12 import java.util.HashMap; 15 import java.util.HashMap;
13 import java.util.List; 16 import java.util.List;
14 import java.util.Map; 17 import java.util.Map;
15 18
16 /** 19 /**
17 * Compiler pass for Chrome-specific needs. Right now it allows the compiler to check types with 20 * Compiler pass for Chrome-specific needs. It handles the following Chrome JS f eatures:
18 * methods defined inside Chrome namespaces. 21 * <ul>
22 * <li>namespace declaration using {@code cr.define()},
23 * <li>unquoted property declaration using {@code {cr|Object}.defineProperty()}.
24 * </ul>
19 * 25 *
20 * <p>The namespaces in Chrome JS are declared as follows: 26 * <p>For the details, see tests inside ChromePassTest.java.
21 * <pre>
22 * cr.define('my.namespace.name', function() {
23 * /** @param {number} arg
24 * function internalStaticMethod(arg) {}
25 *
26 * /** @constructor
27 * function InternalClass() {}
28 *
29 * InternalClass.prototype = {
30 * /** @param {number} arg
31 * method: function(arg) {
32 * internalStaticMethod(arg); // let's demonstrate the change of local na mes after our pass
33 * }
34 * };
35 *
36 * return {
37 * externalStaticMethod: internalStaticMethod,
38 * ExternalClass: InternalClass
39 * }
40 * });
41 * </pre>
42 *
43 * <p>Outside of cr.define() statement the function can be called like this:
44 * {@code my.namespace.name.externalStaticMethod(42);}.
45 *
46 * <p>We need to check the types of arguments and return values of such function s. However, the
47 * function is assigned to its namespace dictionary only at run-time and the ori ginal Closure
48 * Compiler isn't smart enough to predict behavior in that case. Therefore, we n eed to modify the
49 * AST before any compiler checks. That's how we modify it to tell the compiler what's going on:
50 *
51 * <pre>
52 * var my = my || {};
53 * my.namespace = my.namespace || {};
54 * my.namespace.name = my.namespace.name || {};
55 *
56 * cr.define('my.namespace.name', function() {
57 * /** @param {number} arg
58 * my.namespace.name.externalStaticMethod(arg) {}
59 *
60 * /** @constructor
61 * my.namespace.name.ExternalClass = function() {};
62 *
63 * my.namespace.name.ExternalClass.prototype = {
64 * /** @param {number} arg
65 * method: function(arg) {
66 * my.namespace.name.externalStaticMethod(arg); // see, it has been chang ed
67 * }
68 * };
69 *
70 * return {
71 * externalStaticMethod: my.namespace.name.externalStaticMethod,
72 * ExternalClass: my.namespace.name.ExternalClass
73 * }
74 * });
75 * </pre>
76 */ 27 */
77 public class ChromePass extends AbstractPostOrderCallback implements CompilerPas s { 28 public class ChromePass extends AbstractPostOrderCallback implements CompilerPas s {
78 final AbstractCompiler compiler; 29 final AbstractCompiler compiler;
79 30
80 private static final String CR_DEFINE = "cr.define"; 31 private static final String CR_DEFINE = "cr.define";
81 32
82 private static final String CR_DEFINE_COMMON_EXPLANATION = "It should be cal led like this: " + 33 private static final String OBJECT_DEFINE_PROPERTY = "Object.defineProperty" ;
83 "cr.define('name.space', function() '{ ... return {Export: Internal} ; }');"; 34
35 private static final String CR_DEFINE_PROPERTY = "cr.defineProperty";
36
37 private static final String CR_DEFINE_COMMON_EXPLANATION = "It should be cal led like this:"
38 + " cr.define('name.space', function() '{ ... return {Export: Intern al}; }');";
84 39
85 static final DiagnosticType CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS = 40 static final DiagnosticType CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS =
86 DiagnosticType.error("JSC_CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS", 41 DiagnosticType.error("JSC_CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS",
87 "cr.define() should have exactly 2 arguments. " + CR_DEFINE_ COMMON_EXPLANATION); 42 "cr.define() should have exactly 2 arguments. " + CR_DEFINE_ COMMON_EXPLANATION);
88 43
89 static final DiagnosticType CR_DEFINE_INVALID_FIRST_ARGUMENT = 44 static final DiagnosticType CR_DEFINE_INVALID_FIRST_ARGUMENT =
90 DiagnosticType.error("JSC_CR_DEFINE_INVALID_FIRST_ARGUMENT", 45 DiagnosticType.error("JSC_CR_DEFINE_INVALID_FIRST_ARGUMENT",
91 "Invalid first argument for cr.define(). " + CR_DEFINE_COMMO N_EXPLANATION); 46 "Invalid first argument for cr.define(). " + CR_DEFINE_COMMO N_EXPLANATION);
92 47
93 static final DiagnosticType CR_DEFINE_INVALID_SECOND_ARGUMENT = 48 static final DiagnosticType CR_DEFINE_INVALID_SECOND_ARGUMENT =
94 DiagnosticType.error("JSC_CR_DEFINE_INVALID_SECOND_ARGUMENT", 49 DiagnosticType.error("JSC_CR_DEFINE_INVALID_SECOND_ARGUMENT",
95 "Invalid second argument for cr.define(). " + CR_DEFINE_COMM ON_EXPLANATION); 50 "Invalid second argument for cr.define(). " + CR_DEFINE_COMM ON_EXPLANATION);
96 51
97 static final DiagnosticType CR_DEFINE_INVALID_RETURN_IN_FUNCTION = 52 static final DiagnosticType CR_DEFINE_INVALID_RETURN_IN_FUNCTION =
98 DiagnosticType.error("JSC_CR_DEFINE_INVALID_RETURN_IN_SECOND_ARGUMEN T", 53 DiagnosticType.error("JSC_CR_DEFINE_INVALID_RETURN_IN_SECOND_ARGUMEN T",
99 "Function passed as second argument of cr.define() should re turn the " + 54 "Function passed as second argument of cr.define() should re turn the"
100 "dictionary in its last statement. " + CR_DEFINE_COMMON_EXPL ANATION); 55 + " dictionary in its last statement. " + CR_DEFINE_COMMON_E XPLANATION);
56
57 static final DiagnosticType CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND =
58 DiagnosticType.error("JSC_CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND",
59 "Invalid cr.PropertyKind passed to cr.defineProperty(): expe cted ATTR,"
60 + " BOOL_ATTR or JS, found \"{0}\".");
101 61
102 public ChromePass(AbstractCompiler compiler) { 62 public ChromePass(AbstractCompiler compiler) {
103 this.compiler = compiler; 63 this.compiler = compiler;
104 } 64 }
105 65
106 @Override 66 @Override
107 public void process(Node externs, Node root) { 67 public void process(Node externs, Node root) {
108 NodeTraversal.traverse(compiler, root, this); 68 NodeTraversal.traverse(compiler, root, this);
109 } 69 }
110 70
111 @Override 71 @Override
112 public void visit(NodeTraversal t, Node node, Node parent) { 72 public void visit(NodeTraversal t, Node node, Node parent) {
113 if (node.isCall()) { 73 if (node.isCall()) {
114 Node callee = node.getFirstChild(); 74 Node callee = node.getFirstChild();
115 if (callee.matchesQualifiedName(CR_DEFINE)) { 75 if (callee.matchesQualifiedName(CR_DEFINE)) {
116 visitNamespaceDefinition(node, parent); 76 visitNamespaceDefinition(node, parent);
117 compiler.reportCodeChange(); 77 compiler.reportCodeChange();
78 } else if (callee.matchesQualifiedName(OBJECT_DEFINE_PROPERTY) ||
79 callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) {
80 visitPropertyDefinition(node, parent);
81 compiler.reportCodeChange();
118 } 82 }
119 } 83 }
120 } 84 }
121 85
86 private void visitPropertyDefinition(Node call, Node parent) {
87 Node callee = call.getFirstChild();
88 String target = call.getChildAtIndex(1).getQualifiedName();
89 if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY) && !target.endsWith( ".prototype")) {
90 target += ".prototype";
91 }
92
93 Node property = call.getChildAtIndex(2);
94
95 Node getPropNode = NodeUtil.newQualifiedNameNode(compiler.getCodingConve ntion(),
96 target + "." + property.getString()).srcrefTree(call);
97
98 if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) {
99 setJsDocWithType(getPropNode, getTypeByCrPropertyKind(call.getChildA tIndex(3)));
100 } else {
101 setJsDocWithType(getPropNode, new Node(Token.QMARK));
102 }
103
104 Node definitionNode = IR.exprResult(getPropNode).srcref(parent);
105
106 parent.getParent().addChildAfter(definitionNode, parent);
107 }
108
109 private Node getTypeByCrPropertyKind(Node propertyKind) {
110 if (propertyKind.matchesQualifiedName("cr.PropertyKind.ATTR")) {
111 return IR.string("string");
112 }
113 if (propertyKind.matchesQualifiedName("cr.PropertyKind.BOOL_ATTR")) {
114 return IR.string("boolean");
115 }
116 if (propertyKind.matchesQualifiedName("cr.PropertyKind.JS")) {
117 return new Node(Token.QMARK);
118 }
119 compiler.report(JSError.make(propertyKind, CR_DEFINE_PROPERTY_INVALID_PR OPERTY_KIND,
120 propertyKind.getQualifiedName()));
121 return null;
122 }
123
124 private void setJsDocWithType(Node target, Node type) {
125 JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
126 builder.recordType(new JSTypeExpression(type, ""));
127 target.setJSDocInfo(builder.build(target));
128 }
129
122 private void visitNamespaceDefinition(Node crDefineCallNode, Node parent) { 130 private void visitNamespaceDefinition(Node crDefineCallNode, Node parent) {
123 if (crDefineCallNode.getChildCount() != 3) { 131 if (crDefineCallNode.getChildCount() != 3) {
124 compiler.report(JSError.make(crDefineCallNode, CR_DEFINE_WRONG_NUMBE R_OF_ARGUMENTS)); 132 compiler.report(JSError.make(crDefineCallNode, CR_DEFINE_WRONG_NUMBE R_OF_ARGUMENTS));
125 } 133 }
126 134
127 Node namespaceArg = crDefineCallNode.getChildAtIndex(1); 135 Node namespaceArg = crDefineCallNode.getChildAtIndex(1);
128 Node function = crDefineCallNode.getChildAtIndex(2); 136 Node function = crDefineCallNode.getChildAtIndex(2);
129 137
130 if (!namespaceArg.isString()) { 138 if (!namespaceArg.isString()) {
131 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_FIRST_A RGUMENT)); 139 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_FIRST_A RGUMENT));
132 return; 140 return;
133 } 141 }
134 142
135 // TODO(vitalyp): Check namespace name for validity here. It should be a valid chain of 143 // TODO(vitalyp): Check namespace name for validity here. It should be a valid chain of
136 // identifiers. 144 // identifiers.
137 String namespace = namespaceArg.getString(); 145 String namespace = namespaceArg.getString();
138 146
139 List<Node> objectsForQualifiedName = createObjectsForQualifiedName(names pace); 147 List<Node> objectsForQualifiedName = createObjectsForQualifiedName(names pace);
140 for (Node n : objectsForQualifiedName) { 148 for (Node n : objectsForQualifiedName) {
141 parent.getParent().addChildBefore(n, parent); 149 parent.getParent().addChildBefore(n, parent);
142 } 150 }
143 151
144 Node returnNode, functionBlock, objectLit;
145 if (!function.isFunction()) { 152 if (!function.isFunction()) {
146 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_SECOND_ ARGUMENT)); 153 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_SECOND_ ARGUMENT));
147 return; 154 return;
148 } 155 }
149 156
150 if ((functionBlock = function.getLastChild()) == null || 157 Node returnNode, objectLit;
151 (returnNode = functionBlock.getLastChild()) == null || 158 Node functionBlock = function.getLastChild();
159 if ((returnNode = functionBlock.getLastChild()) == null ||
152 !returnNode.isReturn() || 160 !returnNode.isReturn() ||
153 (objectLit = returnNode.getFirstChild()) == null || 161 (objectLit = returnNode.getFirstChild()) == null ||
154 !objectLit.isObjectLit()) { 162 !objectLit.isObjectLit()) {
155 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_RETURN_ IN_FUNCTION)); 163 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_RETURN_ IN_FUNCTION));
156 return; 164 return;
157 } 165 }
158 166
159 Map<String, String> exports = objectLitToMap(objectLit); 167 Map<String, String> exports = objectLitToMap(objectLit);
160 168
161 NodeTraversal.traverse(compiler, functionBlock, new RenameInternalsToExt ernalsCallback( 169 NodeTraversal.traverse(compiler, functionBlock, new RenameInternalsToExt ernalsCallback(
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
203 211
204 return objects; 212 return objects;
205 } 213 }
206 214
207 private Node createJsNode(String code) { 215 private Node createJsNode(String code) {
208 // The parent node after parseSyntheticCode() is SCRIPT node, we need to get rid of it. 216 // The parent node after parseSyntheticCode() is SCRIPT node, we need to get rid of it.
209 return compiler.parseSyntheticCode(code).removeFirstChild(); 217 return compiler.parseSyntheticCode(code).removeFirstChild();
210 } 218 }
211 219
212 private class RenameInternalsToExternalsCallback extends AbstractPostOrderCa llback { 220 private class RenameInternalsToExternalsCallback extends AbstractPostOrderCa llback {
213 final private String namespaceName; 221 private final String namespaceName;
214 final private Map<String, String> exports; 222 private final Map<String, String> exports;
215 final private Node namespaceBlock; 223 private final Node namespaceBlock;
216 224
217 public RenameInternalsToExternalsCallback(String namespaceName, 225 public RenameInternalsToExternalsCallback(String namespaceName,
218 Map<String, String> exports, Node namespaceBlock) { 226 Map<String, String> exports, Node namespaceBlock) {
219 this.namespaceName = namespaceName; 227 this.namespaceName = namespaceName;
220 this.exports = exports; 228 this.exports = exports;
221 this.namespaceBlock = namespaceBlock; 229 this.namespaceBlock = namespaceBlock;
222 } 230 }
223 231
224 @Override 232 @Override
225 public void visit(NodeTraversal t, Node n, Node parent) { 233 public void visit(NodeTraversal t, Node n, Node parent) {
226 if (n == null) {
227 return;
228 }
229
230 if (n.isFunction() && parent == this.namespaceBlock && 234 if (n.isFunction() && parent == this.namespaceBlock &&
231 this.exports.containsKey(n.getFirstChild().getString())) { 235 this.exports.containsKey(n.getFirstChild().getString())) {
232 // It's a top-level function/constructor definition. 236 // It's a top-level function/constructor definition.
233 // 237 //
234 // Change 238 // Change
235 // 239 //
236 // /** Some doc */ 240 // /** Some doc */
237 // function internalName() {} 241 // function internalName() {}
238 // 242 //
239 // to 243 // to
240 // 244 //
241 // /** Some doc */ 245 // /** Some doc */
242 // my.namespace.name.externalName = function internalName() {} ; 246 // my.namespace.name.externalName = function internalName() {} ;
243 // 247 //
244 // by looking up in this.exports for internalName to find the co rrespondent 248 // by looking up in this.exports for internalName to find the co rrespondent
245 // externalName. 249 // externalName.
246 Node functionTree = n.cloneTree(); 250 Node functionTree = n.cloneTree();
247 Node exprResult = IR.exprResult( 251 Node exprResult = IR.exprResult(
248 IR.assign(buildQualifiedName(n.getFirstChild()), functio nTree)); 252 IR.assign(buildQualifiedName(n.getFirstChild()), fun ctionTree).srcref(n)
249 NodeUtil.setDebugInformation(exprResult, n, n.getFirstChild().ge tString()); 253 ).srcref(n);
254
250 if (n.getJSDocInfo() != null) { 255 if (n.getJSDocInfo() != null) {
251 exprResult.getFirstChild().setJSDocInfo(n.getJSDocInfo()); 256 exprResult.getFirstChild().setJSDocInfo(n.getJSDocInfo());
252 functionTree.removeProp(Node.JSDOC_INFO_PROP); 257 functionTree.removeProp(Node.JSDOC_INFO_PROP);
253 } 258 }
254 this.namespaceBlock.replaceChild(n, exprResult); 259 this.namespaceBlock.replaceChild(n, exprResult);
255 } else if (n.isName() && this.exports.containsKey(n.getString()) && 260 } else if (n.isName() && this.exports.containsKey(n.getString()) &&
256 !parent.isFunction()) { 261 !parent.isFunction()) {
257 if (parent.isVar()) { 262 if (parent.isVar()) {
258 if (parent.getParent() == this.namespaceBlock) { 263 if (parent.getParent() == this.namespaceBlock) {
259 // It's a top-level exported variable definition. 264 // It's a top-level exported variable definition.
260 // Change 265 // Change
261 // 266 //
262 // var enum = { 'one': 1, 'two': 2 }; 267 // var enum = { 'one': 1, 'two': 2 };
263 // 268 //
264 // to 269 // to
265 // 270 //
266 // my.namespace.name.enum = { 'one': 1, 'two': 2 }; 271 // my.namespace.name.enum = { 'one': 1, 'two': 2 };
267 Node varContent = n.removeFirstChild(); 272 Node varContent = n.removeFirstChild();
268 Node exprResult = IR.exprResult( 273 Node exprResult = IR.exprResult(
269 IR.assign(buildQualifiedName(n), varContent)); 274 IR.assign(buildQualifiedName(n), varContent) .srcref(parent)
270 NodeUtil.setDebugInformation(exprResult, parent, n.getSt ring()); 275 ).srcref(parent);
276
271 if (parent.getJSDocInfo() != null) { 277 if (parent.getJSDocInfo() != null) {
272 exprResult.getFirstChild().setJSDocInfo(parent.getJS DocInfo().clone()); 278 exprResult.getFirstChild().setJSDocInfo(parent.getJS DocInfo().clone());
273 } 279 }
274 this.namespaceBlock.replaceChild(parent, exprResult); 280 this.namespaceBlock.replaceChild(parent, exprResult);
275 } 281 }
276 } else { 282 } else {
277 // It's a local name referencing exported entity. Change to its global name. 283 // It's a local name referencing exported entity. Change to its global name.
278 Node newNode = buildQualifiedName(n); 284 Node newNode = buildQualifiedName(n);
279 if (n.getJSDocInfo() != null) { 285 if (n.getJSDocInfo() != null) {
280 newNode.setJSDocInfo(n.getJSDocInfo().clone()); 286 newNode.setJSDocInfo(n.getJSDocInfo().clone());
281 } 287 }
282 288
283 // If we alter the name of a called function, then it gets a n explicit "this" 289 // If we alter the name of a called function, then it gets a n explicit "this"
284 // value. 290 // value.
285 if (parent.isCall()) { 291 if (parent.isCall()) {
286 parent.putBooleanProp(Node.FREE_CALL, false); 292 parent.putBooleanProp(Node.FREE_CALL, false);
287 } 293 }
288 294
289 parent.replaceChild(n, newNode); 295 parent.replaceChild(n, newNode);
290 } 296 }
291 } 297 }
292 } 298 }
293 299
294 private Node buildQualifiedName(Node internalName) { 300 private Node buildQualifiedName(Node internalName) {
295 String externalName = this.exports.get(internalName.getString()); 301 String externalName = this.exports.get(internalName.getString());
296 return NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), 302 return NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(),
297 this.namespaceName + "." + exte rnalName, 303 this.namespaceName + "." + externalName).srcrefTree(internal Name);
298 internalName,
299 internalName.getString());
300 } 304 }
301 } 305 }
306
302 } 307 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698