| /* |
| * Copyright 2008 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.dev.js; |
| |
| import com.google.gwt.dev.jjs.Correlation.Literal; |
| import com.google.gwt.dev.jjs.SourceInfo; |
| import com.google.gwt.dev.jjs.SourceOrigin; |
| import com.google.gwt.dev.js.ast.JsArrayAccess; |
| import com.google.gwt.dev.js.ast.JsArrayLiteral; |
| import com.google.gwt.dev.js.ast.JsBinaryOperation; |
| import com.google.gwt.dev.js.ast.JsBinaryOperator; |
| import com.google.gwt.dev.js.ast.JsBlock; |
| import com.google.gwt.dev.js.ast.JsBooleanLiteral; |
| import com.google.gwt.dev.js.ast.JsBreak; |
| import com.google.gwt.dev.js.ast.JsCase; |
| import com.google.gwt.dev.js.ast.JsCatch; |
| import com.google.gwt.dev.js.ast.JsConditional; |
| import com.google.gwt.dev.js.ast.JsContinue; |
| import com.google.gwt.dev.js.ast.JsDebugger; |
| import com.google.gwt.dev.js.ast.JsDefault; |
| import com.google.gwt.dev.js.ast.JsDoWhile; |
| import com.google.gwt.dev.js.ast.JsEmpty; |
| import com.google.gwt.dev.js.ast.JsExprStmt; |
| import com.google.gwt.dev.js.ast.JsExpression; |
| import com.google.gwt.dev.js.ast.JsFor; |
| import com.google.gwt.dev.js.ast.JsForIn; |
| import com.google.gwt.dev.js.ast.JsFunction; |
| import com.google.gwt.dev.js.ast.JsIf; |
| import com.google.gwt.dev.js.ast.JsInvocation; |
| import com.google.gwt.dev.js.ast.JsLabel; |
| import com.google.gwt.dev.js.ast.JsName; |
| import com.google.gwt.dev.js.ast.JsNameRef; |
| import com.google.gwt.dev.js.ast.JsNew; |
| import com.google.gwt.dev.js.ast.JsNode; |
| import com.google.gwt.dev.js.ast.JsNullLiteral; |
| import com.google.gwt.dev.js.ast.JsNumberLiteral; |
| import com.google.gwt.dev.js.ast.JsObjectLiteral; |
| import com.google.gwt.dev.js.ast.JsParameter; |
| import com.google.gwt.dev.js.ast.JsPostfixOperation; |
| import com.google.gwt.dev.js.ast.JsPrefixOperation; |
| import com.google.gwt.dev.js.ast.JsRegExp; |
| import com.google.gwt.dev.js.ast.JsReturn; |
| import com.google.gwt.dev.js.ast.JsRootScope; |
| import com.google.gwt.dev.js.ast.JsScope; |
| import com.google.gwt.dev.js.ast.JsStatement; |
| import com.google.gwt.dev.js.ast.JsStringLiteral; |
| import com.google.gwt.dev.js.ast.JsSwitch; |
| import com.google.gwt.dev.js.ast.JsThisRef; |
| import com.google.gwt.dev.js.ast.JsThrow; |
| import com.google.gwt.dev.js.ast.JsTry; |
| import com.google.gwt.dev.js.ast.JsUnaryOperator; |
| import com.google.gwt.dev.js.ast.JsVars; |
| import com.google.gwt.dev.js.ast.JsWhile; |
| import com.google.gwt.dev.js.rhino.Context; |
| import com.google.gwt.dev.js.rhino.ErrorReporter; |
| import com.google.gwt.dev.js.rhino.EvaluatorException; |
| import com.google.gwt.dev.js.rhino.IRFactory; |
| import com.google.gwt.dev.js.rhino.Node; |
| import com.google.gwt.dev.js.rhino.Parser; |
| import com.google.gwt.dev.js.rhino.TokenStream; |
| import com.google.gwt.dev.util.collect.Stack; |
| |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Parses JavaScript source. |
| */ |
| public class JsParser { |
| |
| public static List<JsStatement> parse(SourceInfo rootSourceInfo, |
| JsScope scope, Reader r) throws IOException, JsParserException { |
| return new JsParser().parseImpl(rootSourceInfo, scope, r); |
| } |
| |
| public static void parseInto(SourceInfo rootSourceInfo, JsScope scope, |
| JsBlock block, Reader r) throws IOException, JsParserException { |
| List<JsStatement> childStmts = parse(rootSourceInfo, scope, r); |
| List<JsStatement> parentStmts = block.getStatements(); |
| parentStmts.addAll(childStmts); |
| } |
| |
| private final Stack<JsScope> scopeStack = new Stack<JsScope>(); |
| private final Stack<SourceInfo> sourceInfoStack = new Stack<SourceInfo>(); |
| |
| private JsParser() { |
| } |
| |
| List<JsStatement> parseImpl(final SourceInfo rootSourceInfo, JsScope scope, |
| Reader r) throws JsParserException, IOException { |
| // Create a custom error handler so that we can throw our own exceptions. |
| Context.enter().setErrorReporter(new ErrorReporter() { |
| @Override |
| public void error(String msg, String loc, int ln, String src, int col) { |
| throw new UncheckedJsParserException(new JsParserException(msg, ln, |
| src, col, rootSourceInfo.getFileName())); |
| } |
| |
| @Override |
| public EvaluatorException runtimeError(String msg, String loc, int ln, |
| String src, int col) { |
| // Never called, but just in case. |
| throw new UncheckedJsParserException(new JsParserException(msg, ln, |
| src, col, rootSourceInfo.getFileName())); |
| } |
| |
| @Override |
| public void warning(String msg, String loc, int ln, String src, int col) { |
| // Ignore warnings. |
| } |
| }); |
| try { |
| // Parse using the Rhino parser. |
| // |
| TokenStream ts = new TokenStream(r, rootSourceInfo.getFileName(), |
| rootSourceInfo.getStartLine()); |
| Parser parser = new Parser(new IRFactory(ts)); |
| Node topNode = (Node) parser.parse(ts); |
| |
| // Map the Rhino AST to ours. |
| pushScope(scope, rootSourceInfo); |
| List<JsStatement> stmts = mapStatements(topNode); |
| popScope(); |
| return stmts; |
| } catch (UncheckedJsParserException e) { |
| throw e.getParserException(); |
| } finally { |
| Context.exit(); |
| } |
| } |
| |
| private JsParserException createParserException(String msg, Node offender) { |
| return new JsParserException(msg, offender.getLineno(), null, 0, |
| sourceInfoStack.peek().getFileName()); |
| } |
| |
| private JsScope getScope() { |
| return scopeStack.peek(); |
| } |
| |
| private SourceInfo makeSourceInfo(Node node) { |
| SourceInfo parent = sourceInfoStack.peek(); |
| int lineno = node.getLineno(); |
| if (lineno == -1) { |
| // Rhino only reports line numbers for statement nodes, not expressions |
| return parent; |
| } |
| return parent.makeChild(SourceOrigin.create(lineno, parent.getFileName())); |
| } |
| |
| /** |
| * Force a distinct child to be created, so correlations can be added. |
| */ |
| private SourceInfo makeSourceInfoDistinct(Node node) { |
| SourceInfo parent = sourceInfoStack.peek(); |
| int lineno = node.getLineno(); |
| if (lineno == -1) { |
| // Rhino only reports line numbers for statement nodes, not expressions |
| lineno = parent.getStartLine(); |
| } |
| return parent.makeChild(SourceOrigin.create(lineno, parent.getFileName())); |
| } |
| |
| private JsNode map(Node node) throws JsParserException { |
| |
| switch (node.getType()) { |
| case TokenStream.SCRIPT: { |
| JsBlock block = new JsBlock(makeSourceInfo(node)); |
| mapStatements(block.getStatements(), node); |
| return block; |
| } |
| |
| case TokenStream.DEBUGGER: |
| return mapDebuggerStatement(node); |
| |
| case TokenStream.VOID: |
| // VOID = nothing was parsed for this node |
| return null; |
| |
| case TokenStream.EXPRSTMT: |
| return mapExprStmt(node); |
| |
| case TokenStream.REGEXP: |
| return mapRegExp(node); |
| |
| case TokenStream.ADD: |
| return mapBinaryOperation(JsBinaryOperator.ADD, node); |
| |
| case TokenStream.SUB: |
| return mapBinaryOperation(JsBinaryOperator.SUB, node); |
| |
| case TokenStream.MUL: |
| return mapBinaryOperation(JsBinaryOperator.MUL, node); |
| |
| case TokenStream.DIV: |
| return mapBinaryOperation(JsBinaryOperator.DIV, node); |
| |
| case TokenStream.MOD: |
| return mapBinaryOperation(JsBinaryOperator.MOD, node); |
| |
| case TokenStream.AND: |
| return mapBinaryOperation(JsBinaryOperator.AND, node); |
| |
| case TokenStream.OR: |
| return mapBinaryOperation(JsBinaryOperator.OR, node); |
| |
| case TokenStream.BITAND: |
| return mapBinaryOperation(JsBinaryOperator.BIT_AND, node); |
| |
| case TokenStream.BITOR: |
| return mapBinaryOperation(JsBinaryOperator.BIT_OR, node); |
| |
| case TokenStream.BITXOR: |
| return mapBinaryOperation(JsBinaryOperator.BIT_XOR, node); |
| |
| case TokenStream.ASSIGN: |
| return mapAssignmentVariant(node); |
| |
| case TokenStream.RELOP: |
| return mapRelationalVariant(node); |
| |
| case TokenStream.EQOP: |
| return mapEqualityVariant(node); |
| |
| case TokenStream.SHOP: |
| return mapShiftVariant(node); |
| |
| case TokenStream.UNARYOP: |
| return mapUnaryVariant(node); |
| |
| case TokenStream.INC: |
| return mapIncDecFixity(JsUnaryOperator.INC, node); |
| |
| case TokenStream.DEC: |
| return mapIncDecFixity(JsUnaryOperator.DEC, node); |
| |
| case TokenStream.HOOK: |
| return mapConditional(node); |
| |
| case TokenStream.STRING: { |
| SourceInfo info = makeSourceInfoDistinct(node); |
| info.addCorrelation(info.getCorrelator().by(Literal.STRING)); |
| return new JsStringLiteral(info, node.getString()); |
| } |
| |
| case TokenStream.NUMBER: |
| return mapNumber(node); |
| |
| case TokenStream.CALL: |
| return mapCall(node); |
| |
| case TokenStream.GETPROP: |
| return mapGetProp(node); |
| |
| case TokenStream.SETPROP: |
| return mapSetProp(node); |
| |
| case TokenStream.DELPROP: |
| return mapDeleteProp(node); |
| |
| case TokenStream.IF: |
| return mapIfStatement(node); |
| |
| case TokenStream.WHILE: |
| return mapDoOrWhileStatement(true, node); |
| |
| case TokenStream.DO: |
| return mapDoOrWhileStatement(false, node); |
| |
| case TokenStream.FOR: |
| return mapForStatement(node); |
| |
| case TokenStream.WITH: |
| return mapWithStatement(node); |
| |
| case TokenStream.GETELEM: |
| return mapGetElem(node); |
| |
| case TokenStream.SETELEM: |
| return mapSetElem(node); |
| |
| case TokenStream.FUNCTION: |
| return mapFunction(node); |
| |
| case TokenStream.BLOCK: |
| return mapBlock(node); |
| |
| case TokenStream.SETNAME: |
| return mapBinaryOperation(JsBinaryOperator.ASG, node); |
| |
| case TokenStream.NAME: |
| case TokenStream.BINDNAME: |
| return mapName(node); |
| |
| case TokenStream.RETURN: |
| return mapReturn(node); |
| |
| case TokenStream.BREAK: |
| return mapBreak(node); |
| |
| case TokenStream.CONTINUE: |
| return mapContinue(node); |
| |
| case TokenStream.OBJLIT: |
| return mapObjectLiteral(node); |
| |
| case TokenStream.ARRAYLIT: |
| return mapArrayLit(node); |
| |
| case TokenStream.VAR: |
| return mapVar(node); |
| |
| case TokenStream.PRIMARY: |
| return mapPrimary(node); |
| |
| case TokenStream.COMMA: |
| return mapBinaryOperation(JsBinaryOperator.COMMA, node); |
| |
| case TokenStream.NEW: |
| return mapNew(node); |
| |
| case TokenStream.THROW: |
| return mapThrowStatement(node); |
| |
| case TokenStream.TRY: |
| return mapTryStatement(node); |
| |
| case TokenStream.SWITCH: |
| return mapSwitchStatement(node); |
| |
| case TokenStream.LABEL: |
| return mapLabel(node); |
| |
| default: |
| int tokenType = node.getType(); |
| throw createParserException("Unexpected top-level token type: " |
| + tokenType, node); |
| } |
| } |
| |
| private JsArrayLiteral mapArrayLit(Node node) throws JsParserException { |
| JsArrayLiteral toLit = new JsArrayLiteral(makeSourceInfo(node)); |
| Node from = node.getFirstChild(); |
| while (from != null) { |
| toLit.getExpressions().add(mapExpression(from)); |
| from = from.getNext(); |
| } |
| return toLit; |
| } |
| |
| /** |
| * Produces a {@link JsNameRef}. |
| */ |
| private JsNameRef mapAsPropertyNameRef(Node nameRefNode) |
| throws JsParserException { |
| JsNode unknown = map(nameRefNode); |
| // This is weird, but for "a.b", the rhino AST calls "b" a string literal. |
| // However, since we know it's for a PROPGET, we can unstringliteralize it. |
| // |
| if (unknown instanceof JsStringLiteral) { |
| JsStringLiteral lit = (JsStringLiteral) unknown; |
| String litName = lit.getValue(); |
| return new JsNameRef(makeSourceInfo(nameRefNode), litName); |
| } else { |
| throw createParserException("Expecting a name reference", nameRefNode); |
| } |
| } |
| |
| private JsExpression mapAssignmentVariant(Node asgNode) |
| throws JsParserException { |
| switch (asgNode.getIntDatum()) { |
| case TokenStream.NOP: |
| return mapBinaryOperation(JsBinaryOperator.ASG, asgNode); |
| |
| case TokenStream.ADD: |
| return mapBinaryOperation(JsBinaryOperator.ASG_ADD, asgNode); |
| |
| case TokenStream.SUB: |
| return mapBinaryOperation(JsBinaryOperator.ASG_SUB, asgNode); |
| |
| case TokenStream.MUL: |
| return mapBinaryOperation(JsBinaryOperator.ASG_MUL, asgNode); |
| |
| case TokenStream.DIV: |
| return mapBinaryOperation(JsBinaryOperator.ASG_DIV, asgNode); |
| |
| case TokenStream.MOD: |
| return mapBinaryOperation(JsBinaryOperator.ASG_MOD, asgNode); |
| |
| case TokenStream.BITAND: |
| return mapBinaryOperation(JsBinaryOperator.ASG_BIT_AND, asgNode); |
| |
| case TokenStream.BITOR: |
| return mapBinaryOperation(JsBinaryOperator.ASG_BIT_OR, asgNode); |
| |
| case TokenStream.BITXOR: |
| return mapBinaryOperation(JsBinaryOperator.ASG_BIT_XOR, asgNode); |
| |
| case TokenStream.LSH: |
| return mapBinaryOperation(JsBinaryOperator.ASG_SHL, asgNode); |
| |
| case TokenStream.RSH: |
| return mapBinaryOperation(JsBinaryOperator.ASG_SHR, asgNode); |
| |
| case TokenStream.URSH: |
| return mapBinaryOperation(JsBinaryOperator.ASG_SHRU, asgNode); |
| |
| default: |
| throw createParserException("Unknown assignment operator variant: " |
| + asgNode.getIntDatum(), asgNode); |
| } |
| } |
| |
| private JsExpression mapBinaryOperation(JsBinaryOperator op, Node node) |
| throws JsParserException { |
| Node from1 = node.getFirstChild(); |
| Node from2 = from1.getNext(); |
| |
| JsExpression to1 = mapExpression(from1); |
| JsExpression to2 = mapExpression(from2); |
| |
| return new JsBinaryOperation(makeSourceInfo(node), op, to1, to2); |
| } |
| |
| private JsBlock mapBlock(Node nodeStmts) throws JsParserException { |
| SourceInfo info = makeSourceInfo(nodeStmts); |
| JsBlock block = new JsBlock(info); |
| pushSourceInfo(info); |
| mapStatements(block.getStatements(), nodeStmts); |
| popSourceInfo(); |
| return block; |
| } |
| |
| private JsBreak mapBreak(Node breakNode) { |
| Node fromLabel = breakNode.getFirstChild(); |
| if (fromLabel != null) { |
| return new JsBreak(makeSourceInfo(breakNode), mapName(fromLabel)); |
| } else { |
| return new JsBreak(makeSourceInfo(breakNode)); |
| } |
| } |
| |
| private JsInvocation mapCall(Node callNode) throws JsParserException { |
| JsInvocation invocation = new JsInvocation(makeSourceInfo(callNode)); |
| |
| // Map the target expression. |
| // |
| Node from = callNode.getFirstChild(); |
| JsExpression to = mapExpression(from); |
| invocation.setQualifier(to); |
| |
| // Iterate over and map the arguments. |
| // |
| List<JsExpression> args = invocation.getArguments(); |
| from = from.getNext(); |
| while (from != null) { |
| to = mapExpression(from); |
| args.add(to); |
| from = from.getNext(); |
| } |
| |
| return invocation; |
| } |
| |
| private JsExpression mapConditional(Node condNode) throws JsParserException { |
| JsConditional toCond = new JsConditional(makeSourceInfo(condNode)); |
| |
| Node fromTest = condNode.getFirstChild(); |
| toCond.setTestExpression(mapExpression(fromTest)); |
| |
| Node fromThen = fromTest.getNext(); |
| toCond.setThenExpression(mapExpression(fromThen)); |
| |
| Node fromElse = fromThen.getNext(); |
| toCond.setElseExpression(mapExpression(fromElse)); |
| |
| return toCond; |
| } |
| |
| private JsContinue mapContinue(Node contNode) { |
| Node fromLabel = contNode.getFirstChild(); |
| if (fromLabel != null) { |
| return new JsContinue(makeSourceInfo(contNode), mapName(fromLabel)); |
| } else { |
| return new JsContinue(makeSourceInfo(contNode)); |
| } |
| } |
| |
| private JsStatement mapDebuggerStatement(Node node) { |
| // Calls an optional method to invoke the debugger. |
| // |
| return new JsDebugger(makeSourceInfo(node)); |
| } |
| |
| private JsExpression mapDeleteProp(Node node) throws JsParserException { |
| Node from = node.getFirstChild(); |
| JsExpression to = mapExpression(from); |
| if (to instanceof JsNameRef) { |
| return new JsPrefixOperation(makeSourceInfo(node), |
| JsUnaryOperator.DELETE, to); |
| } else if (to instanceof JsArrayAccess) { |
| return new JsPrefixOperation(makeSourceInfo(node), |
| JsUnaryOperator.DELETE, to); |
| } else { |
| throw createParserException( |
| "'delete' can only operate on property names and array elements", |
| from); |
| } |
| } |
| |
| private JsStatement mapDoOrWhileStatement(boolean isWhile, Node ifNode) |
| throws JsParserException { |
| |
| // Pull out the pieces we want to map. |
| // |
| Node fromTestExpr; |
| Node fromBody; |
| if (isWhile) { |
| fromTestExpr = ifNode.getFirstChild(); |
| fromBody = ifNode.getFirstChild().getNext(); |
| } else { |
| fromBody = ifNode.getFirstChild(); |
| fromTestExpr = ifNode.getFirstChild().getNext(); |
| } |
| |
| SourceInfo info = makeSourceInfo(ifNode); |
| pushSourceInfo(info); |
| |
| // Map the test expression. |
| // |
| JsExpression toTestExpr = mapExpression(fromTestExpr); |
| |
| // Map the body block. |
| // |
| JsStatement toBody = mapStatement(fromBody); |
| |
| popSourceInfo(); |
| |
| // Create and attach the "while" or "do" statement we're mapping to. |
| // |
| if (isWhile) { |
| return new JsWhile(info, toTestExpr, toBody); |
| } else { |
| return new JsDoWhile(info, toTestExpr, toBody); |
| } |
| } |
| |
| private JsExpression mapEqualityVariant(Node eqNode) throws JsParserException { |
| switch (eqNode.getIntDatum()) { |
| case TokenStream.EQ: |
| return mapBinaryOperation(JsBinaryOperator.EQ, eqNode); |
| |
| case TokenStream.NE: |
| return mapBinaryOperation(JsBinaryOperator.NEQ, eqNode); |
| |
| case TokenStream.SHEQ: |
| return mapBinaryOperation(JsBinaryOperator.REF_EQ, eqNode); |
| |
| case TokenStream.SHNE: |
| return mapBinaryOperation(JsBinaryOperator.REF_NEQ, eqNode); |
| |
| case TokenStream.LT: |
| return mapBinaryOperation(JsBinaryOperator.LT, eqNode); |
| |
| case TokenStream.LE: |
| return mapBinaryOperation(JsBinaryOperator.LTE, eqNode); |
| |
| case TokenStream.GT: |
| return mapBinaryOperation(JsBinaryOperator.GT, eqNode); |
| |
| case TokenStream.GE: |
| return mapBinaryOperation(JsBinaryOperator.GTE, eqNode); |
| |
| default: |
| throw createParserException("Unknown equality operator variant: " |
| + eqNode.getIntDatum(), eqNode); |
| } |
| } |
| |
| private JsExpression mapExpression(Node exprNode) throws JsParserException { |
| JsNode unknown = map(exprNode); |
| if (unknown instanceof JsExpression) { |
| return (JsExpression) unknown; |
| } else { |
| throw createParserException("Expecting an expression", exprNode); |
| } |
| } |
| |
| private JsExprStmt mapExprStmt(Node node) throws JsParserException { |
| pushSourceInfo(makeSourceInfo(node)); |
| JsExpression expr = mapExpression(node.getFirstChild()); |
| popSourceInfo(); |
| return expr.makeStmt(); |
| } |
| |
| private JsStatement mapForStatement(Node forNode) throws JsParserException { |
| Node fromInit = forNode.getFirstChild(); |
| Node fromTest = fromInit.getNext(); |
| Node fromIncr = fromTest.getNext(); |
| Node fromBody = fromIncr.getNext(); |
| |
| SourceInfo info = makeSourceInfo(forNode); |
| if (fromBody == null) { |
| // This could be a "for...in" structure. |
| // We could based on the different child layout. |
| // |
| Node fromIter = forNode.getFirstChild(); |
| Node fromObjExpr = fromIter.getNext(); |
| fromBody = fromObjExpr.getNext(); |
| |
| JsForIn toForIn; |
| if (fromIter.getType() == TokenStream.VAR) { |
| // A named iterator var. |
| // |
| Node fromIterVarName = fromIter.getFirstChild(); |
| String fromName = fromIterVarName.getString(); |
| JsName toName = getScope().declareName(fromName); |
| toForIn = new JsForIn(info, toName); |
| Node fromIterInit = fromIterVarName.getFirstChild(); |
| if (fromIterInit != null) { |
| // That has an initializer expression (useful only for side effects). |
| // |
| toForIn.setIterExpr(mapOptionalExpression(fromIterInit)); |
| } |
| } else { |
| // An unnamed iterator var. |
| // |
| toForIn = new JsForIn(info); |
| toForIn.setIterExpr(mapExpression(fromIter)); |
| } |
| toForIn.setObjExpr(mapExpression(fromObjExpr)); |
| |
| // The body stmt. |
| // |
| JsStatement bodyStmt = mapStatement(fromBody); |
| if (bodyStmt != null) { |
| toForIn.setBody(bodyStmt); |
| } else { |
| toForIn.setBody(new JsEmpty(info)); |
| } |
| |
| return toForIn; |
| } else { |
| // Regular ol' for loop. |
| // |
| JsFor toFor = new JsFor(info); |
| |
| // The first item is either an expression or a JsVars. |
| JsNode initThingy = map(fromInit); |
| if (initThingy != null) { |
| if (initThingy instanceof JsVars) { |
| toFor.setInitVars((JsVars) initThingy); |
| } else { |
| assert (initThingy instanceof JsExpression); |
| toFor.setInitExpr((JsExpression) initThingy); |
| } |
| } |
| toFor.setCondition(mapOptionalExpression(fromTest)); |
| toFor.setIncrExpr(mapOptionalExpression(fromIncr)); |
| |
| JsStatement bodyStmt = mapStatement(fromBody); |
| if (bodyStmt != null) { |
| toFor.setBody(bodyStmt); |
| } else { |
| toFor.setBody(new JsEmpty(info)); |
| } |
| return toFor; |
| } |
| } |
| |
| private JsExpression mapFunction(Node fnNode) throws JsParserException { |
| |
| Node fromFnNameNode = fnNode.getFirstChild(); |
| Node fromParamNode = fnNode.getFirstChild().getNext().getFirstChild(); |
| Node fromBodyNode = fnNode.getFirstChild().getNext().getNext(); |
| |
| // Decide the function's name, if any. |
| // |
| String fromFnName = fromFnNameNode.getString(); |
| JsName toFnName = null; |
| if (fromFnName != null && fromFnName.length() > 0) { |
| toFnName = getScope().declareName(fromFnName); |
| } |
| |
| // Create it, and set the params. |
| // |
| SourceInfo fnSourceInfo = makeSourceInfo(fnNode); |
| JsFunction toFn = new JsFunction(fnSourceInfo, getScope(), toFnName); |
| |
| // Creating a function also creates a new scope, which we push onto |
| // the scope stack. |
| // |
| pushScope(toFn.getScope(), fnSourceInfo); |
| |
| while (fromParamNode != null) { |
| String fromParamName = fromParamNode.getString(); |
| // should this be unique? I think not since you can have dup args. |
| JsName paramName = toFn.getScope().declareName(fromParamName); |
| toFn.getParameters().add(new JsParameter(fnSourceInfo, paramName)); |
| fromParamNode = fromParamNode.getNext(); |
| } |
| |
| // Map the function's body. |
| // |
| JsBlock toBody = mapBlock(fromBodyNode); |
| toFn.setBody(toBody); |
| |
| // Pop the new function's scope off of the scope stack. |
| // |
| popScope(); |
| |
| return toFn; |
| } |
| |
| private JsArrayAccess mapGetElem(Node getElemNode) throws JsParserException { |
| Node from1 = getElemNode.getFirstChild(); |
| Node from2 = from1.getNext(); |
| |
| JsExpression to1 = mapExpression(from1); |
| JsExpression to2 = mapExpression(from2); |
| |
| return new JsArrayAccess(makeSourceInfo(getElemNode), to1, to2); |
| } |
| |
| private JsNameRef mapGetProp(Node getPropNode) throws JsParserException { |
| Node from1 = getPropNode.getFirstChild(); |
| Node from2 = from1.getNext(); |
| |
| JsExpression toQualifier = mapExpression(from1); |
| JsNameRef toNameRef; |
| if (from2 != null) { |
| toNameRef = mapAsPropertyNameRef(from2); |
| } else { |
| // Special properties don't have a second expression. |
| // |
| Object obj = getPropNode.getProp(Node.SPECIAL_PROP_PROP); |
| assert (obj instanceof String); |
| toNameRef = new JsNameRef(makeSourceInfo(getPropNode), (String) obj); |
| } |
| toNameRef.setQualifier(toQualifier); |
| |
| return toNameRef; |
| } |
| |
| private JsIf mapIfStatement(Node ifNode) throws JsParserException { |
| |
| // Pull out the pieces we want to map. |
| // |
| Node fromTestExpr = ifNode.getFirstChild(); |
| Node fromThenBlock = ifNode.getFirstChild().getNext(); |
| Node fromElseBlock = ifNode.getFirstChild().getNext().getNext(); |
| |
| // Create the "if" statement we're mapping to. |
| // |
| JsIf toIf = new JsIf(makeSourceInfo(ifNode)); |
| |
| // Map the test expression. |
| // |
| JsExpression toTestExpr = mapExpression(fromTestExpr); |
| toIf.setIfExpr(toTestExpr); |
| |
| // Map the "then" block. |
| // |
| toIf.setThenStmt(mapStatement(fromThenBlock)); |
| |
| // Map the "else" block. |
| // |
| if (fromElseBlock != null) { |
| toIf.setElseStmt(mapStatement(fromElseBlock)); |
| } |
| |
| return toIf; |
| } |
| |
| private JsExpression mapIncDecFixity(JsUnaryOperator op, Node node) |
| throws JsParserException { |
| switch (node.getIntDatum()) { |
| case TokenStream.PRE: |
| return mapPrefixOperation(op, node); |
| case TokenStream.POST: |
| return mapPostfixOperation(op, node); |
| default: |
| throw createParserException( |
| "Unknown prefix/postfix variant: " + node.getIntDatum(), node); |
| } |
| } |
| |
| private JsLabel mapLabel(Node labelNode) throws JsParserException { |
| String fromName = labelNode.getFirstChild().getString(); |
| JsName toName = getScope().declareName(fromName); |
| Node fromStmt = labelNode.getFirstChild().getNext(); |
| JsLabel toLabel = new JsLabel(makeSourceInfo(labelNode), toName); |
| toLabel.setStmt(mapStatement(fromStmt)); |
| return toLabel; |
| } |
| |
| /** |
| * Creates a reference to a name that may or may not be obfuscatable, based on |
| * whether it matches a known name in the scope. |
| */ |
| private JsNameRef mapName(Node node) { |
| String ident = node.getString(); |
| return new JsNameRef(makeSourceInfo(node), ident); |
| } |
| |
| private JsNew mapNew(Node newNode) throws JsParserException { |
| // Map the constructor expression, which is often just the name of |
| // some lambda. |
| // |
| Node fromCtorExpr = newNode.getFirstChild(); |
| JsNew newExpr = new JsNew(makeSourceInfo(newNode), |
| mapExpression(fromCtorExpr)); |
| |
| // Iterate over and map the arguments. |
| // |
| List<JsExpression> args = newExpr.getArguments(); |
| Node fromArg = fromCtorExpr.getNext(); |
| while (fromArg != null) { |
| args.add(mapExpression(fromArg)); |
| fromArg = fromArg.getNext(); |
| } |
| |
| return newExpr; |
| } |
| |
| private JsExpression mapNumber(Node numberNode) { |
| return new JsNumberLiteral(makeSourceInfo(numberNode), |
| numberNode.getDouble()); |
| } |
| |
| private JsExpression mapObjectLiteral(Node objectLiteralNode) throws JsParserException { |
| JsObjectLiteral.Builder objectLiteralBuilder = |
| JsObjectLiteral.builder(makeSourceInfo(objectLiteralNode)); |
| for (Node propertyComponent = objectLiteralNode.getFirstChild(); propertyComponent != null; |
| propertyComponent = propertyComponent.getNext()) { |
| |
| // get the property label from the first node. |
| JsExpression labelExpression = mapExpression(propertyComponent); |
| |
| // advance to the value node |
| propertyComponent = propertyComponent.getNext(); |
| |
| Node valueNode = propertyComponent; |
| if (valueNode == null) { |
| throw createParserException("Expected an init expression for: " |
| + labelExpression, objectLiteralNode); |
| } |
| objectLiteralBuilder.add( |
| labelExpression.getSourceInfo(), labelExpression, mapExpression(valueNode)); |
| } |
| return objectLiteralBuilder.build(); |
| } |
| private JsExpression mapOptionalExpression(Node exprNode) |
| throws JsParserException { |
| JsNode unknown = map(exprNode); |
| if (unknown != null) { |
| if (unknown instanceof JsExpression) { |
| return (JsExpression) unknown; |
| } else { |
| throw createParserException("Expecting an expression or null", exprNode); |
| } |
| } |
| return null; |
| } |
| |
| private JsExpression mapPostfixOperation(JsUnaryOperator op, Node node) |
| throws JsParserException { |
| Node from = node.getFirstChild(); |
| JsExpression to = mapExpression(from); |
| return new JsPostfixOperation(makeSourceInfo(node), op, to); |
| } |
| |
| private JsExpression mapPrefixOperation(JsUnaryOperator op, Node node) |
| throws JsParserException { |
| Node from = node.getFirstChild(); |
| JsExpression to = mapExpression(from); |
| return new JsPrefixOperation(makeSourceInfo(node), op, to); |
| } |
| |
| private JsExpression mapPrimary(Node node) throws JsParserException { |
| switch (node.getIntDatum()) { |
| case TokenStream.THIS: |
| return new JsThisRef(makeSourceInfo(node)); |
| |
| case TokenStream.TRUE: |
| return JsBooleanLiteral.TRUE; |
| |
| case TokenStream.FALSE: |
| return JsBooleanLiteral.FALSE; |
| |
| case TokenStream.NULL: |
| return JsNullLiteral.INSTANCE; |
| |
| case TokenStream.UNDEFINED: |
| return new JsNameRef(makeSourceInfo(node), |
| JsRootScope.INSTANCE.getUndefined()); |
| |
| default: |
| throw createParserException("Unknown primary: " + node.getIntDatum(), |
| node); |
| } |
| } |
| |
| private JsNode mapRegExp(Node regExpNode) { |
| JsRegExp toRegExp = new JsRegExp(makeSourceInfo(regExpNode)); |
| |
| Node fromPattern = regExpNode.getFirstChild(); |
| toRegExp.setPattern(fromPattern.getString()); |
| |
| Node fromFlags = fromPattern.getNext(); |
| if (fromFlags != null) { |
| toRegExp.setFlags(fromFlags.getString()); |
| } |
| |
| return toRegExp; |
| } |
| |
| private JsExpression mapRelationalVariant(Node relNode) |
| throws JsParserException { |
| switch (relNode.getIntDatum()) { |
| case TokenStream.LT: |
| return mapBinaryOperation(JsBinaryOperator.LT, relNode); |
| |
| case TokenStream.LE: |
| return mapBinaryOperation(JsBinaryOperator.LTE, relNode); |
| |
| case TokenStream.GT: |
| return mapBinaryOperation(JsBinaryOperator.GT, relNode); |
| |
| case TokenStream.GE: |
| return mapBinaryOperation(JsBinaryOperator.GTE, relNode); |
| |
| case TokenStream.INSTANCEOF: |
| return mapBinaryOperation(JsBinaryOperator.INSTANCEOF, relNode); |
| |
| case TokenStream.IN: |
| return mapBinaryOperation(JsBinaryOperator.INOP, relNode); |
| |
| default: |
| throw createParserException("Unknown relational operator variant: " |
| + relNode.getIntDatum(), relNode); |
| } |
| } |
| |
| private JsReturn mapReturn(Node returnNode) throws JsParserException { |
| SourceInfo info = makeSourceInfo(returnNode); |
| JsReturn toReturn = new JsReturn(info); |
| pushSourceInfo(info); |
| Node from = returnNode.getFirstChild(); |
| if (from != null) { |
| JsExpression to = mapExpression(from); |
| toReturn.setExpr(to); |
| } |
| |
| popSourceInfo(); |
| return toReturn; |
| } |
| |
| private JsExpression mapSetElem(Node setElemNode) throws JsParserException { |
| // Reuse the get elem code. |
| // |
| JsArrayAccess lhs = mapGetElem(setElemNode); |
| |
| // Map the RHS. |
| // |
| Node fromRhs = setElemNode.getFirstChild().getNext().getNext(); |
| JsExpression toRhs = mapExpression(fromRhs); |
| |
| return new JsBinaryOperation(makeSourceInfo(setElemNode), |
| JsBinaryOperator.ASG, lhs, toRhs); |
| } |
| |
| private JsExpression mapSetProp(Node getPropNode) throws JsParserException { |
| // Reuse the get prop code. |
| // |
| JsNameRef lhs = mapGetProp(getPropNode); |
| |
| // Map the RHS. |
| // |
| Node fromRhs = getPropNode.getFirstChild().getNext().getNext(); |
| JsExpression toRhs = mapExpression(fromRhs); |
| |
| return new JsBinaryOperation(makeSourceInfo(getPropNode), |
| JsBinaryOperator.ASG, lhs, toRhs); |
| } |
| |
| private JsExpression mapShiftVariant(Node shiftNode) throws JsParserException { |
| switch (shiftNode.getIntDatum()) { |
| case TokenStream.LSH: |
| return mapBinaryOperation(JsBinaryOperator.SHL, shiftNode); |
| |
| case TokenStream.RSH: |
| return mapBinaryOperation(JsBinaryOperator.SHR, shiftNode); |
| |
| case TokenStream.URSH: |
| return mapBinaryOperation(JsBinaryOperator.SHRU, shiftNode); |
| |
| default: |
| throw createParserException("Unknown equality operator variant: " |
| + shiftNode.getIntDatum(), shiftNode); |
| } |
| } |
| |
| private JsStatement mapStatement(Node nodeStmt) throws JsParserException { |
| JsNode unknown = map(nodeStmt); |
| if (unknown != null) { |
| if (unknown instanceof JsStatement) { |
| return (JsStatement) unknown; |
| } else if (unknown instanceof JsExpression) { |
| return ((JsExpression) unknown).makeStmt(); |
| } else { |
| throw createParserException("Expecting a statement", nodeStmt); |
| } |
| } else { |
| // When map() returns null, we return an empty statement. |
| // |
| return new JsEmpty(makeSourceInfo(nodeStmt)); |
| } |
| } |
| |
| private void mapStatements(List<JsStatement> stmts, Node nodeStmts) |
| throws JsParserException { |
| Node curr = nodeStmts.getFirstChild(); |
| while (curr != null) { |
| JsStatement stmt = mapStatement(curr); |
| if (stmt != null) { |
| stmts.add(stmt); |
| } else { |
| // When mapStatement() returns null, we just ignore it. |
| // |
| } |
| curr = curr.getNext(); |
| } |
| } |
| |
| private List<JsStatement> mapStatements(Node nodeStmts) |
| throws JsParserException { |
| List<JsStatement> stmts = new ArrayList<JsStatement>(); |
| mapStatements(stmts, nodeStmts); |
| return stmts; |
| } |
| |
| private JsSwitch mapSwitchStatement(Node switchNode) throws JsParserException { |
| SourceInfo info = makeSourceInfo(switchNode); |
| JsSwitch toSwitch = new JsSwitch(info); |
| pushSourceInfo(info); |
| |
| // The switch expression. |
| // |
| Node fromSwitchExpr = switchNode.getFirstChild(); |
| toSwitch.setExpr(mapExpression(fromSwitchExpr)); |
| |
| // The members. |
| // |
| Node fromMember = fromSwitchExpr.getNext(); |
| while (fromMember != null) { |
| if (fromMember.getType() == TokenStream.CASE) { |
| JsCase toCase = new JsCase(makeSourceInfo(fromMember)); |
| |
| // Set the case expression. In JS, this can be any expression. |
| // |
| Node fromCaseExpr = fromMember.getFirstChild(); |
| toCase.setCaseExpr(mapExpression(fromCaseExpr)); |
| |
| // Set the case statements. |
| // |
| Node fromCaseBlock = fromCaseExpr.getNext(); |
| mapStatements(toCase.getStmts(), fromCaseBlock); |
| |
| // Attach the case to the switch. |
| // |
| toSwitch.getCases().add(toCase); |
| } else { |
| // This should be the only default statement. |
| // If more than one is present, we keep the last one. |
| // |
| assert (fromMember.getType() == TokenStream.DEFAULT); |
| JsDefault toDefault = new JsDefault(makeSourceInfo(fromMember)); |
| |
| // Set the default statements. |
| // |
| Node fromDefaultBlock = fromMember.getFirstChild(); |
| mapStatements(toDefault.getStmts(), fromDefaultBlock); |
| |
| // Attach the default to the switch. |
| // |
| toSwitch.getCases().add(toDefault); |
| } |
| fromMember = fromMember.getNext(); |
| } |
| |
| popSourceInfo(); |
| return toSwitch; |
| } |
| |
| private JsThrow mapThrowStatement(Node throwNode) throws JsParserException { |
| SourceInfo info = makeSourceInfo(throwNode); |
| pushSourceInfo(info); |
| |
| // Create, map, and attach. |
| // |
| Node fromExpr = throwNode.getFirstChild(); |
| JsThrow toThrow = new JsThrow(info, mapExpression(fromExpr)); |
| |
| popSourceInfo(); |
| return toThrow; |
| } |
| |
| private JsTry mapTryStatement(Node tryNode) throws JsParserException { |
| JsTry toTry = new JsTry(makeSourceInfo(tryNode)); |
| |
| // Map the "try" body. |
| // |
| Node fromTryBody = tryNode.getFirstChild(); |
| toTry.setTryBlock(mapBlock(fromTryBody)); |
| |
| // Map zero or more catch blocks. |
| // |
| Node fromCatchNodes = fromTryBody.getNext(); |
| Node fromCatchNode = fromCatchNodes.getFirstChild(); |
| while (fromCatchNode != null) { |
| assert (fromCatchNode.getType() == TokenStream.CATCH); |
| // Map the catch variable. |
| // |
| Node fromCatchVarName = fromCatchNode.getFirstChild(); |
| JsCatch catchBlock = new JsCatch(makeSourceInfo(fromCatchNode), |
| getScope(), fromCatchVarName.getString()); |
| |
| // Pre-advance to the next catch block, if any. |
| // We do this here to decide whether or not this is the last one. |
| // |
| fromCatchNode = fromCatchNode.getNext(); |
| |
| // Map the condition, with a little fixup based on whether or not |
| // this is the last catch block. |
| // |
| Node fromCondition = fromCatchVarName.getNext(); |
| JsExpression toCondition = mapExpression(fromCondition); |
| catchBlock.setCondition(toCondition); |
| if (fromCatchNode == null) { |
| if (toCondition instanceof JsBooleanLiteral) { |
| if (((JsBooleanLiteral) toCondition).getValue()) { |
| // Actually, this is an unconditional catch block. |
| // Indicate that by nulling the condition. |
| // |
| catchBlock.setCondition(null); |
| } |
| } |
| } |
| |
| // Map the catch body. |
| // |
| Node fromCatchBody = fromCondition.getNext(); |
| pushScope(catchBlock.getScope(), catchBlock.getSourceInfo()); |
| catchBlock.setBody(mapBlock(fromCatchBody)); |
| popScope(); |
| |
| // Attach it. |
| // |
| toTry.getCatches().add(catchBlock); |
| } |
| |
| Node fromFinallyNode = fromCatchNodes.getNext(); |
| if (fromFinallyNode != null) { |
| toTry.setFinallyBlock(mapBlock(fromFinallyNode)); |
| } |
| |
| return toTry; |
| } |
| |
| private JsExpression mapUnaryVariant(Node unOp) throws JsParserException { |
| switch (unOp.getIntDatum()) { |
| case TokenStream.SUB: |
| return mapPrefixOperation(JsUnaryOperator.NEG, unOp); |
| |
| case TokenStream.NOT: |
| return mapPrefixOperation(JsUnaryOperator.NOT, unOp); |
| |
| case TokenStream.BITNOT: |
| return mapPrefixOperation(JsUnaryOperator.BIT_NOT, unOp); |
| |
| case TokenStream.TYPEOF: |
| return mapPrefixOperation(JsUnaryOperator.TYPEOF, unOp); |
| |
| case TokenStream.ADD: |
| if (unOp.getFirstChild().getType() != TokenStream.NUMBER) { |
| return mapPrefixOperation(JsUnaryOperator.POS, unOp); |
| } else { |
| // Pretend we didn't see it. |
| return mapExpression(unOp.getFirstChild()); |
| } |
| |
| case TokenStream.VOID: |
| return mapPrefixOperation(JsUnaryOperator.VOID, unOp); |
| |
| default: |
| throw createParserException( |
| "Unknown unary operator variant: " + unOp.getIntDatum(), unOp); |
| } |
| } |
| |
| private JsVars mapVar(Node varNode) throws JsParserException { |
| SourceInfo info = makeSourceInfo(varNode); |
| pushSourceInfo(info); |
| JsVars toVars = new JsVars(info); |
| Node fromVar = varNode.getFirstChild(); |
| while (fromVar != null) { |
| // Use a conservative name allocation strategy that allocates all names |
| // from the function's scope, even the names of properties in field |
| // literals. |
| // |
| String fromName = fromVar.getString(); |
| JsName toName = getScope().declareName(fromName); |
| JsVars.JsVar toVar = new JsVars.JsVar(makeSourceInfo(fromVar), toName); |
| |
| Node fromInit = fromVar.getFirstChild(); |
| if (fromInit != null) { |
| JsExpression toInit = mapExpression(fromInit); |
| toVar.setInitExpr(toInit); |
| } |
| toVars.add(toVar); |
| |
| fromVar = fromVar.getNext(); |
| } |
| |
| popSourceInfo(); |
| return toVars; |
| } |
| |
| private JsNode mapWithStatement(Node withNode) throws JsParserException { |
| // The "with" statement is unsupported because it introduces ambiguity |
| // related to whether or not a name is obfuscatable that we cannot resolve |
| // statically. This is modified in our copy of the Rhino Parser to provide |
| // detailed source & line info. So, this method should never actually be |
| // called. |
| // |
| throw createParserException("Internal error: unexpected token 'with'", |
| withNode); |
| } |
| |
| private void popScope() { |
| scopeStack.pop(); |
| sourceInfoStack.pop(); |
| } |
| |
| private void popSourceInfo() { |
| sourceInfoStack.pop(); |
| } |
| |
| private void pushScope(JsScope scope, SourceInfo sourceInfo) { |
| scopeStack.push(scope); |
| sourceInfoStack.push(sourceInfo); |
| } |
| |
| /** |
| * This should be called when processing any Rhino statement Node that has |
| * line number data so that enclosed expressions will have a useful source |
| * location. |
| * |
| * @see Node#hasLineno |
| */ |
| private void pushSourceInfo(SourceInfo sourceInfo) { |
| assert sourceInfo.getStartLine() >= 0 : "Bad SourceInfo line number"; |
| sourceInfoStack.push(sourceInfo); |
| } |
| } |