View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3.internal;
18  
19  import java.util.Collections;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.Set;
23  import java.util.regex.Pattern;
24  
25  import org.apache.commons.jexl3.JexlExpression;
26  import org.apache.commons.jexl3.JexlFeatures;
27  import org.apache.commons.jexl3.JexlInfo;
28  import org.apache.commons.jexl3.JexlScript;
29  import org.apache.commons.jexl3.parser.*;
30  
31  /**
32   * Helps pinpoint the cause of problems in expressions that fail during evaluation.
33   * <p>
34   * It rebuilds an expression string from the tree and the start/end offsets of the cause in that string.
35   * This implies that exceptions during evaluation do always carry the node that's causing the error.
36   * </p>
37   *
38   * @since 2.0
39   */
40  public class Debugger extends ParserVisitor implements JexlInfo.Detail {
41  
42      /** Checks identifiers that contain spaces or punctuation
43       * (but underscore, at-sign, sharp-sign and dollar).
44       */
45      protected static final Pattern QUOTED_IDENTIFIER =
46              Pattern.compile("\\s|\\p{Punct}&&[^@#$_]");
47      private static  boolean isLambdaExpr(final ASTJexlLambda lambda) {
48          return lambda.jjtGetNumChildren() == 1 && !isStatement(lambda.jjtGetChild(0));
49      }
50  
51      /**
52       * Tests whether a node is a statement (vs an expression).
53       *
54       * @param child the node
55       * @return true if node is a statement
56       */
57      private static boolean isStatement(final JexlNode child) {
58          if (child instanceof ASTCaseStatement) {
59              return child.jjtGetNumChildren() > 0 && isStatement(child.jjtGetChild(0));
60          }
61          return child instanceof ASTJexlScript
62                  || child instanceof ASTBlock
63                  || child instanceof ASTIfStatement
64                  || child instanceof ASTForeachStatement
65                  || child instanceof ASTTryStatement
66                  || child instanceof ASTWhileStatement
67                  || child instanceof ASTDoWhileStatement
68                  || child instanceof ASTAnnotation
69                  || child instanceof ASTThrowStatement
70                  || child instanceof ASTSwitchStatement;
71      }
72  
73      /**
74       * Tests whether a script or expression ends with a semicolumn.
75       *
76       * @param cs the string
77       * @return true if a semicolumn is the last non-whitespace character
78       */
79      private static boolean semicolTerminated(final CharSequence cs) {
80          for(int i = cs.length() - 1; i >= 0; --i) {
81              final char c = cs.charAt(i);
82              if (c == ';') {
83                  return true;
84              }
85              if (!Character.isWhitespace(c)) {
86                  break;
87              }
88          }
89          return false;
90      }
91  
92      /**
93       * Stringifies the pragmas.
94       *
95       * @param builder where to stringify
96       * @param pragmas the pragmas, may be null
97       */
98      private static void writePragmas(final StringBuilder builder, final Map<String, Object> pragmas) {
99          if (pragmas != null) {
100             for (final Map.Entry<String, Object> pragma : pragmas.entrySet()) {
101                 final String key = pragma.getKey();
102                 final Object value = pragma.getValue();
103                 final Set<Object> values = value instanceof Set<?>
104                     ? (Set<Object>) value
105                     : Collections.singleton(value);
106                 for (final Object pragmaValue : values) {
107                     builder.append("#pragma ");
108                     builder.append(key);
109                     builder.append(' ');
110                     acceptValue(builder, pragmaValue, false);
111                     builder.append('\n');
112                 }
113             }
114         }
115 
116     }
117 
118     /** The builder to compose messages. */
119     protected final StringBuilder builder = new StringBuilder();
120 
121     /** The cause of the issue to debug. */
122     protected JexlNode cause;
123 
124     /** The starting character location offset of the cause in the builder. */
125     protected int start;
126 
127     /** The ending character location offset of the cause in the builder. */
128     protected int end;
129 
130     /** The indentation level. */
131     protected int indentLevel;
132 
133     /** Perform indentation?. */
134     protected int indent = 2;
135 
136     /** Accept() relative depth. */
137     protected int depth = Integer.MAX_VALUE;
138 
139     /** Arrow symbol. */
140     protected String arrow = "->";
141 
142     /** EOL. */
143     protected String lf = "\n";
144 
145     /** Pragmas out. */
146     protected boolean outputPragmas;
147 
148     /**
149      * Creates a Debugger.
150      */
151     public Debugger() {
152         // nothing to initialize
153     }
154 
155     /**
156      * Checks if a child node is the cause to debug &amp; adds its representation to the rebuilt expression.
157      *
158      * @param node the child node
159      * @param data visitor pattern argument
160      * @return visitor pattern value
161      */
162     protected Object accept(final JexlNode node, final Object data) {
163         if (depth <= 0 && builder.length() > 0) {
164             builder.append("...");
165             return data;
166         }
167         if (node == cause) {
168             start = builder.length();
169         }
170         depth -= 1;
171         final Object value = node.jjtAccept(this, data);
172         depth += 1;
173         if (node == cause) {
174             end = builder.length();
175         }
176         return value;
177     }
178 
179     /**
180      * Adds a statement node to the rebuilt expression.
181      *
182      * @param child the child node
183      * @param data  visitor pattern argument
184      * @return visitor pattern value
185      */
186     protected Object acceptStatement(final JexlNode child, final Object data) {
187         final JexlNode parent = child.jjtGetParent();
188         if (indent > 0 && (parent instanceof ASTBlock || parent instanceof ASTJexlScript || parent instanceof ASTSwitchStatement)) {
189             for (int i = 0; i < indentLevel; ++i) {
190                 for(int s = 0; s < indent; ++s) {
191                     builder.append(' ');
192                 }
193             }
194         }
195         depth -= 1;
196         final Object value = accept(child, data);
197         depth += 1;
198         // blocks, if, for & while don't need a ';' at end
199         if (!isStatement(child) && !semicolTerminated(builder)) {
200             builder.append(';');
201             if (indent > 0) {
202                 builder.append(lf);
203             } else {
204                 builder.append(' ');
205             }
206         }
207         return value;
208     }
209 
210     /**
211      * Rebuilds an additive expression.
212      *
213      * @param node the node
214      * @param op   the operator
215      * @param data visitor pattern argument
216      * @return visitor pattern value
217      */
218     protected Object additiveNode(final JexlNode node, final String op, final Object data) {
219         // need parenthesis if not in operator precedence order
220         final boolean paren = node.jjtGetParent() instanceof ASTMulNode
221                 || node.jjtGetParent() instanceof ASTDivNode
222                 || node.jjtGetParent() instanceof ASTModNode;
223         final int num = node.jjtGetNumChildren();
224         if (paren) {
225             builder.append('(');
226         }
227         accept(node.jjtGetChild(0), data);
228         for (int i = 1; i < num; ++i) {
229             builder.append(op);
230             accept(node.jjtGetChild(i), data);
231         }
232         if (paren) {
233             builder.append(')');
234         }
235         return data;
236     }
237 
238     /**
239      * Checks if a terminal node is the cause to debug &amp; adds its representation to the rebuilt expression.
240      *
241      * @param node  the child node
242      * @param image the child node token image (optionally null)
243      * @param data  visitor pattern argument
244      * @return visitor pattern value
245      */
246     protected Object check(final JexlNode node, final String image, final Object data) {
247         if (node == cause) {
248             start = builder.length();
249         }
250         if (image != null) {
251             builder.append(image);
252         } else {
253             builder.append(node.toString());
254         }
255         if (node == cause) {
256             end = builder.length();
257         }
258         return data;
259     }
260 
261     /**
262      * Rebuilds an expression from a JEXL node.
263      *
264      * @param node the node to rebuilt from
265      * @return the rebuilt expression
266      * @since 3.0
267      */
268     public String data(final JexlNode node) {
269         start = 0;
270         end = 0;
271         indentLevel = 0;
272         setArrowSymbol(node);
273         if (node != null) {
274             builder.setLength(0);
275             cause = node;
276             accept(node, null);
277         }
278         return builder.toString();
279     }
280 
281     /**
282      * Position the debugger on the root of an expression.
283      *
284      * @param jscript the expression
285      * @return true if the expression was a {@link Script} instance, false otherwise
286      */
287     public boolean debug(final JexlExpression jscript) {
288         if (jscript instanceof Script) {
289             final Script script = (Script) jscript;
290             return debug(script.script);
291         }
292         return false;
293     }
294 
295     /**
296      * Seeks the location of an error cause (a node) in an expression.
297      *
298      * @param node the node to debug
299      * @return true if the cause was located, false otherwise
300      */
301     public boolean debug(final JexlNode node) {
302         return debug(node, true);
303     }
304 
305     /**
306      * Seeks the location of an error cause (a node) in an expression.
307      *
308      * @param node the node to debug
309      * @param r whether we should actively find the root node of the debugged node
310      * @return true if the cause was located, false otherwise
311      */
312     public boolean debug(final JexlNode node, final boolean r) {
313         start = 0;
314         end = 0;
315         indentLevel = 0;
316         setArrowSymbol(node);
317         if (node != null) {
318             builder.setLength(0);
319             cause = node;
320             // make arg cause become the root cause
321             JexlNode walk = node;
322             if (r) {
323                 while (walk.jjtGetParent() != null) {
324                     walk = walk.jjtGetParent();
325                 }
326             }
327             accept(walk, null);
328         }
329         return end > 0;
330     }
331 
332     /**
333      * Position the debugger on the root of a script.
334      *
335      * @param jscript the script
336      * @return true if the script was a {@link Script} instance, false otherwise
337      */
338     public boolean debug(final JexlScript jscript) {
339         if (jscript instanceof Script) {
340             final Script script = (Script) jscript;
341             return debug(script.script);
342         }
343         return false;
344     }
345 
346     /**
347      * Sets this debugger relative maximum depth.
348      *
349      * @param rdepth the maximum relative depth from the debugged node
350      * @return this debugger instance
351      */
352     public Debugger depth(final int rdepth) {
353         this.depth = rdepth;
354         return this;
355     }
356 
357     /**
358      * @return The end offset location of the cause in the expression
359      */
360     @Override
361     public int end() {
362         return end;
363     }
364 
365     /**
366      * Tries (hard) to find the features used to parse a node.
367      *
368      * @param node the node
369      * @return the features or null
370      */
371     protected JexlFeatures getFeatures(final JexlNode node) {
372         JexlNode walk = node;
373         while(walk != null) {
374             if (walk instanceof ASTJexlScript) {
375                 final ASTJexlScript script = (ASTJexlScript) walk;
376                 return script.getFeatures();
377             }
378             walk = walk.jjtGetParent();
379         }
380         return null;
381     }
382 
383     /**
384      * Sets the indentation level.
385      *
386      * @param level the number of spaces for indentation, none if less or equal to zero
387      * @return this debugger instance
388      */
389     public Debugger indentation(final int level) {
390         indent = Math.max(level, 0);
391         indentLevel = 0;
392         return this;
393     }
394 
395     /**
396      * Checks if the children of a node using infix notation is the cause to debug, adds their representation to the
397      * rebuilt expression.
398      *
399      * @param node  the child node
400      * @param infix the child node token
401      * @param paren whether the child should be parenthesized
402      * @param data  visitor pattern argument
403      * @return visitor pattern value
404      */
405     protected Object infixChildren(final JexlNode node, final String infix, final boolean paren, final Object data) {
406         final int num = node.jjtGetNumChildren();
407         if (paren) {
408             builder.append('(');
409         }
410         for (int i = 0; i < num; ++i) {
411             if (i > 0) {
412                 builder.append(infix);
413             }
414             accept(node.jjtGetChild(i), data);
415         }
416         if (paren) {
417             builder.append(')');
418         }
419         return data;
420     }
421 
422     /**
423      * Sets this debugger line-feed string.
424      *
425      * @param lf the string used to delineate lines (usually "\" or "")
426      * @return this debugger instance
427      */
428     public Debugger lineFeed(final String lf) {
429         this.lf = lf;
430         return this;
431     }
432 
433     /**
434      * Checks whether an identifier should be quoted or not.
435      *
436      * @param str the identifier
437      * @return true if needing quotes, false otherwise
438      */
439     protected boolean needQuotes(final String str) {
440         return QUOTED_IDENTIFIER.matcher(str).find()
441                 || "size".equals(str)
442                 || "empty".equals(str);
443     }
444 
445     /**
446      * Lets the debugger write out pragmas if any.
447      *
448      * @param flag turn on or off
449      * @return this debugger instance
450      */
451     public Debugger outputPragmas(final boolean flag) {
452         this.outputPragmas = flag;
453         return this;
454     }
455 
456     /**
457      * Postfix operators.
458      *
459      * @param node a postfix operator
460      * @param prefix the postfix
461      * @param data visitor pattern argument
462      * @return visitor pattern value
463      */
464     protected Object postfixChild(final JexlNode node, final String prefix, final Object data) {
465         final boolean paren = node.jjtGetChild(0).jjtGetNumChildren() > 1;
466         if (paren) {
467             builder.append('(');
468         }
469         accept(node.jjtGetChild(0), data);
470         if (paren) {
471             builder.append(')');
472         }
473         builder.append(prefix);
474         return data;
475     }
476 
477     /**
478      * Checks if the child of a node using prefix notation is the cause to debug, adds their representation to the
479      * rebuilt expression.
480      *
481      * @param node   the node
482      * @param prefix the node token
483      * @param data   visitor pattern argument
484      * @return visitor pattern value
485      */
486     protected Object prefixChild(final JexlNode node, final String prefix, final Object data) {
487         final boolean paren = node.jjtGetChild(0).jjtGetNumChildren() > 1;
488         builder.append(prefix);
489         if (paren) {
490             builder.append('(');
491         }
492         accept(node.jjtGetChild(0), data);
493         if (paren) {
494             builder.append(')');
495         }
496         return data;
497     }
498 
499     /**
500      * Accepts a (simple) value and appends its representation to the builder.
501      *
502      * @param builder where to append
503      * @param value   the value to append
504      */
505     static void acceptValue(final StringBuilder builder, final Object value, final boolean quotedStrings) {
506         if (value == null) {
507             builder.append("null");
508         } else if (value instanceof String) {
509             builder.append(quotedStrings? StringParser.escapeString(value.toString(), '\'') : value.toString());
510         } else if (value instanceof Number) {
511             builder.append(new NumberParser((Number) value));
512         } else if (value instanceof Boolean) {
513             builder.append((Boolean) value ? "true" : "false");
514         } else {
515             builder.append(value.toString());
516         }
517     }
518 
519     /**
520      * Resets this debugger state.
521      */
522     public void reset() {
523         builder.setLength(0);
524         cause = null;
525         start = 0;
526         end = 0;
527         indentLevel = 0;
528         indent = 2;
529         depth = Integer.MAX_VALUE;
530     }
531 
532     /**
533      * Sets the arrow style (fat or thin) depending on features.
534      *
535      * @param node the node to start seeking features from.
536      */
537     protected void setArrowSymbol(final JexlNode node) {
538         final JexlFeatures features = getFeatures(node);
539         if (features != null && features.supportsFatArrow() && !features.supportsThinArrow()) {
540             arrow = "=>";
541         } else {
542             arrow = "->";
543         }
544     }
545 
546     /**
547      * Sets the indentation level.
548      *
549      * @param level the number of spaces for indentation, none if less or equal to zero
550      */
551     public void setIndentation(final int level) {
552         indentation(level);
553     }
554 
555     /**
556      * @return The starting offset location of the cause in the expression
557      */
558     @Override
559     public int start() {
560         return start;
561     }
562 
563     /**
564      * @return The rebuilt expression
565      */
566     @Override
567     public String toString() {
568         return builder.toString();
569     }
570 
571     @Override
572     protected Object visit(final ASTAddNode node, final Object data) {
573         return additiveNode(node, " + ", data);
574     }
575 
576     @Override
577     protected Object visit(final ASTAndNode node, final Object data) {
578         return infixChildren(node, " && ", false, data);
579     }
580 
581     @Override
582     protected Object visit(final ASTAnnotatedStatement node, final Object data) {
583         final int num = node.jjtGetNumChildren();
584         for (int i = 0; i < num; ++i) {
585             if (i > 0) {
586                 builder.append(' ');
587             }
588             final JexlNode child = node.jjtGetChild(i);
589             acceptStatement(child, data);
590         }
591         return data;
592     }
593 
594     @Override
595     protected Object visit(final ASTAnnotation node, final Object data) {
596         final int num = node.jjtGetNumChildren();
597         builder.append('@');
598         builder.append(node.getName());
599         if (num > 0) {
600             accept(node.jjtGetChild(0), data); // zut
601         }
602         return null;
603     }
604 
605     @Override
606     protected Object visit(final ASTArguments node, final Object data) {
607         final int num = node.jjtGetNumChildren();
608         builder.append("(");
609         if (num > 0) {
610             accept(node.jjtGetChild(0), data);
611             for (int i = 1; i < num; ++i) {
612                 builder.append(", ");
613                 accept(node.jjtGetChild(i), data);
614             }
615         }
616         builder.append(")");
617         return data;
618     }
619 
620     @Override
621     protected Object visit(final ASTArrayAccess node, final Object data) {
622         final int num = node.jjtGetNumChildren();
623         for (int i = 0; i < num; ++i) {
624             if (node.isSafeChild(i)) {
625                 builder.append('?');
626             }
627             builder.append('[');
628             accept(node.jjtGetChild(i), data);
629             builder.append(']');
630         }
631         return data;
632     }
633 
634     @Override
635     protected Object visit(final ASTArrayLiteral node, final Object data) {
636         final int num = node.jjtGetNumChildren();
637         builder.append("[ ");
638         if (num > 0) {
639             if (depth <= 0) {
640                 builder.append("...");
641             } else {
642                 accept(node.jjtGetChild(0), data);
643                 for (int i = 1; i < num; ++i) {
644                     builder.append(", ");
645                     accept(node.jjtGetChild(i), data);
646                 }
647             }
648         }
649         builder.append(" ]");
650         return data;
651     }
652 
653     @Override
654     protected Object visit(final ASTAssignment node, final Object data) {
655         return infixChildren(node, " = ", false, data);
656     }
657 
658     @Override
659     protected Object visit(final ASTBitwiseAndNode node, final Object data) {
660         return infixChildren(node, " & ", false, data);
661     }
662 
663     @Override
664     protected Object visit(final ASTBitwiseComplNode node, final Object data) {
665         return prefixChild(node, "~", data);
666     }
667 
668     @Override
669     protected Object visit(final ASTBitwiseOrNode node, final Object data) {
670         final boolean paren = node.jjtGetParent() instanceof ASTBitwiseAndNode;
671         return infixChildren(node, " | ", paren, data);
672     }
673 
674     @Override
675     protected Object visit(final ASTBitwiseXorNode node, final Object data) {
676         final boolean paren = node.jjtGetParent() instanceof ASTBitwiseAndNode;
677         return infixChildren(node, " ^ ", paren, data);
678     }
679 
680     @Override
681     protected Object visit(final ASTBlock node, final Object data) {
682         return acceptBlock(node, 0, data);
683     }
684 
685     private Object acceptBlock(final JexlNode node, final int begin, final Object data) {
686         builder.append('{');
687         if (indent > 0) {
688             indentLevel += 1;
689             builder.append(lf);
690         } else {
691             builder.append(' ');
692         }
693         final int num = node.jjtGetNumChildren();
694         for (int i = begin; i < num; ++i) {
695             final JexlNode child = node.jjtGetChild(i);
696             acceptStatement(child, data);
697         }
698         if (indent > 0) {
699             indentLevel -= 1;
700             for (int i = 0; i < indentLevel; ++i) {
701                 for(int s = 0; s < indent; ++s) {
702                     builder.append(' ');
703                 }
704             }
705         }
706         final char lastChar = builder.charAt(builder.length() - 1);
707         if (!Character.isSpaceChar(lastChar) && lastChar != '\n') {
708             builder.append(' ');
709         }
710         builder.append('}');
711         return data;
712     }
713 
714     @Override
715     protected Object visit(final ASTBreak node, final Object data) {
716         return check(node, "break", data);
717     }
718 
719     @Override
720     protected Object visit(final ASTConstructorNode node, final Object data) {
721         final int num = node.jjtGetNumChildren();
722         builder.append("new");
723         if (num > 0) {
724             final JexlNode c0 = node.jjtGetChild(0);
725             boolean first = true;
726             if (c0 instanceof ASTQualifiedIdentifier) {
727                 builder.append(' ');
728                 accept(c0, data);
729                 builder.append('(');
730             } else {
731                 first = false;
732                 builder.append('(');
733                 accept(c0, data);
734             }
735             for (int i = 1; i < num; ++i) {
736                 if (!first) {
737                     builder.append(", ");
738                 }
739                 accept(node.jjtGetChild(i), data);
740             }
741         }
742         builder.append(")");
743         return data;
744     }
745 
746     @Override
747     protected Object visit(final ASTSwitchStatement node, final Object data) {
748         builder.append("switch (");
749         accept(node.jjtGetChild(0), data);
750         builder.append(") ");
751         acceptBlock(node, 1, data);
752         return data;
753     }
754 
755     @Override
756     protected Object visit(final ASTSwitchExpression node, final Object data) {
757         return visit((ASTSwitchStatement) node, data);
758     }
759 
760     @Override
761     protected Object visit(final ASTCaseStatement node, final Object data) {
762         final JexlNode parent = node.jjtGetParent();
763         final boolean isStatement = parent instanceof ASTSwitchStatement && ((ASTSwitchStatement) parent).isStatement();
764         if (isStatement) {
765             return visitCaseStatement(node, data);
766         }
767         return visitCaseExpression(node, data);
768     }
769 
770     private Object visitCaseStatement(final ASTCaseStatement node, final Object data) {
771         final List<Object> values = node.getValues();
772         if (values.isEmpty()) {
773             // default case
774             builder.append("default : ");
775         } else {
776             // regular case
777             for (final Object value : values) {
778                 builder.append("case ");
779                 acceptValue(builder, value, true);
780                 builder.append(" : ");
781             }
782         }
783         if (node.jjtGetNumChildren() > 0) {
784             accept(node.jjtGetChild(0), data);
785         }
786         return data;
787     }
788 
789     @Override
790     protected Object visit(final ASTCaseExpression node, final Object data) {
791         return visitCaseExpression(node, data);
792     }
793 
794     private Object visitCaseExpression(final ASTCaseStatement node, final Object data) {
795         final List<Object> values = node.getValues();
796         if (values.isEmpty()) {
797             // default case
798             builder.append("default -> ");
799         } else {
800             builder.append("case ");
801             // regular case
802             boolean first = true;
803             for (final Object value : values) {
804                 if (!first) {
805                     builder.append(", ");
806                 } else {
807                     first = false;
808                 }
809                 acceptValue(builder, value, true);
810             }
811             builder.append(" -> ");
812         }
813         accept(node.jjtGetChild(0), data);
814         return data;
815     }
816 
817     @Override
818     protected Object visit(final ASTContinue node, final Object data) {
819         return check(node, "continue", data);
820     }
821 
822     @Override
823     protected Object visit(final ASTDecrementGetNode node, final Object data) {
824         return prefixChild(node, "--", data);
825     }
826 
827     @Override
828     protected Object visit(final ASTDefineVars node, final Object data) {
829         final int num = node.jjtGetNumChildren();
830         if (num > 0) {
831             // var, let, const
832             accept(node.jjtGetChild(0), data);
833             for (int i = 1; i < num; ++i) {
834                 builder.append(", ");
835                 final JexlNode child = node.jjtGetChild(i);
836                 if (child instanceof ASTAssignment) {
837                     final ASTAssignment assign = (ASTAssignment) child;
838                     final int nc = assign.jjtGetNumChildren();
839                     final ASTVar avar = (ASTVar) assign.jjtGetChild(0);
840                     builder.append(avar.getName());
841                     if (nc > 1) {
842                         builder.append(" = ");
843                         accept(assign.jjtGetChild(1), data);
844                     }
845                 } else if (child instanceof ASTVar) {
846                     final ASTVar avar = (ASTVar) child;
847                     builder.append(avar.getName());
848                 } else {
849                     // that's odd
850                     accept(child, data);
851                 }
852             }
853         }
854         return data;
855     }
856 
857     @Override
858     protected Object visit(final ASTDivNode node, final Object data) {
859         return infixChildren(node, " / ", false, data);
860     }
861 
862     @Override
863     protected Object visit(final ASTDoWhileStatement node, final Object data) {
864         builder.append("do ");
865         final int nc = node.jjtGetNumChildren();
866         if (nc > 1) {
867             acceptStatement(node.jjtGetChild(0), data);
868         } else {
869             builder.append(";");
870         }
871         builder.append(" while (");
872         accept(node.jjtGetChild(nc - 1), data);
873         builder.append(")");
874         return data;
875     }
876 
877     @Override
878     protected Object visit(final ASTEmptyFunction node, final Object data) {
879         builder.append("empty ");
880         accept(node.jjtGetChild(0), data);
881         return data;
882     }
883 
884     @Override
885     protected Object visit(final ASTEQNode node, final Object data) {
886         return infixChildren(node, " == ", false, data);
887     }
888 
889     @Override
890     protected Object visit(final ASTEQSNode node, final Object data) {
891         return infixChildren(node, " === ", false, data);
892     }
893 
894     @Override
895     protected Object visit(final ASTERNode node, final Object data) {
896         return infixChildren(node, " =~ ", false, data);
897     }
898 
899     @Override
900     protected Object visit(final ASTEWNode node, final Object data) {
901         return infixChildren(node, " =$ ", false, data);
902     }
903 
904     @Override
905     protected Object visit(final ASTExtendedLiteral node, final Object data) {
906         builder.append("...");
907         return data;
908     }
909 
910     @Override
911     protected Object visit(final ASTFalseNode node, final Object data) {
912         return check(node, "false", data);
913     }
914 
915     @Override
916     protected Object visit(final ASTForeachStatement node, final Object data) {
917         final int form = node.getLoopForm();
918         builder.append("for (");
919         final JexlNode body;
920         if (form == 0) {
921             // for( .. : ...)
922             accept(node.jjtGetChild(0), data);
923             builder.append(" : ");
924             accept(node.jjtGetChild(1), data);
925             builder.append(") ");
926             body = node.jjtGetNumChildren() > 2? node.jjtGetChild(2) : null;
927         } else {
928             // for( .. ; ... ; ..)
929             int nc = 0;
930             // first child is var declaration(s)
931             final JexlNode vars = (form & 1) != 0 ? node.jjtGetChild(nc++) : null;
932             final JexlNode predicate = (form & 2) != 0 ? node.jjtGetChild(nc++) : null;
933             // the loop step
934             final JexlNode step = (form & 4) != 0 ? node.jjtGetChild(nc++) : null;
935             // last child is body
936             body = (form & 8) != 0 ? node.jjtGetChild(nc) : null;
937             if (vars != null) {
938                 accept(vars, data);
939             }
940             builder.append("; ");
941             if (predicate != null) {
942                 accept(predicate, data);
943             }
944             builder.append("; ");
945             if (step != null) {
946                 accept(step, data);
947             }
948             builder.append(") ");
949         }
950         // the body
951         if (body != null) {
952             accept(body, data);
953         } else {
954             builder.append(';');
955         }
956         return data;
957     }
958 
959     @Override
960     protected Object visit(final ASTFunctionNode node, final Object data) {
961         final int num = node.jjtGetNumChildren();
962         if (num == 3) {
963             accept(node.jjtGetChild(0), data);
964             builder.append(":");
965             accept(node.jjtGetChild(1), data);
966             accept(node.jjtGetChild(2), data);
967         } else if (num == 2) {
968             accept(node.jjtGetChild(0), data);
969             accept(node.jjtGetChild(1), data);
970         }
971         return data;
972     }
973 
974     @Override
975     protected Object visit(final ASTGENode node, final Object data) {
976         return infixChildren(node, " >= ", false, data);
977     }
978 
979     @Override
980     protected Object visit(final ASTGetDecrementNode node, final Object data) {
981         return postfixChild(node, "--", data);
982     }
983 
984     @Override
985     protected Object visit(final ASTGetIncrementNode node, final Object data) {
986         return postfixChild(node, "++", data);
987     }
988 
989     @Override
990     protected Object visit(final ASTGTNode node, final Object data) {
991         return infixChildren(node, " > ", false, data);
992     }
993 
994     @Override
995     protected Object visit(final ASTIdentifier node, final Object data) {
996         final String ns = node.getNamespace();
997         final String image = StringParser.escapeIdentifier(node.getName());
998         if (ns == null) {
999             return check(node, image, data);
1000         }
1001         final String nsid = StringParser.escapeIdentifier(ns) + ":" + image;
1002         return check(node, nsid, data);
1003     }
1004 
1005     @Override
1006     protected Object visit(final ASTIdentifierAccess node, final Object data) {
1007         builder.append(node.isSafe() ? "?." : ".");
1008         final String image = node.getName();
1009         if (node.isExpression()) {
1010             builder.append('`');
1011             builder.append(image.replace("`", "\\`"));
1012             builder.append('`');
1013         } else if (needQuotes(image)) {
1014             // quote it
1015             builder.append('\'');
1016             builder.append(image.replace("'", "\\'"));
1017             builder.append('\'');
1018         } else {
1019             builder.append(image);
1020         }
1021         return data;
1022     }
1023 
1024     @Override
1025     protected Object visit(final ASTIfStatement node, final Object data) {
1026         final int numChildren = node.jjtGetNumChildren();
1027         // if (...) ...
1028         builder.append("if (");
1029         accept(node.jjtGetChild(0), data);
1030         builder.append(") ");
1031         acceptStatement(node.jjtGetChild(1), data);
1032         //.. else if (...) ...
1033         for(int c = 2; c <  numChildren - 1; c += 2) {
1034             builder.append(" else if (");
1035             accept(node.jjtGetChild(c), data);
1036             builder.append(") ");
1037             acceptStatement(node.jjtGetChild(c + 1), data);
1038         }
1039         // else... (if odd)
1040         if ((numChildren & 1) == 1) {
1041             builder.append(" else ");
1042             acceptStatement(node.jjtGetChild(numChildren - 1), data);
1043         }
1044         return data;
1045     }
1046 
1047     @Override
1048     protected Object visit(final ASTIncrementGetNode node, final Object data) {
1049         return prefixChild(node, "++", data);
1050     }
1051 
1052     @Override
1053     protected Object visit(final ASTInstanceOf node, final Object data) {
1054         return infixChildren(node, " instanceof ", false, data);
1055     }
1056 
1057     @Override
1058     protected Object visit(final ASTJexlScript node, final Object arg) {
1059         if (outputPragmas) {
1060             writePragmas(builder, node.getPragmas());
1061         }
1062         Object data = arg;
1063         boolean named = false;
1064         // if lambda, produce parameters
1065         if (node instanceof ASTJexlLambda) {
1066             final ASTJexlLambda lambda = (ASTJexlLambda) node;
1067             final JexlNode parent = node.jjtGetParent();
1068             // use lambda syntax if not assigned
1069             final boolean expr = isLambdaExpr(lambda);
1070             named = node.jjtGetChild(0) instanceof ASTVar;
1071             final boolean assigned = parent instanceof ASTAssignment || named;
1072             if (assigned && !expr) {
1073                 builder.append("function");
1074                 if (named) {
1075                     final ASTVar avar = (ASTVar) node.jjtGetChild(0);
1076                     builder.append(' ');
1077                     builder.append(avar.getName());
1078                 }
1079             }
1080             builder.append('(');
1081             final String[] params = lambda.getParameters();
1082             if (params != null) {
1083                 final Scope scope = lambda.getScope();
1084                 final LexicalScope lexicalScope = lambda.getLexicalScope();
1085                 for (int p = 0; p < params.length; ++p) {
1086                     if (p > 0) {
1087                         builder.append(", ");
1088                     }
1089                     final String param = params[p];
1090                     final int symbol = scope.getSymbol(param);
1091                     if (lexicalScope.isConstant(symbol)) {
1092                         builder.append("const ");
1093                     } else if (scope.isLexical(symbol)) {
1094                         builder.append("let ");
1095                     }
1096                     builder.append(visitParameter(param, data));
1097                 }
1098             }
1099             builder.append(')');
1100             if (assigned && !expr) {
1101                 // block follows
1102                 builder.append(' ');
1103             } else {
1104                 builder.append(arrow);
1105                 // add a space if lambda expr otherwise block follows
1106                 if (expr) {
1107                     builder.append(' ');
1108                 }
1109             }
1110         }
1111         // no parameters or done with them
1112         final int num = node.jjtGetNumChildren();
1113         if (num == 1 && !(node instanceof ASTJexlLambda)) {
1114             data = accept(node.jjtGetChild(0), data);
1115         } else {
1116             for (int i = named? 1 : 0; i < num; ++i) {
1117                 final JexlNode child = node.jjtGetChild(i);
1118                 acceptStatement(child, data);
1119             }
1120         }
1121         return data;
1122     }
1123 
1124     @Override
1125     protected Object visit(final ASTJxltLiteral node, final Object data) {
1126         final String img = StringParser.escapeString(node.getLiteral(), '`');
1127         return check(node, img, data);
1128     }
1129 
1130     @Override
1131     protected Object visit(final ASTLENode node, final Object data) {
1132         return infixChildren(node, " <= ", false, data);
1133     }
1134 
1135     @Override
1136     protected Object visit(final ASTLTNode node, final Object data) {
1137         return infixChildren(node, " < ", false, data);
1138     }
1139 
1140     @Override
1141     protected Object visit(final ASTMapEntry node, final Object data) {
1142         accept(node.jjtGetChild(0), data);
1143         builder.append(" : ");
1144         accept(node.jjtGetChild(1), data);
1145         return data;
1146     }
1147 
1148     @Override
1149     protected Object visit(final ASTMapLiteral node, final Object data) {
1150         final int num = node.jjtGetNumChildren();
1151         builder.append("{ ");
1152         if (num > 0) {
1153             if (depth <= 0) {
1154                 builder.append("...");
1155             } else {
1156                 accept(node.jjtGetChild(0), data);
1157                 for (int i = 1; i < num; ++i) {
1158                     builder.append(",");
1159                     accept(node.jjtGetChild(i), data);
1160                 }
1161             }
1162         } else {
1163             builder.append(':');
1164         }
1165         builder.append(" }");
1166         return data;
1167     }
1168 
1169     @Override
1170     protected Object visit(final ASTMethodNode node, final Object data) {
1171         final int num = node.jjtGetNumChildren();
1172         if (num == 2) {
1173             accept(node.jjtGetChild(0), data);
1174             if (depth <= 0) {
1175                 builder.append("(...)");
1176             } else {
1177                 accept(node.jjtGetChild(1), data);
1178             }
1179         }
1180         return data;
1181     }
1182 
1183     @Override
1184     protected Object visit(final ASTModNode node, final Object data) {
1185         return infixChildren(node, " % ", false, data);
1186     }
1187 
1188     @Override
1189     protected Object visit(final ASTMulNode node, final Object data) {
1190         return infixChildren(node, " * ", false, data);
1191     }
1192 
1193     @Override
1194     protected Object visit(final ASTNENode node, final Object data) {
1195         return infixChildren(node, " != ", false, data);
1196     }
1197 
1198     @Override
1199     protected Object visit(final ASTNESNode node, final Object data) {
1200         return infixChildren(node, " !== ", false, data);
1201     }
1202 
1203     @Override
1204     protected Object visit(final ASTNEWNode node, final Object data) {
1205         return infixChildren(node, " !$ ", false, data);
1206     }
1207 
1208     @Override
1209     protected Object visit(final ASTNotInstanceOf node, final Object data) {
1210         return infixChildren(node, " !instanceof ", false, data);
1211     }
1212 
1213     @Override
1214     protected Object visit(final ASTNotNode node, final Object data) {
1215         builder.append("!");
1216         accept(node.jjtGetChild(0), data);
1217         return data;
1218     }
1219 
1220     @Override
1221     protected Object visit(final ASTNRNode node, final Object data) {
1222         return infixChildren(node, " !~ ", false, data);
1223     }
1224 
1225     @Override
1226     protected Object visit(final ASTNSWNode node, final Object data) {
1227         return infixChildren(node, " !^ ", false, data);
1228     }
1229 
1230     @Override
1231     protected Object visit(final ASTNullLiteral node, final Object data) {
1232         check(node, "null", data);
1233         return data;
1234     }
1235 
1236     @Override
1237     protected Object visit(final ASTNullpNode node, final Object data) {
1238         accept(node.jjtGetChild(0), data);
1239         builder.append("??");
1240         accept(node.jjtGetChild(1), data);
1241         return data;
1242     }
1243 
1244     @Override
1245     protected Object visit(final ASTNumberLiteral node, final Object data) {
1246         return check(node, node.toString(), data);
1247     }
1248 
1249     @Override
1250     protected Object visit(final ASTOrNode node, final Object data) {
1251         // need parenthesis if not in operator precedence order
1252         final boolean paren = node.jjtGetParent() instanceof ASTAndNode;
1253         return infixChildren(node, " || ", paren, data);
1254     }
1255 
1256     @Override
1257     protected Object visit(final ASTQualifiedIdentifier node, final Object data) {
1258         final String img = node.getName();
1259         return check(node, img, data);
1260     }
1261 
1262     @Override
1263     protected Object visit(final ASTRangeNode node, final Object data) {
1264         if (depth <= 0) {
1265             builder.append("( .. )");
1266             return data;
1267         }
1268         return infixChildren(node, " .. ", false, data);
1269     }
1270 
1271     @Override
1272     protected Object visit(final ASTReference node, final Object data) {
1273         final int num = node.jjtGetNumChildren();
1274         for (int i = 0; i < num; ++i) {
1275             accept(node.jjtGetChild(i), data);
1276         }
1277         return data;
1278     }
1279 
1280     @Override
1281     protected Object visit(final ASTReferenceExpression node, final Object data) {
1282         final JexlNode first = node.jjtGetChild(0);
1283         builder.append('(');
1284         accept(first, data);
1285         builder.append(')');
1286         final int num = node.jjtGetNumChildren();
1287         for (int i = 1; i < num; ++i) {
1288             builder.append("[");
1289             accept(node.jjtGetChild(i), data);
1290             builder.append("]");
1291         }
1292         return data;
1293     }
1294 
1295     @Override
1296     protected Object visit(final ASTRegexLiteral node, final Object data) {
1297         final String img = StringParser.escapeString(node.toString(), '/');
1298         return check(node, "~" + img, data);
1299     }
1300 
1301     @Override
1302     protected Object visit(final ASTReturnStatement node, final Object data) {
1303         builder.append("return");
1304         if (node.jjtGetNumChildren() > 0) {
1305             builder.append(' ');
1306             accept(node.jjtGetChild(0), data);
1307         }
1308         return data;
1309     }
1310 
1311     @Override
1312     protected Object visit(final ASTSetAddNode node, final Object data) {
1313         return infixChildren(node, " += ", false, data);
1314     }
1315 
1316     @Override
1317     protected Object visit(final ASTSetAndNode node, final Object data) {
1318         return infixChildren(node, " &= ", false, data);
1319     }
1320 
1321     @Override
1322     protected Object visit(final ASTSetDivNode node, final Object data) {
1323         return infixChildren(node, " /= ", false, data);
1324     }
1325 
1326     @Override
1327     protected Object visit(final ASTSetLiteral node, final Object data) {
1328         final int num = node.jjtGetNumChildren();
1329         builder.append("{ ");
1330         if (num > 0) {
1331             if (depth <= 0) {
1332                 builder.append("...");
1333             } else {
1334                 accept(node.jjtGetChild(0), data);
1335                 for (int i = 1; i < num; ++i) {
1336                     builder.append(",");
1337                     accept(node.jjtGetChild(i), data);
1338                 }
1339             }
1340         }
1341         builder.append(" }");
1342         return data;
1343     }
1344 
1345     @Override
1346     protected Object visit(final ASTSetModNode node, final Object data) {
1347         return infixChildren(node, " %= ", false, data);
1348     }
1349 
1350     @Override
1351     protected Object visit(final ASTSetMultNode node, final Object data) {
1352         return infixChildren(node, " *= ", false, data);
1353     }
1354 
1355     @Override
1356     protected Object visit(final ASTSetOrNode node, final Object data) {
1357         return infixChildren(node, " |= ", false, data);
1358     }
1359 
1360     @Override
1361     protected Object visit(final ASTSetShiftLeftNode node, final Object data) {
1362         return infixChildren(node, " <<= ", false, data);
1363     }
1364 
1365     @Override
1366     protected Object visit(final ASTSetShiftRightNode node, final Object data) {
1367         return infixChildren(node, " >>= ", false, data);
1368     }
1369 
1370     @Override
1371     protected Object visit(final ASTSetShiftRightUnsignedNode node, final Object data) {
1372         return infixChildren(node, " >>>= ", false, data);
1373     }
1374 
1375     @Override
1376     protected Object visit(final ASTSetSubNode node, final Object data) {
1377         return infixChildren(node, " -= ", false, data);
1378     }
1379 
1380     @Override
1381     protected Object visit(final ASTSetXorNode node, final Object data) {
1382         return infixChildren(node, " ^= ", false, data);
1383     }
1384 
1385     @Override
1386     protected Object visit(final ASTShiftLeftNode node, final Object data) {
1387         return infixChildren(node, " << ", false, data);
1388     }
1389 
1390     @Override
1391     protected Object visit(final ASTShiftRightNode node, final Object data) {
1392         return infixChildren(node, " >> ", false, data);
1393     }
1394 
1395     @Override
1396     protected Object visit(final ASTShiftRightUnsignedNode node, final Object data) {
1397         return infixChildren(node, " >>> ", false, data);
1398     }
1399 
1400     @Override
1401     protected Object visit(final ASTSizeFunction node, final Object data) {
1402         builder.append("size ");
1403         accept(node.jjtGetChild(0), data);
1404         return data;
1405     }
1406 
1407     @Override
1408     protected Object visit(final ASTStringLiteral node, final Object data) {
1409         final String img = StringParser.escapeString(node.getLiteral(), '\'');
1410         return check(node, img, data);
1411     }
1412 
1413     @Override
1414     protected Object visit(final ASTSubNode node, final Object data) {
1415         return additiveNode(node, " - ", data);
1416     }
1417 
1418     @Override
1419     protected Object visit(final ASTSWNode node, final Object data) {
1420         return infixChildren(node, " =^ ", false, data);
1421     }
1422 
1423     @Override
1424     protected Object visit(final ASTTernaryNode node, final Object data) {
1425         accept(node.jjtGetChild(0), data);
1426         if (node.jjtGetNumChildren() > 2) {
1427             builder.append("? ");
1428             accept(node.jjtGetChild(1), data);
1429             builder.append(" : ");
1430             accept(node.jjtGetChild(2), data);
1431         } else {
1432             builder.append("?: ");
1433             accept(node.jjtGetChild(1), data);
1434 
1435         }
1436         return data;
1437     }
1438 
1439     @Override
1440     protected Object visit(final ASTThrowStatement node, final Object data) {
1441         builder.append("throw ");
1442         accept(node.jjtGetChild(0), data);
1443         return data;
1444     }
1445 
1446     @Override
1447     protected Object visit(final ASTTrueNode node, final Object data) {
1448         check(node, "true", data);
1449         return data;
1450     }
1451 
1452     @Override
1453     protected Object visit(final ASTTryResources node, final Object data) {
1454         final int tryBody = node.jjtGetNumChildren() - 1;
1455         builder.append(" (");
1456         accept(node.jjtGetChild(0), data);
1457         for(int c = 1; c < tryBody; ++c) {
1458             builder.append("; ");
1459             accept(node.jjtGetChild(c), data);
1460         }
1461         builder.append(") ");
1462         accept(node.jjtGetChild(tryBody), data);
1463         return data;
1464     }
1465 
1466     @Override
1467     protected Object visit(final ASTTryStatement node, final Object data) {
1468         builder.append("try");
1469         int nc = 0;
1470         // try-body (with or without resources)
1471         accept(node.jjtGetChild(nc++), data);
1472         // catch-body
1473         if (node.hasCatchClause()) {
1474             builder.append("catch (");
1475             accept(node.jjtGetChild(nc++), data);
1476             builder.append(") ");
1477             accept(node.jjtGetChild(nc++), data);
1478         }
1479         // finally-body
1480         if (node.hasFinallyClause()) {
1481             builder.append(indent > 0? lf : ' ');
1482             builder.append("finally ");
1483             accept(node.jjtGetChild(nc), data);
1484         }
1485         return data;
1486     }
1487 
1488     @Override
1489     protected Object visit(final ASTUnaryMinusNode node, final Object data) {
1490         return prefixChild(node, "-", data);
1491     }
1492 
1493     @Override
1494     protected Object visit(final ASTUnaryPlusNode node, final Object data) {
1495         return prefixChild(node, "+", data);
1496     }
1497 
1498     @Override
1499     protected Object visit(final ASTVar node, final Object data) {
1500         if (node.isConstant()) {
1501             builder.append("const ");
1502         } else  if (node.isLexical()) {
1503             builder.append("let ");
1504         } else {
1505             builder.append("var ");
1506         }
1507         check(node, node.getName(), data);
1508         return data;
1509     }
1510 
1511     @Override
1512     protected Object visit(final ASTWhileStatement node, final Object data) {
1513         builder.append("while (");
1514         accept(node.jjtGetChild(0), data);
1515         builder.append(") ");
1516         if (node.jjtGetNumChildren() > 1) {
1517             acceptStatement(node.jjtGetChild(1), data);
1518         } else {
1519             builder.append(';');
1520         }
1521         return data;
1522     }
1523 
1524     /**
1525      * A pseudo visitor for parameters.
1526      *
1527      * @param p the parameter name
1528      * @param data the visitor argument
1529      * @return the parameter name to use
1530      */
1531     protected String visitParameter(final String p, final Object data) {
1532         return p;
1533     }
1534 }