/*
* ExpressionStringBuilder.java
*
* Copyright (c) 2012 Mike Strobel
*
* This source code is based on the Dynamic Language Runtime from Microsoft,
* Copyright (c) Microsoft Corporation.
*
* This source code is subject to terms and conditions of the Apache License, Version 2.0.
* A copy of the license can be found in the License.html file at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* Apache License, Version 2.0.
*
* You must not remove this notice, or any other, from this software.
*/
package com.strobel.expressions;
import com.strobel.core.StringUtilities;
import com.strobel.reflection.BindingFlags;
import com.strobel.reflection.CallingConvention;
import com.strobel.reflection.MemberInfo;
import com.strobel.reflection.MethodInfo;
import com.strobel.reflection.PrimitiveTypes;
import com.strobel.reflection.Type;
import com.strobel.util.TypeUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Set;
/**
* @author Mike Strobel
*/
final class ExpressionStringBuilder extends ExpressionVisitor {
private final static String lineSeparator = System.getProperty("line.separator");
private final StringBuilder _out;
private int _indentLevel = 0;
private int _blockDepth = 0;
private boolean _indentPending = false;
private HashMap<Object, Integer> _ids;
private ExpressionStringBuilder() {
_out = new StringBuilder();
}
private int getParameterId(final ParameterExpression p) {
if (_ids == null) {
_ids = new HashMap<>();
_ids.put(p, 0);
return 0;
}
else if (_ids.containsKey(p)) {
return _ids.get(p);
}
final int id = _ids.size();
_ids.put(p, id);
return id;
}
private void increaseIndent() {
++_indentLevel;
}
private void decreaseIndent() {
--_indentLevel;
}
private void flush() {
if (_indentPending) {
return;
}
_out.append(lineSeparator);
_indentPending = true;
}
private void applyIndent() {
if (!_indentPending) {
return;
}
for (int i = 0; i < _indentLevel; i++) {
_out.append(" ");
}
_indentPending = false;
}
private void out(final String s) {
applyIndent();
_out.append(s);
}
private void out(final char c) {
applyIndent();
_out.append(c);
}
private void outMember(final Expression instance, final MemberInfo member) {
if (instance != null) {
visit(instance);
out("." + member.getName());
}
else {
// For static members, include the type name
out(member.getDeclaringType().getName() + "." + member.getName());
}
}
@Override
public String toString() {
return _out.toString();
}
static String expressionToString(final Expression node) {
assert node != null;
final ExpressionStringBuilder esb = new ExpressionStringBuilder();
esb.visit(node);
return esb.toString();
}
static String catchBlockToString(final CatchBlock node) {
assert node != null;
final ExpressionStringBuilder esb = new ExpressionStringBuilder();
esb.visitCatchBlock(node);
return esb.toString();
}
static String switchCaseToString(final SwitchCase node) {
assert node != null;
final ExpressionStringBuilder esb = new ExpressionStringBuilder();
esb.visitSwitchCase(node);
return esb.toString();
}
private void visitList(final IArgumentProvider arguments) {
for (int i = 0, n = arguments.getArgumentCount(); i < n; i++) {
if (i != 0) {
out(", ");
}
visit(arguments.getArgument(i));
}
}
private void visitList(final ExpressionList<? extends Expression> expressions) {
for (int i = 0, n = expressions.size(); i < n; i++) {
if (i != 0) {
out(", ");
}
visit(expressions.get(i));
}
}
private <T extends Expression> void visitExpressions(final char open, final ExpressionList<T> expressions, final char close) {
out(open);
if (expressions != null) {
boolean isFirst = true;
for (final T e : expressions) {
if (isFirst) {
isFirst = false;
}
else {
out(", ");
}
visit(e);
}
}
out(close);
}
@Override
public Expression visit(final Expression node) {
return super.visit(node);
}
@Override
protected Expression visitDefaultValue(final DefaultValueExpression node) {
out("default(");
out(node.getType().getFullName());
out(')');
return node;
}
@Override
protected Expression visitExtension(final Expression node) {
// Prefer a toString override, if available.
final Set<BindingFlags> flags = BindingFlags.PublicInstanceExact;
final MethodInfo toString = node.getType().getMethod("toString", flags, CallingConvention.Standard, Type.EmptyTypes);
if (toString != null && toString.getDeclaringType() != Type.of(Expression.class)) {
out(node.toString());
return node;
}
out("[");
if (node.getNodeType() == ExpressionType.Extension) {
out(node.getType().getFullName());
}
else {
out(node.getNodeType().toString());
}
out("]");
return node;
}
@Override
protected Expression visitMember(final MemberExpression node) {
outMember(node.getTarget(), node.getMember());
return node;
}
@Override
protected Expression visitConstant(final ConstantExpression node) {
final Object value = node.getValue();
if (value == null) {
out("null");
}
else if (value instanceof Class<?>) {
out(((Class<?>)value).getSimpleName());
out(".class");
}
else if (value instanceof Type<?>) {
out(((Type<?>)value).getName());
out(".class");
}
else if (value instanceof Character) {
final char ch = (Character)value;
if (ch < 0x20 || ch > 0x7E) {
out(String.format("'\\u%1$04X'", (int)ch));
}
else {
out('\'');
out(ch);
out('\'');
}
}
else {
final String toString = value.getClass().isArray() ? arrayToString(value) : value.toString();
if (value instanceof String) {
out('"');
out(((String)value).replace("\r", "\\r").replace("\n", "\\n"));
out('"');
}
else if (toString.equals(value.getClass().toString())) {
out("value(");
out(toString);
out(')');
}
else {
out(toString);
}
}
return node;
}
private String arrayToString(final Object value) {
final Type<Object> type = Type.getType(value);
if (!type.isArray()) {
return value.toString();
}
switch (type.getKind()) {
case BOOLEAN:
return Arrays.toString((boolean[])value);
case BYTE:
return Arrays.toString((byte[])value);
case SHORT:
return Arrays.toString((short[])value);
case INT:
return Arrays.toString((int[])value);
case LONG:
return Arrays.toString((long[])value);
case CHAR:
return Arrays.toString((char[])value);
case FLOAT:
return Arrays.toString((float[])value);
case DOUBLE:
return Arrays.toString((double[])value);
default:
return Arrays.toString((Object[])value);
}
}
@Override
protected Expression visitParameter(final ParameterExpression node) {
if (StringUtilities.isNullOrEmpty(node.getName())) {
final int id = getParameterId(node);
out("Param_" + id);
}
else {
out(node.getName());
}
return node;
}
@Override
protected Expression visitUnary(final UnaryExpression node) {
switch (node.getNodeType()) {
case Convert:
out('(');
out(node.getType().getSimpleDescription());
out(')');
break;
case Not:
out("Not(");
break;
case Negate:
out("-");
break;
case UnaryPlus:
out("+");
break;
case Quote:
break;
case Throw:
out("throw(");
break;
case Increment:
out("Increment(");
break;
case Decrement:
out("Decrement(");
break;
case PreIncrementAssign:
out("++");
break;
case PreDecrementAssign:
out("--");
break;
case OnesComplement:
out("~(");
break;
default:
out(node.getNodeType().toString());
out("(");
break;
}
visit(node.getOperand());
switch (node.getNodeType()) {
case Negate:
case UnaryPlus:
case PreDecrementAssign:
case PreIncrementAssign:
case Quote:
break;
case PostIncrementAssign:
out("++");
break;
case PostDecrementAssign:
out("--");
break;
default:
out(")");
break;
}
return node;
}
@Override
protected Expression visitTypeBinary(final TypeBinaryExpression node) {
if (node.getNodeType() == ExpressionType.TypeEqual) {
return visit(node.reduceTypeEqual());
}
visit(node.getOperand());
out(" instanceof ");
out(node.getTypeOperand().getName());
return node;
}
@Override
protected Expression visitBinary(final BinaryExpression node) {
if (node.getNodeType() == ExpressionType.ArrayIndex) {
visit(node.getLeft());
out("[");
visit(node.getRight());
out("]");
}
else {
final String op;
switch (node.getNodeType()) {
case Assign:
op = "=";
break;
case Equal:
case ReferenceEqual:
op = "==";
break;
case NotEqual:
case ReferenceNotEqual:
op = "!=";
break;
case AndAlso:
op = "AndAlso";
break;
case OrElse:
op = "OrElse";
break;
case GreaterThan:
op = ">";
break;
case LessThan:
op = "<";
break;
case GreaterThanOrEqual:
op = ">=";
break;
case LessThanOrEqual:
op = "<=";
break;
case Add:
op = "+";
break;
case AddAssign:
op = "+=";
break;
case Subtract:
op = "-";
break;
case SubtractAssign:
op = "-=";
break;
case Divide:
op = "/";
break;
case DivideAssign:
op = "/=";
break;
case Modulo:
op = "%";
break;
case ModuloAssign:
op = "%=";
break;
case Multiply:
op = "*";
break;
case MultiplyAssign:
op = "*=";
break;
case LeftShift:
op = "<<";
break;
case LeftShiftAssign:
op = "<<=";
break;
case RightShift:
op = ">>";
break;
case RightShiftAssign:
op = ">>=";
break;
case UnsignedRightShift:
op = ">>>";
break;
case UnsignedRightShiftAssign:
op = ">>>=";
break;
case And:
if (TypeUtils.hasBuiltInEqualityOperator(node.getType(), PrimitiveTypes.Boolean)) {
op = "And";
}
else {
op = "&";
}
break;
case AndAssign:
if (TypeUtils.hasBuiltInEqualityOperator(node.getType(), PrimitiveTypes.Boolean)) {
op = "&&=";
}
else {
op = "&=";
}
break;
case Or:
if (TypeUtils.hasBuiltInEqualityOperator(node.getType(), PrimitiveTypes.Boolean)) {
op = "Or";
}
else {
op = "|";
}
break;
case OrAssign:
if (TypeUtils.hasBuiltInEqualityOperator(node.getType(), PrimitiveTypes.Boolean)) {
op = "||=";
}
else {
op = "|=";
}
break;
case ExclusiveOr:
op = "^";
break;
case ExclusiveOrAssign:
op = "^=";
break;
case Coalesce:
op = "??";
break;
default:
throw new IllegalStateException();
}
out("(");
visit(node.getLeft());
out(' ');
out(op);
out(' ');
visit(node.getRight());
out(")");
}
return node;
}
@Override
protected Expression visitGoto(final GotoExpression node) {
switch (node.getKind()) {
case Goto:
out("goto ");
break;
case Return:
out("return ");
break;
case Break:
out("break ");
break;
case Continue:
out("continue ");
break;
}
final Expression value = node.getValue();
if (value != null) {
visit(value);
out(' ');
}
final LabelTarget target = node.getTarget();
if (target != null) {
visitLabelTarget(target);
}
return node;
}
@Override
protected Expression visitLabel(final LabelExpression node) {
final LabelTarget target = node.getTarget();
visitLabelTarget(target);
if (target.getName() != null) {
out(":");
}
return node;
}
@Override
protected LabelTarget visitLabelTarget(final LabelTarget node) {
final String name = node.getName();
if (name != null) {
out(name);
}
return node;
}
@Override
public <T> LambdaExpression<T> visitLambda(final LambdaExpression<T> node) {
final ParameterExpressionList parameters = node.getParameters();
if (parameters.size() == 1) {
// p => body
visit(parameters.get(0));
}
else {
// (p1, p2, ..., pn) => body
visitExpressions('(', parameters, ')');
}
out(" => ");
visit(node.getBody());
return node;
}
@Override
protected Expression visitLoop(final LoopExpression node) {
final LabelTarget breakTarget = node.getBreakTarget();
final LabelTarget continueTarget = node.getContinueTarget();
final boolean hasBlock = node.getBody() instanceof BlockExpression;
out("loop");
flush();
if (continueTarget != null && continueTarget.getName() != null) {
visitLabelTarget(continueTarget);
out(':');
flush();
}
if (!hasBlock) {
increaseIndent();
}
visit(node.getBody());
flush();
if (breakTarget != null && breakTarget.getName() != null) {
visitLabelTarget(breakTarget);
out(':');
flush();
}
if (!hasBlock) {
decreaseIndent();
}
return node;
}
@Override
protected Expression visitForEach(final ForEachExpression node) {
final ParameterExpression variable = node.getVariable();
final LabelTarget breakTarget = node.getBreakTarget();
final LabelTarget continueTarget = node.getContinueTarget();
final boolean hasBlock = node.getBody() instanceof BlockExpression;
out("for (");
out(variable.getType().getName());
out(' ');
visit(variable);
out(" : ");
visit(node.getSequence());
out(")");
flush();
if (!hasBlock) {
increaseIndent();
}
if (continueTarget != null && continueTarget.getName() != null) {
visitLabelTarget(continueTarget);
flush();
}
visit(node.getBody());
flush();
if (breakTarget != null && breakTarget.getName() != null) {
visitLabelTarget(breakTarget);
flush();
}
if (!hasBlock) {
decreaseIndent();
}
return node;
}
@Override
protected Expression visitNew(final NewExpression node) {
out("new ");
out(node.getType().getSimpleDescription());
out('(');
visitList(node);
out(')');
return node;
}
@Override
protected Expression visitNewArray(final NewArrayExpression node) {
out("new ");
out(node.getType().getElementType().getSimpleDescription());
if (node.getNodeType() == ExpressionType.NewArrayBounds) {
final ExpressionList<? extends Expression> dimensions = node.getExpressions();
for (int i = 0, n = dimensions.size(); i < n; i++) {
out('[');
visit(dimensions.get(i));
out(']');
}
}
else {
out("[] {");
final ExpressionList<? extends Expression> items = node.getExpressions();
if (!items.isEmpty()) {
out(' ');
}
visitList(items);
if (!items.isEmpty()) {
out(' ');
}
out('}');
}
return node;
}
@Override
protected Expression visitFor(final ForExpression node) {
final ParameterExpression variable = node.getVariable();
final LabelTarget breakTarget = node.getBreakTarget();
final LabelTarget continueTarget = node.getContinueTarget();
final boolean hasBlock = node.getBody() instanceof BlockExpression;
out("for (");
out(variable.getType().getSimpleDescription());
out(' ');
visit(variable);
out(" = ");
visit(node.getInitializer());
out("; ");
visit(node.getTest());
out("; ");
visit(node.getStep());
out(")");
flush();
if (!hasBlock) {
increaseIndent();
}
visit(node.getBody());
flush();
if (breakTarget != null && breakTarget.getName() != null) {
visitLabelTarget(breakTarget);
flush();
}
if (continueTarget != null && continueTarget.getName() != null) {
visitLabelTarget(continueTarget);
flush();
}
if (!hasBlock) {
decreaseIndent();
}
return node;
}
@Override
protected Expression visitBlock(final BlockExpression node) {
++_blockDepth;
out('{');
increaseIndent();
flush();
for (final Expression v : node.getVariables()) {
out(v.getType().getSimpleDescription());
out(' ');
visit(v);
flush();
}
for (int i = 0, n = node.getExpressionCount(); i < n; i++) {
visit(node.getExpression(i));
flush();
}
decreaseIndent();
out('}');
--_blockDepth;
return node;
}
@Override
protected Expression visitInvocation(final InvocationExpression node) {
final ExpressionList arguments = node.getArguments();
out("Invoke(");
visit(node.getExpression());
for (int i = 0, n = arguments.size(); i < n; i++) {
out(", ");
visit(arguments.get(i));
}
out(")");
return node;
}
@Override
protected Expression visitMethodCall(final MethodCallExpression node) {
final Expression target = node.getTarget();
if (target != null) {
visit(target);
out(".");
}
else {
out(node.getMethod().getDeclaringType().getSimpleDescription());
out(".");
}
out(node.getMethod().getName());
out("(");
final ExpressionList<? extends Expression> arguments = node.getArguments();
for (int i = 0, n = arguments.size(); i < n; i++) {
if (i > 0) {
out(", ");
}
visit(arguments.get(i));
}
out(")");
return node;
}
@Override
protected Expression visitConditional(final ConditionalExpression node) {
if (node.getType() != PrimitiveTypes.Void) {
out('(');
visit(node.getTest());
out(" ? ");
visit(node.getIfTrue());
out(" : ");
visit(node.getIfFalse());
out(')');
return node;
}
if (node.getIfFalse() instanceof DefaultValueExpression &&
node.getIfFalse().getType() == PrimitiveTypes.Void) {
out("if (");
visit(node.getTest());
out(") ");
if (_blockDepth > 0) {
flush();
increaseIndent();
}
visit(node.getIfTrue());
if (_blockDepth > 0) {
decreaseIndent();
}
return node;
}
out("if (");
visit(node.getTest());
out(") ");
if (_blockDepth > 0) {
flush();
increaseIndent();
}
visit(node.getIfTrue());
if (_blockDepth > 0) {
flush();
decreaseIndent();
}
out(" else ");
if (_blockDepth > 0) {
flush();
increaseIndent();
}
visit(node.getIfFalse());
if (_blockDepth > 0) {
decreaseIndent();
}
return node;
}
@Override
public CatchBlock visitCatchBlock(final CatchBlock node) {
out("catch (" + node.getTest().getFullName());
if (node.getVariable() != null) {
final String variableName = node.getVariable().getName();
out(variableName != null ? variableName : "");
}
out(") { ... }");
return node;
}
@Override
public SwitchCase visitSwitchCase(final SwitchCase node) {
out("case ");
visitExpressions('(', node.getTestValues(), ')');
out(':');
flush();
increaseIndent();
visit(node.getBody());
flush();
out("break;");
decreaseIndent();
flush();
return node;
}
@Override
protected Expression visitSwitch(final SwitchExpression node) {
out("switch ");
out("(");
visit(node.getSwitchValue());
out(") {");
for (final SwitchCase switchCase : node.getCases()) {
flush();
visitSwitchCase(switchCase);
}
final Expression defaultBody = node.getDefaultBody();
if (defaultBody != null) {
flush();
out("default:");
flush();
increaseIndent();
visit(defaultBody);
flush();
out("break;");
decreaseIndent();
}
flush();
out('}');
return node;
}
@Override
public <T extends Expression> T visitAndConvert(final T node, final String callerName) {
return super.visitAndConvert(node, callerName);
}
@Override
public <T extends Expression> ExpressionList<T> visitAndConvertList(final ExpressionList<T> nodes, final String callerName) {
return super.visitAndConvertList(nodes, callerName);
}
@Override
public ParameterExpressionList visitAndConvertList(final ParameterExpressionList nodes, final String callerName) {
return super.visitAndConvertList(nodes, callerName);
}
}