blob: b72d89961e2cb2a27440c2a6f611421687d19216 [file] [log] [blame]
/*
* Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.nashorn.internal.ir.debug;
import static jdk.nashorn.internal.runtime.Source.sourceFor;
import java.util.ArrayList;
import java.util.List;
import jdk.nashorn.internal.ir.AccessNode;
import jdk.nashorn.internal.ir.BinaryNode;
import jdk.nashorn.internal.ir.Block;
import jdk.nashorn.internal.ir.BlockStatement;
import jdk.nashorn.internal.ir.BreakNode;
import jdk.nashorn.internal.ir.CallNode;
import jdk.nashorn.internal.ir.CaseNode;
import jdk.nashorn.internal.ir.CatchNode;
import jdk.nashorn.internal.ir.ContinueNode;
import jdk.nashorn.internal.ir.EmptyNode;
import jdk.nashorn.internal.ir.Expression;
import jdk.nashorn.internal.ir.ExpressionStatement;
import jdk.nashorn.internal.ir.ForNode;
import jdk.nashorn.internal.ir.FunctionNode;
import jdk.nashorn.internal.ir.IdentNode;
import jdk.nashorn.internal.ir.IfNode;
import jdk.nashorn.internal.ir.IndexNode;
import jdk.nashorn.internal.ir.JoinPredecessorExpression;
import jdk.nashorn.internal.ir.LabelNode;
import jdk.nashorn.internal.ir.LiteralNode;
import jdk.nashorn.internal.ir.Node;
import jdk.nashorn.internal.ir.ObjectNode;
import jdk.nashorn.internal.ir.PropertyNode;
import jdk.nashorn.internal.ir.ReturnNode;
import jdk.nashorn.internal.ir.RuntimeNode;
import jdk.nashorn.internal.ir.SplitNode;
import jdk.nashorn.internal.ir.Statement;
import jdk.nashorn.internal.ir.SwitchNode;
import jdk.nashorn.internal.ir.TernaryNode;
import jdk.nashorn.internal.ir.ThrowNode;
import jdk.nashorn.internal.ir.TryNode;
import jdk.nashorn.internal.ir.UnaryNode;
import jdk.nashorn.internal.ir.VarNode;
import jdk.nashorn.internal.ir.WhileNode;
import jdk.nashorn.internal.ir.WithNode;
import jdk.nashorn.internal.ir.visitor.SimpleNodeVisitor;
import jdk.nashorn.internal.parser.JSONParser;
import jdk.nashorn.internal.parser.Lexer.RegexToken;
import jdk.nashorn.internal.parser.Parser;
import jdk.nashorn.internal.parser.TokenType;
import jdk.nashorn.internal.runtime.Context;
import jdk.nashorn.internal.runtime.ParserException;
import jdk.nashorn.internal.runtime.Source;
/**
* This IR writer produces a JSON string that represents AST as a JSON string.
*/
public final class JSONWriter extends SimpleNodeVisitor {
/**
* Returns AST as JSON compatible string.
*
* @param context context
* @param code code to be parsed
* @param name name of the code source (used for location)
* @param includeLoc tells whether to include location information for nodes or not
* @return JSON string representation of AST of the supplied code
*/
public static String parse(final Context context, final String code, final String name, final boolean includeLoc) {
final Parser parser = new Parser(context.getEnv(), sourceFor(name, code), new Context.ThrowErrorManager(), context.getEnv()._strict, context.getLogger(Parser.class));
final JSONWriter jsonWriter = new JSONWriter(includeLoc);
try {
final FunctionNode functionNode = parser.parse(); //symbol name is ":program", default
functionNode.accept(jsonWriter);
return jsonWriter.getString();
} catch (final ParserException e) {
e.throwAsEcmaException();
return null;
}
}
@Override
public boolean enterJoinPredecessorExpression(final JoinPredecessorExpression joinPredecessorExpression) {
final Expression expr = joinPredecessorExpression.getExpression();
if(expr != null) {
expr.accept(this);
} else {
nullValue();
}
return false;
}
@Override
protected boolean enterDefault(final Node node) {
objectStart();
location(node);
return true;
}
private boolean leave() {
objectEnd();
return false;
}
@Override
protected Node leaveDefault(final Node node) {
objectEnd();
return null;
}
@Override
public boolean enterAccessNode(final AccessNode accessNode) {
enterDefault(accessNode);
type("MemberExpression");
comma();
property("object");
accessNode.getBase().accept(this);
comma();
property("property", accessNode.getProperty());
comma();
property("computed", false);
return leave();
}
@Override
public boolean enterBlock(final Block block) {
enterDefault(block);
type("BlockStatement");
comma();
array("body", block.getStatements());
return leave();
}
@Override
public boolean enterBinaryNode(final BinaryNode binaryNode) {
enterDefault(binaryNode);
final String name;
if (binaryNode.isAssignment()) {
name = "AssignmentExpression";
} else if (binaryNode.isLogical()) {
name = "LogicalExpression";
} else {
name = "BinaryExpression";
}
type(name);
comma();
property("operator", binaryNode.tokenType().getName());
comma();
property("left");
binaryNode.lhs().accept(this);
comma();
property("right");
binaryNode.rhs().accept(this);
return leave();
}
@Override
public boolean enterBreakNode(final BreakNode breakNode) {
enterDefault(breakNode);
type("BreakStatement");
comma();
final String label = breakNode.getLabelName();
if(label != null) {
property("label", label);
} else {
property("label");
nullValue();
}
return leave();
}
@Override
public boolean enterCallNode(final CallNode callNode) {
enterDefault(callNode);
type("CallExpression");
comma();
property("callee");
callNode.getFunction().accept(this);
comma();
array("arguments", callNode.getArgs());
return leave();
}
@Override
public boolean enterCaseNode(final CaseNode caseNode) {
enterDefault(caseNode);
type("SwitchCase");
comma();
final Node test = caseNode.getTest();
property("test");
if (test != null) {
test.accept(this);
} else {
nullValue();
}
comma();
array("consequent", caseNode.getBody().getStatements());
return leave();
}
@Override
public boolean enterCatchNode(final CatchNode catchNode) {
enterDefault(catchNode);
type("CatchClause");
comma();
property("param");
catchNode.getException().accept(this);
comma();
final Node guard = catchNode.getExceptionCondition();
if (guard != null) {
property("guard");
guard.accept(this);
comma();
}
property("body");
catchNode.getBody().accept(this);
return leave();
}
@Override
public boolean enterContinueNode(final ContinueNode continueNode) {
enterDefault(continueNode);
type("ContinueStatement");
comma();
final String label = continueNode.getLabelName();
if(label != null) {
property("label", label);
} else {
property("label");
nullValue();
}
return leave();
}
@Override
public boolean enterEmptyNode(final EmptyNode emptyNode) {
enterDefault(emptyNode);
type("EmptyStatement");
return leave();
}
@Override
public boolean enterExpressionStatement(final ExpressionStatement expressionStatement) {
// handle debugger statement
final Node expression = expressionStatement.getExpression();
if (expression instanceof RuntimeNode) {
expression.accept(this);
return false;
}
enterDefault(expressionStatement);
type("ExpressionStatement");
comma();
property("expression");
expression.accept(this);
return leave();
}
@Override
public boolean enterBlockStatement(final BlockStatement blockStatement) {
enterDefault(blockStatement);
type("BlockStatement");
comma();
property("block");
blockStatement.getBlock().accept(this);
return leave();
}
@Override
public boolean enterForNode(final ForNode forNode) {
enterDefault(forNode);
if (forNode.isForIn() || (forNode.isForEach() && forNode.getInit() != null)) {
type("ForInStatement");
comma();
final Node init = forNode.getInit();
assert init != null;
property("left");
init.accept(this);
comma();
final Node modify = forNode.getModify();
assert modify != null;
property("right");
modify.accept(this);
comma();
property("body");
forNode.getBody().accept(this);
comma();
property("each", forNode.isForEach());
} else {
type("ForStatement");
comma();
final Node init = forNode.getInit();
property("init");
if (init != null) {
init.accept(this);
} else {
nullValue();
}
comma();
final Node test = forNode.getTest();
property("test");
if (test != null) {
test.accept(this);
} else {
nullValue();
}
comma();
final Node update = forNode.getModify();
property("update");
if (update != null) {
update.accept(this);
} else {
nullValue();
}
comma();
property("body");
forNode.getBody().accept(this);
}
return leave();
}
@Override
public boolean enterFunctionNode(final FunctionNode functionNode) {
final boolean program = functionNode.isProgram();
if (program) {
return emitProgram(functionNode);
}
enterDefault(functionNode);
final String name;
if (functionNode.isDeclared()) {
name = "FunctionDeclaration";
} else {
name = "FunctionExpression";
}
type(name);
comma();
property("id");
final FunctionNode.Kind kind = functionNode.getKind();
if (functionNode.isAnonymous() || kind == FunctionNode.Kind.GETTER || kind == FunctionNode.Kind.SETTER) {
nullValue();
} else {
functionNode.getIdent().accept(this);
}
comma();
array("params", functionNode.getParameters());
comma();
arrayStart("defaults");
arrayEnd();
comma();
property("rest");
nullValue();
comma();
property("body");
functionNode.getBody().accept(this);
comma();
property("generator", false);
comma();
property("expression", false);
return leave();
}
private boolean emitProgram(final FunctionNode functionNode) {
enterDefault(functionNode);
type("Program");
comma();
// body consists of nested functions and statements
final List<Statement> stats = functionNode.getBody().getStatements();
final int size = stats.size();
int idx = 0;
arrayStart("body");
for (final Node stat : stats) {
stat.accept(this);
if (idx != (size - 1)) {
comma();
}
idx++;
}
arrayEnd();
return leave();
}
@Override
public boolean enterIdentNode(final IdentNode identNode) {
enterDefault(identNode);
final String name = identNode.getName();
if ("this".equals(name)) {
type("ThisExpression");
} else {
type("Identifier");
comma();
property("name", identNode.getName());
}
return leave();
}
@Override
public boolean enterIfNode(final IfNode ifNode) {
enterDefault(ifNode);
type("IfStatement");
comma();
property("test");
ifNode.getTest().accept(this);
comma();
property("consequent");
ifNode.getPass().accept(this);
final Node elsePart = ifNode.getFail();
comma();
property("alternate");
if (elsePart != null) {
elsePart.accept(this);
} else {
nullValue();
}
return leave();
}
@Override
public boolean enterIndexNode(final IndexNode indexNode) {
enterDefault(indexNode);
type("MemberExpression");
comma();
property("object");
indexNode.getBase().accept(this);
comma();
property("property");
indexNode.getIndex().accept(this);
comma();
property("computed", true);
return leave();
}
@Override
public boolean enterLabelNode(final LabelNode labelNode) {
enterDefault(labelNode);
type("LabeledStatement");
comma();
property("label", labelNode.getLabelName());
comma();
property("body");
labelNode.getBody().accept(this);
return leave();
}
@SuppressWarnings("rawtypes")
@Override
public boolean enterLiteralNode(final LiteralNode literalNode) {
enterDefault(literalNode);
if (literalNode instanceof LiteralNode.ArrayLiteralNode) {
type("ArrayExpression");
comma();
array("elements", ((LiteralNode.ArrayLiteralNode)literalNode).getElementExpressions());
} else {
type("Literal");
comma();
property("value");
final Object value = literalNode.getValue();
if (value instanceof RegexToken) {
// encode RegExp literals as Strings of the form /.../<flags>
final RegexToken regex = (RegexToken)value;
final StringBuilder regexBuf = new StringBuilder();
regexBuf.append('/');
regexBuf.append(regex.getExpression());
regexBuf.append('/');
regexBuf.append(regex.getOptions());
buf.append(quote(regexBuf.toString()));
} else {
final String str = literalNode.getString();
// encode every String literal with prefix '$' so that script
// can differentiate b/w RegExps as Strings and Strings.
buf.append(literalNode.isString()? quote("$" + str) : str);
}
}
return leave();
}
@Override
public boolean enterObjectNode(final ObjectNode objectNode) {
enterDefault(objectNode);
type("ObjectExpression");
comma();
array("properties", objectNode.getElements());
return leave();
}
@Override
public boolean enterPropertyNode(final PropertyNode propertyNode) {
final Node key = propertyNode.getKey();
final Node value = propertyNode.getValue();
if (value != null) {
objectStart();
location(propertyNode);
property("key");
key.accept(this);
comma();
property("value");
value.accept(this);
comma();
property("kind", "init");
objectEnd();
} else {
// getter
final Node getter = propertyNode.getGetter();
if (getter != null) {
objectStart();
location(propertyNode);
property("key");
key.accept(this);
comma();
property("value");
getter.accept(this);
comma();
property("kind", "get");
objectEnd();
}
// setter
final Node setter = propertyNode.getSetter();
if (setter != null) {
if (getter != null) {
comma();
}
objectStart();
location(propertyNode);
property("key");
key.accept(this);
comma();
property("value");
setter.accept(this);
comma();
property("kind", "set");
objectEnd();
}
}
return false;
}
@Override
public boolean enterReturnNode(final ReturnNode returnNode) {
enterDefault(returnNode);
type("ReturnStatement");
comma();
final Node arg = returnNode.getExpression();
property("argument");
if (arg != null) {
arg.accept(this);
} else {
nullValue();
}
return leave();
}
@Override
public boolean enterRuntimeNode(final RuntimeNode runtimeNode) {
final RuntimeNode.Request req = runtimeNode.getRequest();
if (req == RuntimeNode.Request.DEBUGGER) {
enterDefault(runtimeNode);
type("DebuggerStatement");
return leave();
}
return false;
}
@Override
public boolean enterSplitNode(final SplitNode splitNode) {
return false;
}
@Override
public boolean enterSwitchNode(final SwitchNode switchNode) {
enterDefault(switchNode);
type("SwitchStatement");
comma();
property("discriminant");
switchNode.getExpression().accept(this);
comma();
array("cases", switchNode.getCases());
return leave();
}
@Override
public boolean enterTernaryNode(final TernaryNode ternaryNode) {
enterDefault(ternaryNode);
type("ConditionalExpression");
comma();
property("test");
ternaryNode.getTest().accept(this);
comma();
property("consequent");
ternaryNode.getTrueExpression().accept(this);
comma();
property("alternate");
ternaryNode.getFalseExpression().accept(this);
return leave();
}
@Override
public boolean enterThrowNode(final ThrowNode throwNode) {
enterDefault(throwNode);
type("ThrowStatement");
comma();
property("argument");
throwNode.getExpression().accept(this);
return leave();
}
@Override
public boolean enterTryNode(final TryNode tryNode) {
enterDefault(tryNode);
type("TryStatement");
comma();
property("block");
tryNode.getBody().accept(this);
comma();
final List<? extends Node> catches = tryNode.getCatches();
final List<CatchNode> guarded = new ArrayList<>();
CatchNode unguarded = null;
if (catches != null) {
for (final Node n : catches) {
final CatchNode cn = (CatchNode)n;
if (cn.getExceptionCondition() != null) {
guarded.add(cn);
} else {
assert unguarded == null: "too many unguarded?";
unguarded = cn;
}
}
}
array("guardedHandlers", guarded);
comma();
property("handler");
if (unguarded != null) {
unguarded.accept(this);
} else {
nullValue();
}
comma();
property("finalizer");
final Node finallyNode = tryNode.getFinallyBody();
if (finallyNode != null) {
finallyNode.accept(this);
} else {
nullValue();
}
return leave();
}
@Override
public boolean enterUnaryNode(final UnaryNode unaryNode) {
enterDefault(unaryNode);
final TokenType tokenType = unaryNode.tokenType();
if (tokenType == TokenType.NEW) {
type("NewExpression");
comma();
final CallNode callNode = (CallNode)unaryNode.getExpression();
property("callee");
callNode.getFunction().accept(this);
comma();
array("arguments", callNode.getArgs());
} else {
final String operator;
final boolean prefix;
switch (tokenType) {
case INCPOSTFIX:
prefix = false;
operator = "++";
break;
case DECPOSTFIX:
prefix = false;
operator = "--";
break;
case INCPREFIX:
operator = "++";
prefix = true;
break;
case DECPREFIX:
operator = "--";
prefix = true;
break;
default:
prefix = true;
operator = tokenType.getName();
break;
}
type(unaryNode.isAssignment()? "UpdateExpression" : "UnaryExpression");
comma();
property("operator", operator);
comma();
property("prefix", prefix);
comma();
property("argument");
unaryNode.getExpression().accept(this);
}
return leave();
}
@Override
public boolean enterVarNode(final VarNode varNode) {
final Node init = varNode.getInit();
if (init instanceof FunctionNode && ((FunctionNode)init).isDeclared()) {
// function declaration - don't emit VariableDeclaration instead
// just emit FunctionDeclaration using 'init' Node.
init.accept(this);
return false;
}
enterDefault(varNode);
type("VariableDeclaration");
comma();
arrayStart("declarations");
// VariableDeclarator
objectStart();
location(varNode.getName());
type("VariableDeclarator");
comma();
property("id");
varNode.getName().accept(this);
comma();
property("init");
if (init != null) {
init.accept(this);
} else {
nullValue();
}
// VariableDeclarator
objectEnd();
// declarations
arrayEnd();
return leave();
}
@Override
public boolean enterWhileNode(final WhileNode whileNode) {
enterDefault(whileNode);
type(whileNode.isDoWhile() ? "DoWhileStatement" : "WhileStatement");
comma();
if (whileNode.isDoWhile()) {
property("body");
whileNode.getBody().accept(this);
comma();
property("test");
whileNode.getTest().accept(this);
} else {
property("test");
whileNode.getTest().accept(this);
comma();
property("body");
whileNode.getBody().accept(this);
}
return leave();
}
@Override
public boolean enterWithNode(final WithNode withNode) {
enterDefault(withNode);
type("WithStatement");
comma();
property("object");
withNode.getExpression().accept(this);
comma();
property("body");
withNode.getBody().accept(this);
return leave();
}
// Internals below
private JSONWriter(final boolean includeLocation) {
this.buf = new StringBuilder();
this.includeLocation = includeLocation;
}
private final StringBuilder buf;
private final boolean includeLocation;
private String getString() {
return buf.toString();
}
private void property(final String key, final String value, final boolean escape) {
buf.append('"');
buf.append(key);
buf.append("\":");
if (value != null) {
if (escape) {
buf.append('"');
}
buf.append(value);
if (escape) {
buf.append('"');
}
}
}
private void property(final String key, final String value) {
property(key, value, true);
}
private void property(final String key, final boolean value) {
property(key, Boolean.toString(value), false);
}
private void property(final String key, final int value) {
property(key, Integer.toString(value), false);
}
private void property(final String key) {
property(key, null);
}
private void type(final String value) {
property("type", value);
}
private void objectStart(final String name) {
buf.append('"');
buf.append(name);
buf.append("\":{");
}
private void objectStart() {
buf.append('{');
}
private void objectEnd() {
buf.append('}');
}
private void array(final String name, final List<? extends Node> nodes) {
// The size, idx comparison is just to avoid trailing comma..
final int size = nodes.size();
int idx = 0;
arrayStart(name);
for (final Node node : nodes) {
if (node != null) {
node.accept(this);
} else {
nullValue();
}
if (idx != (size - 1)) {
comma();
}
idx++;
}
arrayEnd();
}
private void arrayStart(final String name) {
buf.append('"');
buf.append(name);
buf.append('"');
buf.append(':');
buf.append('[');
}
private void arrayEnd() {
buf.append(']');
}
private void comma() {
buf.append(',');
}
private void nullValue() {
buf.append("null");
}
private void location(final Node node) {
if (includeLocation) {
objectStart("loc");
// source name
final Source src = lc.getCurrentFunction().getSource();
property("source", src.getName());
comma();
// start position
objectStart("start");
final int start = node.getStart();
property("line", src.getLine(start));
comma();
property("column", src.getColumn(start));
objectEnd();
comma();
// end position
objectStart("end");
final int end = node.getFinish();
property("line", src.getLine(end));
comma();
property("column", src.getColumn(end));
objectEnd();
// end 'loc'
objectEnd();
comma();
}
}
private static String quote(final String str) {
return JSONParser.quote(str);
}
}