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