/* * Copyright 2016 Nabarun Mondal * 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.noga.njexl.lang; import java.util.regex.Pattern; import com.noga.njexl.lang.parser.*; /** * Helps pinpoint the cause of problems in expressions that fail during evaluation. * <p> * It rebuilds an expression string from the tree and the start/end offsets of the cause * in that string. * </p> * This implies that exceptions during evaluation do allways carry the node that's causing * the error. * @since 2.0 */ public final class Debugger implements ParserVisitor { /** The builder to compose messages. */ private final StringBuilder builder; /** The cause of the issue to debug. */ private JexlNode cause; /** The starting character location offset of the cause in the builder. */ private int start; /** The ending character location offset of the cause in the builder. */ private int end; public static String getText(JexlNode node){ Debugger d = new Debugger(); String s = d.data(node); return s; } /** * Creates a Debugger. */ Debugger() { builder = new StringBuilder(); cause = null; start = 0; end = 0; } /** * Seeks the location of an error cause (a node) in an expression. * @param node the node to debug * @return true if the cause was located, false otherwise */ public boolean debug(JexlNode node) { start = 0; end = 0; if (node != null) { builder.setLength(0); this.cause = node; // make arg cause become the root cause JexlNode root = node; while (root.jjtGetParent() != null) { root = root.jjtGetParent(); } root.jjtAccept(this, null); } return end > 0; } /** * @return The rebuilt expression */ public String data() { return builder.toString(); } /** * Rebuilds an expression from a Jexl node. * @param node the node to rebuilt from * @return the rebuilt expression * @since 2.1 */ public String data(JexlNode node) { start = 0; end = 0; if (node != null) { builder.setLength(0); this.cause = node; node.jjtAccept(this, null); } return builder.toString(); } /** * @return The starting offset location of the cause in the expression */ public int start() { return start; } /** * @return The end offset location of the cause in the expression */ public int end() { return end; } /** * Checks if a child node is the cause to debug & adds its representation * to the rebuilt expression. * @param node the child node * @param data visitor pattern argument * @return visitor pattern value */ private Object accept(JexlNode node, Object data) { if (node == cause) { start = builder.length(); } Object value = node.jjtAccept(this, data); if (node == cause) { end = builder.length(); } return value; } /** * Adds a statement node to the rebuilt expression. * @param child the child node * @param data visitor pattern argument * @return visitor pattern value */ private Object acceptStatement(JexlNode child, Object data) { Object value = accept(child, data); // blocks, if, for & while dont need a ';' at end if (child instanceof ASTBlock || child instanceof ASTIfStatement || child instanceof ASTForeachStatement || child instanceof ASTWhileStatement || child instanceof ASTWhereStatement || child instanceof ASTMethodDef) { return value; } builder.append(";"); return value; } /** * Checks if a terminal node is the the cause to debug & adds its * representation to the rebuilt expression. * @param node the child node * @param image the child node token image (may be null) * @param data visitor pattern argument * @return visitor pattern value */ private Object check(JexlNode node, String image, Object data) { if (node == cause) { start = builder.length(); } if (image != null) { builder.append(image); } else { builder.append(node.toString()); } if (node == cause) { end = builder.length(); } return data; } /** * Checks if the children of a node using infix notation is the cause to debug, * adds their representation to the rebuilt expression. * @param node the child node * @param infix the child node token * @param paren whether the child should be parenthesized * @param data visitor pattern argument * @return visitor pattern value */ private Object infixChildren(JexlNode node, String infix, boolean paren, Object data) { int num = node.jjtGetNumChildren(); //child.jjtGetNumChildren() > 1; if (paren) { builder.append("("); } for (int i = 0; i < num; ++i) { if (i > 0) { builder.append(infix); } accept(node.jjtGetChild(i), data); } if (paren) { builder.append(")"); } return data; } /** * Checks if the child of a node using prefix notation is the cause to debug, * adds their representation to the rebuilt expression. * @param node the node * @param prefix the node token * @param data visitor pattern argument * @return visitor pattern value */ private Object prefixChild(JexlNode node, String prefix, Object data) { boolean paren = node.jjtGetChild(0).jjtGetNumChildren() > 1; builder.append(prefix); if (paren) { builder.append("("); } accept(node.jjtGetChild(0), data); if (paren) { builder.append(")"); } return data; } /** {@inheritDoc} */ public Object visit(ASTAdditiveNode node, Object data) { // need parenthesis if not in operator precedence order boolean paren = node.jjtGetParent() instanceof ASTMulNode || node.jjtGetParent() instanceof ASTDivNode || node.jjtGetParent() instanceof ASTModNode; int num = node.jjtGetNumChildren(); //child.jjtGetNumChildren() > 1; if (paren) { builder.append("("); } accept(node.jjtGetChild(0), data); for (int i = 1; i < num; ++i) { accept(node.jjtGetChild(i), data); } if (paren) { builder.append(")"); } return data; } @Override public Object visit(ASTImportStatement node, Object data) { builder.append("import '"); builder.append( node.jjtGetChild(0).image); builder.append("' as "); builder.append(node.jjtGetChild(1).image); return data; } @Override public Object visit(ASTExtendsDef node, Object data) { int numChild = node.jjtGetNumChildren(); accept(node.jjtGetChild(0), data); if ( numChild == 2) { builder.append(":"); accept(node.jjtGetChild(1),data); } return data; } @Override public Object visit(ASTClassDef node, Object data) { int numChild = node.jjtGetNumChildren(); builder.append("def "); // the name of the class builder.append( node.jjtGetChild(0).image); int i = 1; if( i< numChild -1 ) { // the extends now builder.append(" : "); int numSupers = numChild - 2; if (numSupers > 0) { // accept the first accept(node.jjtGetChild(i), data); i++; while (i < numChild - 1) { builder.append(","); accept(node.jjtGetChild(i),data); i++; } } } // the block now : accept(node.jjtGetChild(numChild - 1), data); return data; } @Override public Object visit(ASTArgDef node, Object data) { int numChild = node.jjtGetNumChildren(); accept(node.jjtGetChild(0),data); if ( numChild == 2) { builder.append("="); accept(node.jjtGetChild(1),data); } return data; } @Override public Object visit(ASTParamDef node, Object data) { int numChild = node.jjtGetNumChildren(); accept(node.jjtGetChild(0),data); if ( numChild == 2 ) { builder.append("="); accept(node.jjtGetChild(1),data); } return data; } @Override public Object visit(ASTMethodDef node, Object data) { int numChild = node.jjtGetNumChildren(); builder.append("def "); JexlNode id = node.jjtGetChild(0) ; int start = 1 ; if ( id instanceof ASTIdentifier ){ builder.append( id.image); start = 2; } builder.append("( "); int numParams = numChild - start; if ( numParams > 0 ) { accept(node.jjtGetChild(start-1), data); for (int i = start ; i < numChild - 1; i++) { builder.append(","); accept(node.jjtGetChild(i), data); } } builder.append(") "); // now the body accept(node.jjtGetChild(numChild - 1), data); return data; } /** {@inheritDoc} */ public Object visit(ASTAdditiveOperator node, Object data) { builder.append(' '); builder.append(node.image); builder.append(' '); return data; } /** {@inheritDoc} */ public Object visit(ASTAndNode node, Object data) { return infixChildren(node, " && ", false, data); } /** {@inheritDoc} */ public Object visit(ASTArrayAccess node, Object data) { accept(node.jjtGetChild(0), data); int num = node.jjtGetNumChildren(); for (int i = 1; i < num; ++i) { builder.append("["); accept(node.jjtGetChild(i), data); builder.append("]"); } return data; } /** {@inheritDoc} */ public Object visit(ASTArrayLiteral node, Object data) { int num = node.jjtGetNumChildren(); builder.append("[ "); if (num > 0) { accept(node.jjtGetChild(0), data); for (int i = 1; i < num; ++i) { builder.append(", "); accept(node.jjtGetChild(i), data); } } builder.append(" ]"); return data; } /** {@inheritDoc} */ public Object visit(ASTAssignment node, Object data) { return infixChildren(node, " = ", false, data); } /** {@inheritDoc} */ public Object visit(ASTTagContainer node, Object data) { builder.append(":"); accept(node.jjtGetChild(0), data); return data ; } /** {@inheritDoc} */ public Object visit(ASTTuple node, Object data) { int c = node.jjtGetNumChildren(); builder.append("#("); accept(node.jjtGetChild(0), data); for ( int i = 1; i < c ; i++ ){ builder.append(","); accept(node.jjtGetChild(i), data); } builder.append(") "); return data ; } /** {@inheritDoc} */ public Object visit(ASTBitwiseAndNode node, Object data) { return infixChildren(node, " & ", false, data); } /** {@inheritDoc} */ public Object visit(ASTBitwiseComplNode node, Object data) { return prefixChild(node, "~", data); } /** {@inheritDoc} */ public Object visit(ASTBitwiseOrNode node, Object data) { boolean paren = node.jjtGetParent() instanceof ASTBitwiseAndNode; return infixChildren(node, " | ", paren, data); } /** {@inheritDoc} */ public Object visit(ASTBitwiseXorNode node, Object data) { boolean paren = node.jjtGetParent() instanceof ASTBitwiseAndNode; return infixChildren(node, " ^ ", paren, data); } /** {@inheritDoc} */ public Object visit(ASTBlock node, Object data) { builder.append("{ "); int num = node.jjtGetNumChildren(); for (int i = 0; i < num; ++i) { JexlNode child = node.jjtGetChild(i); acceptStatement(child, data); } builder.append(" }"); return data; } /** {@inheritDoc} */ public Object visit(ASTDivNode node, Object data) { return infixChildren(node, " / ", false, data); } /** {@inheritDoc} */ public Object visit(ASTEmptyFunction node, Object data) { builder.append("empty("); accept(node.jjtGetChild(0), data); builder.append(")"); return data; } public Object loopJumps(String value, JexlNode node, Object data){ builder.append(value); int c = node.jjtGetNumChildren() ; if ( c > 0 ){ builder.append(" ( "); accept( node.jjtGetChild(0), data ); builder.append(" )"); if ( c > 1 ){ accept( node.jjtGetChild(1), data ); } } return data; } /** {@inheritDoc} */ public Object visit(ASTBreakStatement node, Object data) { return loopJumps(" break ", node, data ); } /** {@inheritDoc} */ public Object visit(ASTContinueStatement node, Object data) { return loopJumps(" continue ", node, data ); } /** {@inheritDoc} */ public Object visit(ASTDefinedFunction node, Object data) { builder.append("#def("); accept(node.jjtGetChild(0), data); builder.append(")"); return data; } public Object visit(ASTISANode node, Object data) { return infixChildren(node, " isa ", false, data); } public Object visit(ASTINNode node, Object data) { return infixChildren(node, " @ ", false, data); } /** {@inheritDoc} */ public Object visit(ASTAEQNode node, Object data) { return infixChildren(node, " === ", false, data); } @Override public Object visit(ASTInOrderNode node, Object data) { return infixChildren(node, " #@ ", false, data); } @Override public Object visit(ASTEndWithNode node, Object data) { return infixChildren(node, " #$ ", false, data); } @Override public Object visit(ASTStartWithNode node, Object data) { return infixChildren(node, " #^ ", false, data); } /** {@inheritDoc} */ public Object visit(ASTEQNode node, Object data) { return infixChildren(node, " == ", false, data); } /** {@inheritDoc} */ public Object visit(ASTERNode node, Object data) { return infixChildren(node, " =~ ", false, data); } /** {@inheritDoc} */ public Object visit(ASTFalseNode node, Object data) { return check(node, "false", data); } /** {@inheritDoc} */ public Object visit(ASTForeachStatement node, Object data) { return accept(node.jjtGetChild(0), data); } /** {@inheritDoc} */ public Object visit(ASTExpressionFor node, Object data) { if ( node.jjtGetNumChildren() > 0 ) { return accept(node.jjtGetChild(0), data); } builder.append( " true "); return true ; } /** {@inheritDoc} */ public Object visit(ASTForWithIterator node, Object data) { builder.append("for("); accept(node.jjtGetChild(0), data); builder.append(" : "); accept(node.jjtGetChild(1), data); builder.append(") "); if (node.jjtGetNumChildren() > 2) { acceptStatement(node.jjtGetChild(2), data); } else { builder.append(';'); } return data; } /** {@inheritDoc} */ public Object visit(ASTForWithCondition node, Object data) { builder.append("for("); accept(node.jjtGetChild(0), data); builder.append(" ; "); accept(node.jjtGetChild(1), data); builder.append(" ; "); accept(node.jjtGetChild(2), data); builder.append(" ) "); if (node.jjtGetNumChildren() > 3) { acceptStatement(node.jjtGetChild(3), data); } else { builder.append(';'); } return data; } /** {@inheritDoc} */ public Object visit(ASTGENode node, Object data) { return infixChildren(node, " >= ", false, data); } /** {@inheritDoc} */ public Object visit(ASTGTNode node, Object data) { return infixChildren(node, " > ", false, data); } /** Checks identifiers that contain space, quote, double-quotes or backspace. */ private static final Pattern QUOTED_IDENTIFIER = Pattern.compile("['\"\\s\\\\]"); /** {@inheritDoc} */ public Object visit(ASTIdentifier node, Object data) { String image = node.image; if (QUOTED_IDENTIFIER.matcher(image).find()) { // quote it image = "'" + node.image.replace("'", "\\'") + "'"; } return check(node, image, data); } /** {@inheritDoc} */ public Object visit(ASTIfStatement node, Object data) { builder.append("if ("); accept(node.jjtGetChild(0), data); builder.append(") "); if (node.jjtGetNumChildren() > 1) { acceptStatement(node.jjtGetChild(1), data); if (node.jjtGetNumChildren() > 2) { builder.append(" else "); acceptStatement(node.jjtGetChild(2), data); } else { builder.append(';'); } } else { builder.append(';'); } return data; } /** {@inheritDoc} */ public Object visit(ASTNumberLiteral node, Object data) { return check(node, node.image, data); } /** {@inheritDoc} */ public Object visit(ASTJexlScript node, Object data) { int num = node.jjtGetNumChildren(); for (int i = 0; i < num; ++i) { JexlNode child = node.jjtGetChild(i); acceptStatement(child, data); } return data; } /** {@inheritDoc} */ public Object visit(ASTLENode node, Object data) { return infixChildren(node, " <= ", false, data); } /** {@inheritDoc} */ public Object visit(ASTLTNode node, Object data) { return infixChildren(node, " < ", false, data); } /** {@inheritDoc} */ public Object visit(ASTArrayRange node, Object data) { builder.append("["); accept(node.jjtGetChild(0), data); builder.append(":"); accept(node.jjtGetChild(1), data); if ( node.jjtGetNumChildren() == 3 ){ builder.append(":"); accept(node.jjtGetChild(2), data); } builder.append("]"); return data ; } /** {@inheritDoc} */ public Object visit(ASTMapEntry node, Object data) { accept(node.jjtGetChild(0), data); builder.append(" : "); accept(node.jjtGetChild(1), data); return data; } /** {@inheritDoc} */ public Object visit(ASTMapLiteral node, Object data) { int num = node.jjtGetNumChildren(); builder.append("{ "); if (num > 0) { accept(node.jjtGetChild(0), data); for (int i = 1; i < num; ++i) { builder.append(", "); accept(node.jjtGetChild(i), data); } } else { builder.append(':'); } builder.append(" }"); return data; } /** {@inheritDoc} */ public Object visit(ASTConstructorNode node, Object data) { int num = node.jjtGetNumChildren(); builder.append("new "); builder.append("("); accept(node.jjtGetChild(0), data); for (int i = 1; i < num; ++i) { builder.append(", "); accept(node.jjtGetChild(i), data); } builder.append(")"); return data; } /** {@inheritDoc} */ public Object visit(ASTFunctionNode node, Object data) { int num = node.jjtGetNumChildren(); accept(node.jjtGetChild(0), data); builder.append(":"); accept(node.jjtGetChild(1), data); JexlNode n ; int start = 2; if ( num > 2 ) { n = node.jjtGetChild(2); if (n instanceof ASTBlock) { accept(n,data); start = 3; } } builder.append("("); for (int i = start; i < num; ++i) { if (i > start) { builder.append(", "); } accept(node.jjtGetChild(i), data); } builder.append(")"); return data; } /** {@inheritDoc} */ public Object visit(ASTMethodNode node, Object data) { int num = node.jjtGetNumChildren(); accept(node.jjtGetChild(0), data); JexlNode n ; int start = 1; if ( num > 1 ) { n = node.jjtGetChild(1); if (n instanceof ASTBlock) { accept(n,data); start = 2; } } builder.append("("); for (int i = start; i < num; ++i) { if (i > start ) { builder.append(", "); } n = node.jjtGetChild(i); accept(n, data); } builder.append(")"); return data; } /** {@inheritDoc} */ public Object visit(ASTModNode node, Object data) { return infixChildren(node, " % ", false, data); } /** {@inheritDoc} */ public Object visit(ASTMulNode node, Object data) { return infixChildren(node, " * ", false, data); } /** {@inheritDoc} */ public Object visit(ASTPowNode node, Object data) { return infixChildren(node, " ** ", false, data); } /** {@inheritDoc} */ public Object visit(ASTNENode node, Object data) { return infixChildren(node, " != ", false, data); } /** {@inheritDoc} */ public Object visit(ASTNRNode node, Object data) { return infixChildren(node, " !~ ", false, data); } /** {@inheritDoc} */ public Object visit(ASTNotNode node, Object data) { builder.append("!"); accept(node.jjtGetChild(0), data); return data; } /** {@inheritDoc} */ public Object visit(ASTNullLiteral node, Object data) { check(node, "null", data); return data; } /** {@inheritDoc} */ public Object visit(ASTOrNode node, Object data) { // need parenthesis if not in operator precedence order boolean paren = node.jjtGetParent() instanceof ASTAndNode; return infixChildren(node, " || ", paren, data); } /** {@inheritDoc} */ public Object visit(ASTReference node, Object data) { int num = node.jjtGetNumChildren(); accept(node.jjtGetChild(0), data); for (int i = 1; i < num; ++i) { builder.append("."); accept(node.jjtGetChild(i), data); } return data; } /** {@inheritDoc} */ public Object visit(ASTReferenceExpression node, Object data) { JexlNode first = node.jjtGetChild(0); builder.append('('); accept(first, data); builder.append(')'); int num = node.jjtGetNumChildren(); for (int i = 1; i < num; ++i) { builder.append("["); accept(node.jjtGetChild(i), data); builder.append("]"); } return data; } /** {@inheritDoc} */ public Object visit(ASTReturnStatement node, Object data) { builder.append("return "); if ( node.jjtGetNumChildren() > 0 ) { accept(node.jjtGetChild(0), data); } return data; } /** {@inheritDoc} */ public Object visit(ASTSizeFunction node, Object data) { builder.append("size("); accept(node.jjtGetChild(0), data); builder.append(")"); return data; } /** {@inheritDoc} */ public Object visit(ASTSizeMethod node, Object data) { check(node, "size()", data); return data; } /** {@inheritDoc} */ public Object visit(ASTStringLiteral node, Object data) { String img = node.image.replace("'", "\\'"); return check(node, "'" + img + "'", data); } /** {@inheritDoc} */ public Object visit(ASTCurryingLiteral node, Object data) { String img = node.image.replace("`", "\\`"); return check(node, "`" + img + "`", data); } /** {@inheritDoc} */ public Object visit(ASTTernaryNode node, Object data) { accept(node.jjtGetChild(0), data); if (node.jjtGetNumChildren() > 2) { builder.append("? "); accept(node.jjtGetChild(1), data); builder.append(" : "); accept(node.jjtGetChild(2), data); } else { builder.append("?:"); accept(node.jjtGetChild(1), data); } return data; } /** {@inheritDoc} */ public Object visit(ASTNullCoalesce node, Object data) { accept(node.jjtGetChild(0), data); builder.append("??"); accept(node.jjtGetChild(1), data); return data; } /** {@inheritDoc} */ public Object visit(ASTTrueNode node, Object data) { check(node, "true", data); return data; } /** * {@inheritDoc} */ public Object visit(ASTUnarySizeNode node, Object data) { builder.append("#|"); accept(node.jjtGetChild(0), data); builder.append("|"); return data; } /** {@inheritDoc} */ public Object visit(ASTUnaryMinusNode node, Object data) { return prefixChild(node, "-", data); } /** {@inheritDoc} */ public Object visit(ASTVar node, Object data) { builder.append("var "); check(node, node.image, data); return data; } /** {@inheritDoc} */ public Object visit(ASTWhileStatement node, Object data) { builder.append("while ("); accept(node.jjtGetChild(0), data); builder.append(") "); if (node.jjtGetNumChildren() > 1) { acceptStatement(node.jjtGetChild(1), data); } else { builder.append(';'); } return data; } /** {@inheritDoc} */ public Object visit(ASTWhereStatement node, Object data) { builder.append("where ("); accept(node.jjtGetChild(0), data); builder.append(") "); if (node.jjtGetNumChildren() > 1) { acceptStatement(node.jjtGetChild(1), data); } else { builder.append(';'); } return data; } /** * {@inheritDoc} */ @Override public Object visit(ASTCaseStatement node, Object data) { builder.append("case "); accept(node.jjtGetChild(0), data); builder.append( " : ") ; acceptStatement(node.jjtGetChild(1), data); return data; } /** * {@inheritDoc} */ @Override public Object visit(ASTMatchStatement node, Object data) { builder.append("#match ("); accept(node.jjtGetChild(0), data); builder.append("){ "); int n = node.jjtGetNumChildren(); for ( int i = 1; i < n; i++ ){ accept(node.jjtGetChild(i),data); } builder.append("}"); return data ; } /** {@inheritDoc} */ public Object visit(ASTGoToStatement node, Object data) { builder.append("goto "); builder.append("#"); accept(node.jjtGetChild(0), data); if (node.jjtGetNumChildren() > 1) { builder.append(" "); acceptStatement(node.jjtGetChild(1), data); } builder.append(" ;"); return data; } /** {@inheritDoc} */ public Object visit(ASTLabelledStatement node, Object data) { builder.append("#"); accept(node.jjtGetChild(0), data); builder.append( "\n"); accept(node.jjtGetChild(1), data); return data; } /** {@inheritDoc} */ public Object visit(ASTAtomicStatement node, Object data) { builder.append("#atomic"); accept(node.jjtGetChild(0), data); return data; } /** {@inheritDoc} */ public Object visit(ASTClockStatement node, Object data) { builder.append("#clock"); accept(node.jjtGetChild(0), data); return data; } /** {@inheritDoc} */ public Object visit(SimpleNode node, Object data) { throw new UnsupportedOperationException("unexpected type of node"); } /** {@inheritDoc} */ public Object visit(ASTAmbiguous node, Object data) { throw new UnsupportedOperationException("unexpected type of node"); } }