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

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: 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
33 private static final String OBJECT_DEFINE_PROPERTY = "Object.defineProperty" ;
34
35 private static final String CR_DEFINE_PROPERTY = "cr.defineProperty";
36
82 private static final String CR_DEFINE_COMMON_EXPLANATION = "It should be cal led like this: " + 37 private static final String CR_DEFINE_COMMON_EXPLANATION = "It should be cal led like this: " +
83 "cr.define('name.space', function() '{ ... return {Export: Internal} ; }');"; 38 "cr.define('name.space', function() '{ ... return {Export: Internal} ; }');";
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_EXPL ANATION);
101 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, " +
Tyler Breisacher (Chromium) 2014/08/12 15:57:29 If you define PropertyKind as an @enum then the ty
Vitaly Pavlenko 2014/08/12 17:44:17 Acknowledged.
60 "BOOL_ATTR or JS, found \"{0}\".");
Tyler Breisacher (Chromium) 2014/08/12 15:57:29 nit: Put the space and the '+' on the next line:
Vitaly Pavlenko 2014/08/12 17:44:17 How does one select between two possibilities?
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 Node target = call.getChildAtIndex(1);
89 Node property = call.getChildAtIndex(2);
90
91 Node getPropNode = NodeUtil.newQualifiedNameNode(compiler.getCodingConve ntion(),
92 target.getQualifiedName() + "." + property.getString());
93
94 if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) {
95 setJsDocWithType(getPropNode, getTypeByCrPropertyKind(call.getChildA tIndex(3)));
96 } else {
97 setJsDocWithType(getPropNode, new Node(Token.STAR));
98 }
99
100 Node definitionNode = IR.exprResult(getPropNode).srcrefTree(parent);
101
102 parent.getParent().addChildAfter(definitionNode, parent);
103 }
104
105 private Node getTypeByCrPropertyKind(Node propertyKind) {
106 if (propertyKind.matchesQualifiedName("cr.PropertyKind.ATTR")) {
107 return IR.string("string");
108 } else if (propertyKind.matchesQualifiedName("cr.PropertyKind.BOOL_ATTR" )) {
109 return IR.string("boolean");
110 } else if (propertyKind.matchesQualifiedName("cr.PropertyKind.JS")) {
111 return new Node(Token.STAR);
Tyler Breisacher (Chromium) 2014/08/12 16:32:49 I was discussing this with dimvar@ a little bit an
Vitaly Pavlenko 2014/08/12 17:44:17 Done.
112 } else {
113 compiler.report(JSError.make(propertyKind, CR_DEFINE_PROPERTY_INVALI D_PROPERTY_KIND,
114 propertyKind.getQualifiedName()));
115 return null;
116 }
117 }
118
119 private void setJsDocWithType(Node target, Node type) {
120 JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
121 builder.recordType(new JSTypeExpression(type, ""));
122 target.setJSDocInfo(builder.build(target));
123 }
124
122 private void visitNamespaceDefinition(Node crDefineCallNode, Node parent) { 125 private void visitNamespaceDefinition(Node crDefineCallNode, Node parent) {
123 if (crDefineCallNode.getChildCount() != 3) { 126 if (crDefineCallNode.getChildCount() != 3) {
124 compiler.report(JSError.make(crDefineCallNode, CR_DEFINE_WRONG_NUMBE R_OF_ARGUMENTS)); 127 compiler.report(JSError.make(crDefineCallNode, CR_DEFINE_WRONG_NUMBE R_OF_ARGUMENTS));
125 } 128 }
126 129
127 Node namespaceArg = crDefineCallNode.getChildAtIndex(1); 130 Node namespaceArg = crDefineCallNode.getChildAtIndex(1);
128 Node function = crDefineCallNode.getChildAtIndex(2); 131 Node function = crDefineCallNode.getChildAtIndex(2);
129 132
130 if (!namespaceArg.isString()) { 133 if (!namespaceArg.isString()) {
131 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_FIRST_A RGUMENT)); 134 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_FIRST_A RGUMENT));
(...skipping 106 matching lines...) Expand 10 before | Expand all | Expand 10 after
238 // 241 //
239 // to 242 // to
240 // 243 //
241 // /** Some doc */ 244 // /** Some doc */
242 // my.namespace.name.externalName = function internalName() {} ; 245 // my.namespace.name.externalName = function internalName() {} ;
243 // 246 //
244 // by looking up in this.exports for internalName to find the co rrespondent 247 // by looking up in this.exports for internalName to find the co rrespondent
245 // externalName. 248 // externalName.
246 Node functionTree = n.cloneTree(); 249 Node functionTree = n.cloneTree();
247 Node exprResult = IR.exprResult( 250 Node exprResult = IR.exprResult(
248 IR.assign(buildQualifiedName(n.getFirstChild()), functio nTree)); 251 IR.assign(buildQualifiedName(n.getFirstChild()), fun ctionTree)
249 NodeUtil.setDebugInformation(exprResult, n, n.getFirstChild().ge tString()); 252 ).srcrefTree(n);
253
250 if (n.getJSDocInfo() != null) { 254 if (n.getJSDocInfo() != null) {
251 exprResult.getFirstChild().setJSDocInfo(n.getJSDocInfo()); 255 exprResult.getFirstChild().setJSDocInfo(n.getJSDocInfo());
252 functionTree.removeProp(Node.JSDOC_INFO_PROP); 256 functionTree.removeProp(Node.JSDOC_INFO_PROP);
253 } 257 }
254 this.namespaceBlock.replaceChild(n, exprResult); 258 this.namespaceBlock.replaceChild(n, exprResult);
255 } else if (n.isName() && this.exports.containsKey(n.getString()) && 259 } else if (n.isName() && this.exports.containsKey(n.getString()) &&
256 !parent.isFunction()) { 260 !parent.isFunction()) {
257 if (parent.isVar()) { 261 if (parent.isVar()) {
258 if (parent.getParent() == this.namespaceBlock) { 262 if (parent.getParent() == this.namespaceBlock) {
259 // It's a top-level exported variable definition. 263 // It's a top-level exported variable definition.
260 // Change 264 // Change
261 // 265 //
262 // var enum = { 'one': 1, 'two': 2 }; 266 // var enum = { 'one': 1, 'two': 2 };
263 // 267 //
264 // to 268 // to
265 // 269 //
266 // my.namespace.name.enum = { 'one': 1, 'two': 2 }; 270 // my.namespace.name.enum = { 'one': 1, 'two': 2 };
267 Node varContent = n.removeFirstChild(); 271 Node varContent = n.removeFirstChild();
268 Node exprResult = IR.exprResult( 272 Node exprResult = IR.exprResult(
269 IR.assign(buildQualifiedName(n), varContent)); 273 IR.assign(buildQualifiedName(n), varContent)).sr crefTree(parent);
270 NodeUtil.setDebugInformation(exprResult, parent, n.getSt ring()); 274
271 if (parent.getJSDocInfo() != null) { 275 if (parent.getJSDocInfo() != null) {
272 exprResult.getFirstChild().setJSDocInfo(parent.getJS DocInfo().clone()); 276 exprResult.getFirstChild().setJSDocInfo(parent.getJS DocInfo().clone());
273 } 277 }
274 this.namespaceBlock.replaceChild(parent, exprResult); 278 this.namespaceBlock.replaceChild(parent, exprResult);
275 } 279 }
276 } else { 280 } else {
277 // It's a local name referencing exported entity. Change to its global name. 281 // It's a local name referencing exported entity. Change to its global name.
278 Node newNode = buildQualifiedName(n); 282 Node newNode = buildQualifiedName(n);
279 if (n.getJSDocInfo() != null) { 283 if (n.getJSDocInfo() != null) {
280 newNode.setJSDocInfo(n.getJSDocInfo().clone()); 284 newNode.setJSDocInfo(n.getJSDocInfo().clone());
281 } 285 }
282 286
283 // If we alter the name of a called function, then it gets a n explicit "this" 287 // If we alter the name of a called function, then it gets a n explicit "this"
284 // value. 288 // value.
285 if (parent.isCall()) { 289 if (parent.isCall()) {
286 parent.putBooleanProp(Node.FREE_CALL, false); 290 parent.putBooleanProp(Node.FREE_CALL, false);
287 } 291 }
288 292
289 parent.replaceChild(n, newNode); 293 parent.replaceChild(n, newNode);
290 } 294 }
291 } 295 }
292 } 296 }
293 297
294 private Node buildQualifiedName(Node internalName) { 298 private Node buildQualifiedName(Node internalName) {
295 String externalName = this.exports.get(internalName.getString()); 299 String externalName = this.exports.get(internalName.getString());
296 return NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), 300 return NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(),
297 this.namespaceName + "." + exte rnalName, 301 this.namespaceName + "." + externalName).srcrefTree(internal Name);
298 internalName,
299 internalName.getString());
300 } 302 }
301 } 303 }
304
302 } 305 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698