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