1 | // Copyright (C) 2010 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 | // case 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.js; |
16 | |
17 | import com.google.caja.lexer.FilePosition; |
18 | import com.google.caja.lexer.Keyword; |
19 | import com.google.caja.lexer.SourceBreaks; |
20 | import com.google.caja.parser.ParseTreeNode; |
21 | import com.google.caja.parser.ParseTreeNodes; |
22 | import com.google.caja.parser.ParserBase; |
23 | import com.google.caja.util.Lists; |
24 | import com.google.caja.util.Maps; |
25 | import com.google.javascript.jscomp.jsonml.JsonML; |
26 | import com.google.javascript.jscomp.jsonml.TagAttr; |
27 | import com.google.javascript.jscomp.jsonml.TagType; |
28 | |
29 | import java.util.Collections; |
30 | import java.util.List; |
31 | import java.util.Map; |
32 | |
33 | /** |
34 | * Translates JsonML into a Caja parse tree. |
35 | * |
36 | * @author mikesamuel@gmail.com |
37 | */ |
38 | public final class JsonMLConverter { |
39 | private static final Expression[] NO_EXPRS = new Expression[0]; |
40 | |
41 | private final Map<String, SourceBreaks> breaksPerFile; |
42 | |
43 | public JsonMLConverter(Map<String, SourceBreaks> breaksPerFile) { |
44 | this.breaksPerFile = Maps.newHashMap(breaksPerFile); |
45 | } |
46 | |
47 | public ParseTreeNode toNode(JsonML jsonML) { |
48 | return toNode(jsonML, ParseTreeNode.class); |
49 | } |
50 | |
51 | public <T extends ParseTreeNode> T toNode(JsonML jsonML, Class<T> clazz) { |
52 | ParseTreeNode node = toParseTreeNode(jsonML); |
53 | if (Statement.class == clazz && node instanceof Expression) { |
54 | node = new ExpressionStmt((Expression) node); |
55 | } |
56 | return clazz.cast(node); |
57 | } |
58 | |
59 | private ParseTreeNode toParseTreeNode(JsonML jsonML) { |
60 | FilePosition pos = positionFrom(jsonML); |
61 | List<? extends JsonML> children = jsonML.getChildren(); |
62 | switch (jsonML.getType()) { |
63 | case ArrayExpr: { |
64 | List<Expression> elements = Lists.newArrayList(children.size()); |
65 | for (JsonML child : children) { |
66 | if (child.getType() == TagType.Empty) { |
67 | elements.add(new Elision(pos)); |
68 | } else { |
69 | elements.add(toNode(child, Expression.class)); |
70 | } |
71 | } |
72 | return new ArrayConstructor(pos, elements); |
73 | } |
74 | case ObjectExpr: |
75 | return new ObjectConstructor(pos, toNodes(children, ObjProperty.class)); |
76 | case AssignExpr: case BinaryExpr: case CountExpr: case MemberExpr: |
77 | case UnaryExpr: { |
78 | String symbol = (String) jsonML.getAttribute(TagAttr.OP); |
79 | OperatorType opType; |
80 | if ("()".equals(symbol) || "[]".equals(symbol)) { |
81 | opType = OperatorType.BRACKET; |
82 | symbol = symbol.substring(0, 1); |
83 | } else { |
84 | switch (children.size()) { |
85 | case 1: |
86 | opType = Boolean.FALSE.equals( |
87 | jsonML.getAttribute(TagAttr.IS_PREFIX)) |
88 | ? OperatorType.POSTFIX : OperatorType.PREFIX; |
89 | break; |
90 | case 2: opType = OperatorType.INFIX; break; |
91 | case 3: opType = OperatorType.TERNARY; break; |
92 | default: throw new AssertionError(); |
93 | } |
94 | } |
95 | Operator op = Operator.lookupOperation(symbol, opType); |
96 | Expression[] operands = toNodes(children, Expression.class) |
97 | .toArray(NO_EXPRS); |
98 | if (op == Operator.MEMBER_ACCESS) { |
99 | String member = ((StringLiteral) operands[1]).getUnquotedValue(); |
100 | if (ParserBase.isJavascriptIdentifier(member) |
101 | && !Keyword.isKeyword(member)) { |
102 | operands[1] = new Reference(new Identifier( |
103 | operands[1].getFilePosition(), member)); |
104 | } else { |
105 | op = Operator.SQUARE_BRACKET; |
106 | } |
107 | } |
108 | return Operation.create(pos, op, operands); |
109 | } |
110 | case CallExpr: |
111 | return Operation.create( |
112 | pos, Operator.FUNCTION_CALL, |
113 | toNodes(children, Expression.class).toArray(NO_EXPRS)); |
114 | case ConditionalExpr: |
115 | return Operation.create( |
116 | pos, Operator.TERNARY, |
117 | toNodes(children, Expression.class).toArray(NO_EXPRS)); |
118 | case DeleteExpr: |
119 | return Operation.create( |
120 | pos, Operator.DELETE, |
121 | toNodes(children, Expression.class).toArray(NO_EXPRS)); |
122 | case EvalExpr: { |
123 | Expression[] operands = new Expression[children.size() + 1]; |
124 | toNodes(children, Expression.class).toArray(operands); |
125 | System.arraycopy(operands, 0, operands, 1, children.size()); |
126 | operands[0] = new Reference( |
127 | new Identifier(FilePosition.startOf(pos), "eval")); |
128 | return Operation.create(pos, Operator.FUNCTION_CALL, operands); |
129 | } |
130 | case InvokeExpr: { |
131 | Expression obj = toNode(children.get(0), Expression.class); |
132 | Expression key = toNode(children.get(1), Expression.class); |
133 | Operator op = Operator.SQUARE_BRACKET; |
134 | if (".".equals(jsonML.getAttribute(TagAttr.OP))) { |
135 | String name = ((StringLiteral) key).getUnquotedValue(); |
136 | if (ParserBase.isJavascriptIdentifier(name) |
137 | && !Keyword.isKeyword(name)) { |
138 | key = new Reference(new Identifier(key.getFilePosition(), name)); |
139 | op = Operator.MEMBER_ACCESS; |
140 | } |
141 | } |
142 | List<Expression> operands = Lists.newArrayList(children.size() - 1); |
143 | operands.add(Operation.create( |
144 | FilePosition.span(obj.getFilePosition(), key.getFilePosition()), |
145 | op, obj, key)); |
146 | operands.addAll(toNodes( |
147 | children.subList(2, children.size()), Expression.class)); |
148 | return Operation.create( |
149 | pos, Operator.FUNCTION_CALL, operands.toArray(NO_EXPRS)); |
150 | } |
151 | case LogicalAndExpr: |
152 | return Operation.create( |
153 | pos, Operator.LOGICAL_AND, |
154 | toNodes(children, Expression.class).toArray(NO_EXPRS)); |
155 | case LogicalOrExpr: |
156 | return Operation.create( |
157 | pos, Operator.LOGICAL_OR, |
158 | toNodes(children, Expression.class).toArray(NO_EXPRS)); |
159 | case NewExpr: { |
160 | int n = children.size(); |
161 | Expression ctor = toNode(children.get(0), Expression.class); |
162 | List<Expression> operands = Lists.newArrayList(n); |
163 | operands.add(Operation.create( |
164 | ctor.getFilePosition(), Operator.CONSTRUCTOR, ctor)); |
165 | operands.addAll(toNodes(children.subList(1, n), Expression.class)); |
166 | return Operation.create( |
167 | pos, Operator.FUNCTION_CALL, operands.toArray(NO_EXPRS)); |
168 | } |
169 | case TypeofExpr: |
170 | return Operation.create( |
171 | pos, Operator.TYPEOF, |
172 | toNodes(children, Expression.class).toArray(NO_EXPRS)); |
173 | case LiteralExpr: { |
174 | String type = (String) jsonML.getAttribute(TagAttr.TYPE); |
175 | if ("string".equals(type)) { |
176 | return new StringLiteral( |
177 | pos, |
178 | StringLiteral.toQuotedValue( |
179 | (String) jsonML.getAttribute(TagAttr.VALUE))); |
180 | } else if ("boolean".equals(type)) { |
181 | return new BooleanLiteral( |
182 | pos, (Boolean) jsonML.getAttribute(TagAttr.VALUE)); |
183 | } else if ("number".equals(type)) { |
184 | Number number = (Number) jsonML.getAttribute(TagAttr.VALUE); |
185 | if (number instanceof Integer) { |
186 | return new IntegerLiteral(pos, number.intValue()); |
187 | } else { |
188 | return new RealLiteral(pos, number.doubleValue()); |
189 | } |
190 | } else if ("null".equals(type)) { |
191 | return new NullLiteral(pos); |
192 | } else { |
193 | throw new IllegalArgumentException(type); |
194 | } |
195 | } |
196 | case RegExpExpr: { |
197 | String flags = (String) jsonML.getAttribute(TagAttr.FLAGS); |
198 | if (flags == null) { flags = ""; } |
199 | String regex = "/" + jsonML.getAttribute(TagAttr.BODY) + "/" + flags; |
200 | return new RegexpLiteral(pos, regex); |
201 | } |
202 | case ThisExpr: |
203 | return new Reference(new Identifier(pos, "this")); |
204 | case BlockStmt: case Program: |
205 | return new Block(pos, toNodes(children, Statement.class)); |
206 | case BreakStmt: { |
207 | String label = (String) jsonML.getAttribute(TagAttr.LABEL); |
208 | if (label == null) { label = ""; } |
209 | return new BreakStmt(pos, label); |
210 | } |
211 | case ContinueStmt: { |
212 | String label = (String) jsonML.getAttribute(TagAttr.LABEL); |
213 | if (label == null) { label = ""; } |
214 | return new ContinueStmt(pos, label); |
215 | } |
216 | case DebuggerStmt: |
217 | return new DebuggerStmt(pos); |
218 | case DoWhileStmt: |
219 | return new DoWhileLoop( |
220 | pos, "", toNode(children.get(0), Statement.class), |
221 | toNode(children.get(1), Expression.class)); |
222 | case EmptyStmt: |
223 | return new Noop(pos); |
224 | case ForInStmt: |
225 | if (children.get(0).getType() == TagType.VarDecl) { |
226 | return new ForEachLoop( |
227 | pos, "", toNode(children.get(0), Declaration.class), |
228 | toNode(children.get(1), Expression.class), |
229 | toNode(children.get(2), Statement.class)); |
230 | } else { |
231 | return new ForEachLoop( |
232 | pos, "", toNode(children.get(0), Expression.class), |
233 | toNode(children.get(1), Expression.class), |
234 | toNode(children.get(2), Statement.class)); |
235 | } |
236 | case ForStmt: |
237 | return new ForLoop( |
238 | pos, "", |
239 | toNodeOrNoop(children.get(0)), |
240 | toNodeOrTrue(children.get(1)), |
241 | toNodeOrNoop(children.get(2)), |
242 | toNode(children.get(3), Statement.class)); |
243 | case FunctionExpr: |
244 | return toFunctionConstructor(jsonML); |
245 | case IdExpr: |
246 | return new Reference( |
247 | new Identifier(pos, (String) jsonML.getAttribute(TagAttr.NAME))); |
248 | case IfStmt: { |
249 | Expression cond = toNode(children.get(0), Expression.class); |
250 | Statement thenClause = toNode(children.get(1), Statement.class); |
251 | Statement elseClause = children.get(2).getType() != TagType.EmptyStmt |
252 | ? toNode(children.get(2), Statement.class) : null; |
253 | List<ParseTreeNode> childNodes = Lists.newArrayList(); |
254 | childNodes.add(cond); |
255 | childNodes.add(thenClause); |
256 | if (elseClause != null) { |
257 | if (elseClause instanceof Conditional) { |
258 | childNodes.addAll(elseClause.children()); |
259 | } else { |
260 | childNodes.add(elseClause); |
261 | } |
262 | } |
263 | return new Conditional(pos, null, childNodes); |
264 | } |
265 | case LabelledStmt: { |
266 | String label = (String) jsonML.getAttribute(TagAttr.LABEL); |
267 | Statement body = toNode(children.get(0), Statement.class); |
268 | if (body instanceof LabeledStatement |
269 | && !(body instanceof LabeledStmtWrapper)) { |
270 | return ParseTreeNodes.newNodeInstance( |
271 | body.getClass(), body.getFilePosition(), label, body.children()); |
272 | } else { |
273 | return new LabeledStmtWrapper(pos, label, body); |
274 | } |
275 | } |
276 | case ReturnStmt: |
277 | if (children.isEmpty()) { |
278 | return new ReturnStmt(pos, null); |
279 | } else { |
280 | return new ReturnStmt(pos, toNode(children.get(0), Expression.class)); |
281 | } |
282 | case SwitchStmt: |
283 | return new SwitchStmt( |
284 | pos, "", toNode(children.get(0), Expression.class), |
285 | toNodes(children.subList(1, children.size()), SwitchCase.class)); |
286 | case ThrowStmt: |
287 | return new ThrowStmt(pos, toNode(children.get(0), Expression.class)); |
288 | case TryStmt: { |
289 | Block body = toNode(children.get(0), Block.class); |
290 | CatchStmt catchStmt = null; |
291 | FinallyStmt finallyStmt = null; |
292 | int i = 1, n = children.size(); |
293 | if (children.get(i).getType() == TagType.CatchClause) { |
294 | catchStmt = toNode(children.get(i++), CatchStmt.class); |
295 | } else if (children.get(i).getType() == TagType.Empty) { |
296 | ++i; |
297 | } |
298 | if (i < n) { |
299 | Block block = toNode(children.get(i), Block.class); |
300 | finallyStmt = new FinallyStmt(block.getFilePosition(), block); |
301 | } |
302 | return new TryStmt(pos, body, catchStmt, finallyStmt); |
303 | } |
304 | case WhileStmt: |
305 | return new WhileLoop( |
306 | pos, "", toNode(children.get(0), Expression.class), |
307 | toNode(children.get(1), Statement.class)); |
308 | case WithStmt: |
309 | return new WithStmt( |
310 | pos, toNode(children.get(0), Expression.class), |
311 | toNode(children.get(1), Statement.class)); |
312 | case FunctionDecl: |
313 | return new FunctionDeclaration(toFunctionConstructor(jsonML)); |
314 | case ParamDecl: |
315 | throw new IllegalStateException("Orphaned param"); |
316 | case PrologueDecl: |
317 | throw new IllegalStateException("Orphaned prologue"); |
318 | case VarDecl: { |
319 | List<Declaration> decls = Lists.newArrayList(); |
320 | for (JsonML child : children) { |
321 | FilePosition childPos = positionFrom(child); |
322 | if (child.getType() == TagType.InitPatt) { |
323 | List<JsonML> initPattParts = child.getChildren(); |
324 | decls.add(new Declaration( |
325 | childPos, toNode(initPattParts.get(0), Identifier.class), |
326 | toNode(initPattParts.get(1), Expression.class))); |
327 | } else { |
328 | decls.add(new Declaration( |
329 | childPos, toNode(child, Identifier.class), null)); |
330 | } |
331 | } |
332 | if (decls.size() == 1) { |
333 | return decls.get(0); |
334 | } |
335 | return new MultiDeclaration(pos, decls); |
336 | } |
337 | case DataProp: |
338 | return new ValueProperty( |
339 | pos, |
340 | StringLiteral.valueOf( |
341 | FilePosition.span(pos, positionFrom(children.get(0))), |
342 | (String) jsonML.getAttribute(TagAttr.NAME)), |
343 | toNode(children.get(0), Expression.class)); |
344 | case GetterProp: |
345 | return new GetterProperty( |
346 | pos, |
347 | StringLiteral.valueOf( |
348 | FilePosition.span(pos, positionFrom(children.get(0))), |
349 | (String) jsonML.getAttribute(TagAttr.NAME)), |
350 | toFunctionConstructor(children.get(0))); |
351 | case SetterProp: |
352 | return new SetterProperty( |
353 | pos, |
354 | StringLiteral.valueOf( |
355 | FilePosition.span(pos, positionFrom(children.get(0))), |
356 | (String) jsonML.getAttribute(TagAttr.NAME)), |
357 | toFunctionConstructor(children.get(0))); |
358 | case IdPatt: |
359 | return new Identifier(pos, (String) jsonML.getAttribute(TagAttr.NAME)); |
360 | case InitPatt: |
361 | throw new IllegalStateException("Orphaned init patt"); |
362 | case Case: { |
363 | Expression value = toNode(children.get(0), Expression.class); |
364 | int n = children.size(); |
365 | List<Statement> body = toNodes(children.subList(1, n), Statement.class); |
366 | FilePosition bodyPos = body.isEmpty() |
367 | ? FilePosition.span( |
368 | FilePosition.endOf(value.getFilePosition()), pos) |
369 | : FilePosition.span( |
370 | FilePosition.startOf(body.get(0).getFilePosition()), pos); |
371 | return new CaseStmt(pos, value, new Block(bodyPos, body)); |
372 | } |
373 | case DefaultCase: { |
374 | List<Statement> body = toNodes(children, Statement.class); |
375 | FilePosition bodyPos = body.isEmpty() |
376 | ? pos |
377 | : FilePosition.span( |
378 | FilePosition.startOf(body.get(0).getFilePosition()), pos); |
379 | return new DefaultCaseStmt(pos, new Block(bodyPos, body)); |
380 | } |
381 | case CatchClause: { |
382 | Identifier exName = toNode(children.get(0), Identifier.class); |
383 | return new CatchStmt( |
384 | pos, new Declaration(exName.getFilePosition(), exName, null), |
385 | toNode(children.get(1), Block.class)); |
386 | } |
387 | case Empty: |
388 | throw new IllegalStateException("Orphaned empty"); |
389 | } |
390 | return null; |
391 | } |
392 | |
393 | private FilePosition positionFrom(JsonML jsonML) { |
394 | SourceBreaks breaks = breaksPerFile.get(jsonML.getAttribute(TagAttr.SOURCE)); |
395 | if (breaks == null) { return FilePosition.UNKNOWN; } |
396 | int pos = (Integer) jsonML.getAttribute(TagAttr.OPAQUE_POSITION); |
397 | int start = pos >>> 16; |
398 | return breaks.toFilePosition(start, (pos & 0xfff) + start); |
399 | } |
400 | |
401 | private Expression toNodeOrTrue(JsonML jsonML) { |
402 | if (jsonML.getType() == TagType.Empty) { |
403 | return new BooleanLiteral(positionFrom(jsonML), true); |
404 | } else { |
405 | return toNode(jsonML, Expression.class); |
406 | } |
407 | } |
408 | |
409 | private Statement toNodeOrNoop(JsonML jsonML) { |
410 | if (jsonML.getType() == TagType.Empty) { |
411 | return new Noop(positionFrom(jsonML)); |
412 | } else { |
413 | return toNode(jsonML, Statement.class); |
414 | } |
415 | } |
416 | |
417 | private <T extends ParseTreeNode> List<T> toNodes( |
418 | List<? extends JsonML> jsonMLs, Class<T> clazz) { |
419 | int n = jsonMLs.size(); |
420 | if (n == 0) { return Collections.emptyList(); } |
421 | List<T> out = Lists.newArrayList(n); |
422 | int i = 0; |
423 | if (jsonMLs.get(0).getType() == TagType.PrologueDecl) { |
424 | FilePosition start = null; |
425 | FilePosition end; |
426 | List<Directive> directives = Lists.newArrayList(); |
427 | do { |
428 | JsonML decl = jsonMLs.get(i++); |
429 | end = positionFrom(decl); |
430 | if (start == null) { start = end; } |
431 | directives.add(new Directive( |
432 | end, (String) decl.getAttribute(TagAttr.DIRECTIVE))); |
433 | } while (i < n && jsonMLs.get(i).getType() == TagType.PrologueDecl); |
434 | DirectivePrologue directive = new DirectivePrologue( |
435 | FilePosition.span(start, end), null, directives); |
436 | out.add(clazz.cast(directive)); |
437 | } |
438 | while (i < n) { |
439 | out.add(toNode(jsonMLs.get(i++), clazz)); |
440 | } |
441 | return out; |
442 | } |
443 | |
444 | private FunctionConstructor toFunctionConstructor(JsonML jsonML) { |
445 | FilePosition pos = positionFrom(jsonML); |
446 | List<? extends JsonML> children = jsonML.getChildren(); |
447 | JsonML name = children.get(0); |
448 | Identifier nameNode; |
449 | if (name.getType() == TagType.Empty) { |
450 | nameNode = new Identifier(positionFrom(name), null); |
451 | } else { |
452 | nameNode = toNode(name, Identifier.class); |
453 | } |
454 | List<FormalParam> params = Lists.newArrayList(); |
455 | for (JsonML param : children.get(1).getChildren()) { |
456 | params.add(new FormalParam(toNode(param, Identifier.class))); |
457 | } |
458 | FilePosition blockEnd = FilePosition.endOf(pos); |
459 | FilePosition blockStart = blockEnd; |
460 | List<Statement> bodyParts = toNodes( |
461 | children.subList(2, children.size()), Statement.class); |
462 | if (!bodyParts.isEmpty()) { |
463 | blockStart = bodyParts.get(0).getFilePosition(); |
464 | } |
465 | Block body = new Block(FilePosition.span(blockStart, blockEnd), bodyParts); |
466 | return new FunctionConstructor(pos, nameNode, params, body); |
467 | } |
468 | } |