1 | // Copyright (C) 2009 Google Inc. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | package com.google.caja.parser.quasiliteral; |
16 | |
17 | import com.google.caja.SomethingWidgyHappenedError; |
18 | import com.google.caja.lexer.FilePosition; |
19 | import com.google.caja.parser.ParseTreeNode; |
20 | import com.google.caja.parser.ParseTreeNodeContainer; |
21 | import com.google.caja.parser.js.Block; |
22 | import com.google.caja.parser.js.CatchStmt; |
23 | import com.google.caja.parser.js.Declaration; |
24 | import com.google.caja.parser.js.Expression; |
25 | import com.google.caja.parser.js.ExpressionStmt; |
26 | import com.google.caja.parser.js.FormalParam; |
27 | import com.google.caja.parser.js.FunctionConstructor; |
28 | import com.google.caja.parser.js.FunctionDeclaration; |
29 | import com.google.caja.parser.js.Identifier; |
30 | import com.google.caja.parser.js.MultiDeclaration; |
31 | import com.google.caja.parser.js.NullLiteral; |
32 | import com.google.caja.parser.js.Reference; |
33 | import com.google.caja.parser.js.Statement; |
34 | import com.google.caja.reporting.MessagePart; |
35 | import com.google.caja.reporting.MessageQueue; |
36 | import com.google.caja.util.Lists; |
37 | import com.google.caja.util.Maps; |
38 | |
39 | import java.util.Collections; |
40 | import java.util.List; |
41 | import java.util.Map; |
42 | |
43 | final class AlphaRenamingRewriter extends Rewriter { |
44 | private final Map<Scope, NameContext<String, ?>> contexts |
45 | = Maps.newIdentityHashMap(); |
46 | |
47 | AlphaRenamingRewriter( |
48 | final MessageQueue mq, final NameContext<String, ?> rootContext) { |
49 | super(mq, false, false); |
50 | |
51 | addRules(new Rule[] { |
52 | ///////////////// |
53 | // Scope Rules // |
54 | ///////////////// |
55 | |
56 | new Rule() { |
57 | @Override |
58 | @RuleDescription( |
59 | name="rootScope", |
60 | synopsis="introduces a root scope", |
61 | reason="lets us rename globals", |
62 | matches="/* Expression */ @e", |
63 | substitutes="@e") |
64 | public ParseTreeNode fire(ParseTreeNode node, Scope scope) { |
65 | if (node instanceof Expression && scope == null) { |
66 | Expression e = (Expression) node; |
67 | Block bl = new Block( |
68 | e.getFilePosition(), |
69 | Collections.singletonList(new ExpressionStmt(e))); |
70 | scope = Scope.fromProgram(bl, mq); |
71 | contexts.put(scope, rootContext); |
72 | return expand(e, scope); |
73 | } |
74 | return NONE; |
75 | } |
76 | }, |
77 | |
78 | new Rule() { |
79 | @Override |
80 | @RuleDescription( |
81 | name="fns", |
82 | synopsis=("introduces function scope and assigns rewritten names" |
83 | + " for function names, formals, and locals"), |
84 | reason="", |
85 | matches="function @name?(@params*) { @body* }", |
86 | substitutes="function @name?(@params*) { @headDecls?; @body* }") |
87 | public ParseTreeNode fire(ParseTreeNode node, Scope scope) { |
88 | FunctionConstructor fc; |
89 | if (node instanceof FunctionConstructor) { |
90 | fc = (FunctionConstructor) node; |
91 | } else if (node instanceof FunctionDeclaration) { |
92 | fc = ((FunctionDeclaration) node).getInitializer(); |
93 | } else { |
94 | return NONE; |
95 | } |
96 | Map<String, ParseTreeNode> bindings = match(fc); |
97 | if (bindings != null) { |
98 | boolean isDeclaration = fc != node; |
99 | NameContext<String, ?> context = contexts.get(scope); |
100 | NameContext<String, ?> newContext = context.makeChildContext(); |
101 | Scope newScope = Scope.fromFunctionConstructor(scope, fc); |
102 | List<Declaration> headDecls = Lists.newArrayList(); |
103 | |
104 | if (newScope.hasFreeThis()) { |
105 | NameContext.VarInfo<String, ?> vi; |
106 | try { |
107 | vi = newContext.declare("this", FilePosition.UNKNOWN); |
108 | } catch (NameContext.RedeclarationException ex) { |
109 | throw new SomethingWidgyHappenedError( |
110 | "Local variable unexpectedly not set", ex); |
111 | } |
112 | headDecls.add((Declaration) QuasiBuilder.substV( |
113 | "var @newName = this", |
114 | "newName", new Identifier(FilePosition.UNKNOWN, vi.newName))); |
115 | } |
116 | if (newScope.hasFreeArguments()) { |
117 | NameContext.VarInfo<String, ?> vi; |
118 | try { |
119 | vi = newContext.declare("arguments", FilePosition.UNKNOWN); |
120 | } catch (NameContext.RedeclarationException ex) { |
121 | throw new SomethingWidgyHappenedError( |
122 | "Local variable unexpectedly not set", ex); |
123 | } |
124 | headDecls.add((Declaration) QuasiBuilder.substV( |
125 | "var @newName = arguments", |
126 | "newName", new Identifier(FilePosition.UNKNOWN, vi.newName))); |
127 | } |
128 | |
129 | for (String local : newScope.getLocals()) { |
130 | try { |
131 | newContext.declare( |
132 | local, newScope.getLocationOfDeclaration(local)); |
133 | } catch (NameContext.RedeclarationException ex) { |
134 | // Might occur if a var named arguments is defined. |
135 | } |
136 | } |
137 | contexts.put(newScope, newContext); |
138 | |
139 | Identifier name = fc.getIdentifier(); |
140 | Identifier rewrittenName; |
141 | if (name.getName() == null) { |
142 | rewrittenName = name; |
143 | } else if (!isSynthetic(name)) { |
144 | rewrittenName = new Identifier( |
145 | name.getFilePosition(), |
146 | (isDeclaration ? context : newContext) |
147 | .lookup(name.getName()).newName); |
148 | } else { |
149 | rewrittenName = name; |
150 | } |
151 | |
152 | List<FormalParam> newFormals = Lists.newArrayList(); |
153 | for (FormalParam p : fc.getParams()) { |
154 | if (!isSynthetic(p.getIdentifier())) { |
155 | NameContext.VarInfo<String, ?> v |
156 | = newContext.lookup(p.getIdentifierName()); |
157 | if (v == null) { |
158 | // Occurs when an invalid parameter appears, |
159 | // e.g., function (arguments) { ... } |
160 | try { |
161 | v = newContext.declare( |
162 | p.getIdentifierName(), p.getFilePosition()); |
163 | } catch (NameContext.RedeclarationException ex) { |
164 | // If it was previously declared then v wouldn't be null. |
165 | throw new SomethingWidgyHappenedError(ex); |
166 | } |
167 | } |
168 | FormalParam newP = new FormalParam(new Identifier( |
169 | p.getFilePosition(), v.newName)); |
170 | newFormals.add(newP); |
171 | } else { |
172 | newFormals.add(p); |
173 | } |
174 | } |
175 | |
176 | // For a declaration, a name is normally introduced in both the |
177 | // scope containing the declaration, and the function body scope. |
178 | // We produce a declaration with the outer name, but in the inner |
179 | // scope the function name should refer to the function itself. |
180 | // The only exception is that if there is a local declaration |
181 | // inside the local scope that masks the function name, then we |
182 | // should not clobber it. |
183 | // Examples: |
184 | // (function f() { |
185 | // var f = 0; |
186 | // return f; |
187 | // })() === 0 |
188 | // and |
189 | // (function f() { |
190 | // function f() { return 0; } |
191 | // return f(); |
192 | // })() === 0 |
193 | // |
194 | // Because the var f or inner function f masks the outer function f, |
195 | // the name "f" should not be considered to refer to the function |
196 | // within its body. The condition |
197 | // newScope.isFunction(name.getName()) |
198 | // && !newScope.isDeclaredFunction(name.getName()) |
199 | // checks that the name still refers to the outer function, not a |
200 | // variable or a different function that is declared within the |
201 | // body. |
202 | // The second clause is required because isDeclaredFunction implies |
203 | // isDeclaredFunction but we need to distinguish the two cases. |
204 | // For a declaration, a name is normally introduced in both the |
205 | // scope containing the declaration, and the function body scope. |
206 | // We produce a declaration with the outer name, but in the inner |
207 | // scope the function name should refer to the function itself. |
208 | // The only exception is that if there is a local declaration |
209 | // inside the local scope that masks the function name, then we |
210 | // should not clobber it. |
211 | // Examples: |
212 | // (function f() { |
213 | // var f = 0; |
214 | // return f; |
215 | // })() === 0 |
216 | // and |
217 | // (function f() { |
218 | // function f() { return 0; } |
219 | // return f(); |
220 | // })() === 0 |
221 | // |
222 | // Because the var f or inner function f masks the outer function f, |
223 | // the name "f" should not be considered to refer to the function |
224 | // within its body. The condition |
225 | // newScope.isFunction(name.getName()) |
226 | // && !newScope.isDeclaredFunction(name.getName()) |
227 | // checks that the name still refers to the outer function, not a |
228 | // variable or a different function that is declared within the |
229 | // body. |
230 | // The second clause is required because isDeclaredFunction implies |
231 | // isFunction but we need to distinguish the two cases. |
232 | if (isDeclaration && !isSynthetic(name) |
233 | && newScope.isFunction(name.getName()) |
234 | && !newScope.isDeclaredFunction(name.getName())) { |
235 | headDecls.add((Declaration) QuasiBuilder.substV( |
236 | "var @innerName = @outerName;", |
237 | "outerName", new Reference(rewrittenName), |
238 | "innerName", new Identifier( |
239 | name.getFilePosition(), |
240 | newContext.lookup(name.getName()).newName))); |
241 | // TODO(mikesamuel): skip if the self name is never used. |
242 | } |
243 | |
244 | FunctionConstructor out = (FunctionConstructor) substV( |
245 | "name", rewrittenName, |
246 | "headDecls", optionalDeclarations(headDecls), |
247 | "params", new ParseTreeNodeContainer(newFormals), |
248 | "body", expandAll(bindings.get("body"), newScope)); |
249 | out.setFilePosition(fc.getFilePosition()); |
250 | return isDeclaration ? new FunctionDeclaration(out) : out; |
251 | } |
252 | return NONE; |
253 | } |
254 | }, |
255 | |
256 | new Rule() { |
257 | @Override |
258 | @RuleDescription( |
259 | name="block", |
260 | synopsis="block scoping", |
261 | reason="", |
262 | matches="{ @body* }", |
263 | substitutes="{ @body* }") |
264 | public ParseTreeNode fire(ParseTreeNode node, Scope scope) { |
265 | if (node instanceof Block) { |
266 | Block bl = (Block) node; |
267 | List<Statement> stmts = Lists.newArrayList(); |
268 | Scope newScope = Scope.fromPlainBlock(scope); |
269 | NameContext<String, ?> newContext = contexts.get(scope) |
270 | .makeChildContext(); |
271 | contexts.put(newScope, newContext); |
272 | for (String local : newScope.getLocals()) { |
273 | try { |
274 | newContext.declare( |
275 | local, newScope.getLocationOfDeclaration(local)); |
276 | } catch (NameContext.RedeclarationException ex) { |
277 | throw new SomethingWidgyHappenedError( |
278 | "Local variable unexpectedly not set", ex); |
279 | } |
280 | } |
281 | for (Statement s : bl.children()) { |
282 | stmts.add((Statement) expand(s, newScope)); |
283 | } |
284 | stmts.addAll(0, newScope.getStartStatements()); |
285 | return new Block(bl.getFilePosition(), stmts); |
286 | } |
287 | return NONE; |
288 | } |
289 | }, |
290 | |
291 | new Rule() { |
292 | @Override |
293 | @RuleDescription( |
294 | name="catch", |
295 | synopsis="catch block scoping", |
296 | reason="", |
297 | matches="catch (@e) { @body* }", |
298 | substitutes="catch (@e) { @body* }") |
299 | public ParseTreeNode fire(ParseTreeNode node, Scope scope) { |
300 | if (node instanceof CatchStmt) { |
301 | CatchStmt cs = (CatchStmt) node; |
302 | Scope newScope = Scope.fromCatchStmt(scope, cs); |
303 | NameContext<String, ?> context = contexts.get(scope); |
304 | NameContext<String, ?> newContext = context.makeChildContext(); |
305 | contexts.put(newScope, newContext); |
306 | try { |
307 | newContext.declare(cs.getException().getIdentifierName(), |
308 | cs.getException().getFilePosition()); |
309 | } catch (NameContext.RedeclarationException ex) { |
310 | ex.toMessageQueue(mq); |
311 | } |
312 | return expandAll(cs, newScope); |
313 | } |
314 | return NONE; |
315 | } |
316 | }, |
317 | |
318 | ////////////// |
319 | // Renaming // |
320 | ////////////// |
321 | |
322 | new Rule() { |
323 | @Override |
324 | @RuleDescription( |
325 | name="memberAccess", |
326 | synopsis="", |
327 | reason="so that we do not mistakenly rename property names", |
328 | matches="@o.@r", |
329 | substitutes="@o.@r") |
330 | public ParseTreeNode fire(ParseTreeNode node, Scope scope) { |
331 | Map<String, ParseTreeNode> bindings = match(node); |
332 | if (bindings != null) { |
333 | return substV("o", expand(bindings.get("o"), scope), |
334 | "r", bindings.get("r")); |
335 | } |
336 | return NONE; |
337 | } |
338 | }, |
339 | new Rule() { |
340 | @Override |
341 | @RuleDescription( |
342 | name="thisReference", |
343 | synopsis="Disallow this in the global scope.", |
344 | reason="The declaration cannot be rewritten.", |
345 | matches="this", |
346 | substitutes="this") |
347 | public ParseTreeNode fire(ParseTreeNode node, Scope scope) { |
348 | if (match(node) != null) { |
349 | if (scope.isOuter()) { |
350 | mq.addMessage( |
351 | RewriterMessageType.THIS_IN_GLOBAL_CONTEXT, |
352 | node.getFilePosition()); |
353 | return new NullLiteral(node.getFilePosition()); |
354 | } |
355 | } |
356 | return NONE; |
357 | } |
358 | }, |
359 | new Rule() { |
360 | @Override |
361 | @RuleDescription( |
362 | name="argumentsReference", |
363 | synopsis="Disallow arguments in the global scope.", |
364 | reason="The declaration cannot be rewritten.", |
365 | matches="arguments", |
366 | substitutes="arguments") |
367 | public ParseTreeNode fire(ParseTreeNode node, Scope scope) { |
368 | if (match(node) != null) { |
369 | if (scope.isOuter()) { |
370 | mq.addMessage( |
371 | RewriterMessageType.ARGUMENTS_IN_GLOBAL_CONTEXT, |
372 | node.getFilePosition()); |
373 | return new NullLiteral(node.getFilePosition()); |
374 | } |
375 | } |
376 | return NONE; |
377 | } |
378 | }, |
379 | new Rule() { |
380 | @Override |
381 | @RuleDescription( |
382 | name="rename", |
383 | synopsis="", |
384 | reason="", |
385 | matches="/* Reference */ @r", |
386 | substitutes="@r") |
387 | public ParseTreeNode fire(ParseTreeNode node, Scope scope) { |
388 | if (node instanceof Reference) { |
389 | Reference r = (Reference) node; |
390 | if (!isSynthetic(r)) { |
391 | FilePosition pos = r.getFilePosition(); |
392 | String rname = r.getIdentifierName(); |
393 | NameContext<String, ?> context = contexts.get(scope); |
394 | NameContext.VarInfo<String, ?> vi = context.lookup(rname); |
395 | if (vi != null) { |
396 | return new Reference(new Identifier(pos, vi.newName)); |
397 | } else { |
398 | mq.addMessage( |
399 | RewriterMessageType.FREE_VARIABLE, pos, |
400 | MessagePart.Factory.valueOf(rname)); |
401 | return new NullLiteral(pos); |
402 | } |
403 | } |
404 | } |
405 | return NONE; |
406 | } |
407 | }, |
408 | new Rule() { |
409 | @Override |
410 | @RuleDescription( |
411 | name="decl", |
412 | synopsis="rewrite declaration identifiers", |
413 | reason="", |
414 | matches="var @i = @v?", |
415 | substitutes="var @ri = @v?;") |
416 | public ParseTreeNode fire(ParseTreeNode node, Scope scope) { |
417 | Map<String, ParseTreeNode> bindings = match(node); |
418 | if (bindings != null) { |
419 | Identifier i = (Identifier) bindings.get("i"); |
420 | Expression v = (Expression) bindings.get("v"); |
421 | Identifier ri; |
422 | if (!isSynthetic(i)) { |
423 | NameContext<String, ?> context = contexts.get(scope); |
424 | NameContext.VarInfo<String, ?> var = context.lookup(i.getName()); |
425 | if (var == null) { // A variable like arguments |
426 | return expandAll(node, scope); |
427 | } |
428 | ri = new Identifier(i.getFilePosition(), var.newName); |
429 | } else { |
430 | ri = i; |
431 | } |
432 | return substV( |
433 | "ri", ri, |
434 | "v", v != null ? expand(v, scope) : null); |
435 | } |
436 | return NONE; |
437 | } |
438 | }, |
439 | new Rule() { |
440 | @Override |
441 | @RuleDescription( |
442 | name="other", |
443 | synopsis="", |
444 | reason="", |
445 | matches="@n", |
446 | substitutes="@n") |
447 | public ParseTreeNode fire(ParseTreeNode node, Scope scope) { |
448 | return expandAll(node, scope); |
449 | } |
450 | }, |
451 | }); |
452 | } |
453 | |
454 | private static Statement optionalDeclarations(List<Declaration> decls) { |
455 | switch (decls.size()) { |
456 | case 0: return null; |
457 | case 1: return decls.get(0); |
458 | default: return new MultiDeclaration(FilePosition.UNKNOWN, decls); |
459 | } |
460 | } |
461 | } |