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.parser;
18  
19  import org.apache.commons.jexl3.JexlEngine;
20  import org.apache.commons.jexl3.JexlException;
21  import org.apache.commons.jexl3.JexlFeatures;
22  import org.apache.commons.jexl3.JexlInfo;
23  import org.apache.commons.jexl3.internal.LexicalScope;
24  import org.apache.commons.jexl3.internal.Scope;
25  
26  import java.io.BufferedReader;
27  import java.io.IOException;
28  import java.io.StringReader;
29  import java.util.ArrayDeque;
30  import java.util.Arrays;
31  import java.util.Deque;
32  import java.util.HashSet;
33  import java.util.IdentityHashMap;
34  import java.util.LinkedHashSet;
35  import java.util.Map;
36  import java.util.Set;
37  import java.util.TreeMap;
38  import java.util.function.Predicate;
39  
40  
41  /**
42   * The base class for parsing, manages the parameter/local variable frame.
43   */
44  public abstract class JexlParser extends StringParser {
45      /**
46       * The associated controller.
47       */
48      protected final FeatureController featureController = new FeatureController(JexlEngine.DEFAULT_FEATURES);
49      /**
50       * The basic source info.
51       */
52      protected JexlInfo info = null;
53      /**
54       * The source being processed.
55       */
56      protected String source = null;
57      /**
58       * The map of named registers aka script parameters.
59       * <p>Each parameter is associated to a register and is materialized
60       * as an offset in the registers array used during evaluation.</p>
61       */
62      protected Scope scope = null;
63      /**
64       * When parsing inner functions/lambda, need to stack the scope (sic).
65       */
66      protected final Deque<Scope> scopes = new ArrayDeque<>();
67      /**
68       * The list of pragma declarations.
69       */
70      protected Map<String, Object> pragmas = null;
71      /**
72       * The known namespaces.
73       */
74      protected Set<String> namespaces = null;
75      /**
76       * The number of nested loops.
77       */
78      protected int loopCount = 0;
79      /**
80       * Stack of parsing loop counts.
81       */
82      protected final Deque<Integer> loopCounts = new ArrayDeque<>();
83      /**
84       * The current lexical block.
85       */
86      protected LexicalUnit block = null;
87      /**
88       * Stack of lexical blocks.
89       */
90      protected final Deque<LexicalUnit> blocks = new ArrayDeque<>();
91      /**
92       * The map of lexical to functional blocks.
93       */
94      protected final Map<LexicalUnit, Scope> blockScopes = new IdentityHashMap<>();
95  
96      /**
97       * A lexical unit is the container defining local symbols and their
98       * visibility boundaries.
99       */
100     public interface LexicalUnit {
101         /**
102          * Declares a local symbol.
103          * @param symbol the symbol index in the scope
104          * @return true if declaration was successful, false if symbol was already declared
105          */
106         boolean declareSymbol(int symbol);
107         void setConstant(int symbol);
108 
109         /**
110          * Checks whether a symbol is declared in this lexical unit.
111          * @param symbol the symbol
112          * @return true if declared, false otherwise
113          */
114         boolean hasSymbol(int symbol);
115         boolean isConstant(int symbol);
116 
117         /**
118          * @return the number of local variables declared in this unit
119          */
120         int getSymbolCount();
121 
122         /**
123          * @return the set of symbols identifiers declared in this unit
124          */
125         LexicalScope getLexicalScope();
126     }
127 
128     /**
129      * Cleanup.
130      * @param features the feature set to restore if any
131      */
132     protected void cleanup(final JexlFeatures features) {
133         info = null;
134         source = null;
135         scope = null;
136         scopes.clear();
137         pragmas = null;
138         namespaces = null;
139         loopCounts.clear();
140         loopCount = 0;
141         blocks.clear();
142         block = null;
143         blockScopes.clear();
144         this.setFeatures(features);
145     }
146 
147     /**
148      * Utility function to create '.' separated string from a list of string.
149      * @param lstr the list of strings
150      * @return the dotted version
151      */
152     protected static String stringify(final Iterable<String> lstr) {
153         final StringBuilder strb = new StringBuilder();
154         boolean dot = false;
155         for(final String str : lstr) {
156             if (!dot) {
157                dot = true;
158             } else {
159                strb.append('.');
160             }
161             strb.append(str);
162         }
163         return strb.toString();
164     }
165 
166     /**
167      * Read a given source line.
168      * @param src the source
169      * @param lineno the line number
170      * @return the line
171      */
172     protected static String readSourceLine(final String src, final int lineno) {
173         String msg = "";
174         if (src != null && lineno >= 0) {
175             try {
176                 final BufferedReader reader = new BufferedReader(new StringReader(src));
177                 for (int l = 0; l < lineno; ++l) {
178                     msg = reader.readLine();
179                 }
180             } catch (final IOException xio) {
181                 // ignore, very unlikely but then again...
182             }
183         }
184         return msg;
185     }
186 
187     /**
188      * Internal, for debug purpose only.
189      * @param registers whether register syntax is recognized by this parser
190      */
191     public void allowRegisters(final boolean registers) {
192         featureController.setFeatures(new JexlFeatures(featureController.getFeatures()).register(registers));
193     }
194 
195     /**
196      * Sets a new set of options.
197      * @param features the parser features
198      */
199     protected void setFeatures(final JexlFeatures features) {
200         this.featureController.setFeatures(features);
201     }
202 
203     /**
204      * @return the current set of features active during parsing
205      */
206     protected JexlFeatures getFeatures() {
207         return featureController.getFeatures();
208     }
209 
210     /**
211      * Disables pragma feature is pragma-anywhere feature is disabled.
212      */
213     protected void controlPragmaAnywhere() {
214         final JexlFeatures features = getFeatures();
215         if (features.supportsPragma() && !features.supportsPragmaAnywhere()) {
216             featureController.setFeatures(new JexlFeatures(featureController.getFeatures()).pragma(false));
217         }
218     }
219 
220     /**
221      * Gets the frame used by this parser.
222      * <p> Since local variables create new symbols, it is important to
223      * regain access after parsing to known which / how-many registers are needed. </p>
224      * @return the named register map
225      */
226     protected Scope getScope() {
227         return scope;
228     }
229 
230     /**
231      * Create a new local variable scope and push it as current.
232      */
233     protected void pushScope() {
234         if (scope != null) {
235             scopes.push(scope);
236         }
237         scope = new Scope(scope, (String[]) null);
238         loopCounts.push(loopCount);
239         loopCount = 0;
240     }
241 
242     /**
243      * Pops back to previous local variable scope.
244      */
245     protected void popScope() {
246         if (!scopes.isEmpty()) {
247             scope = scopes.pop();
248         } else {
249             scope = null;
250         }
251         if (!loopCounts.isEmpty()) {
252             loopCount = loopCounts.pop();
253         }
254     }
255 
256     /**
257      * Gets the lexical unit used by this parser.
258      * @return the named register map
259      */
260     protected LexicalUnit getUnit() {
261         return block;
262     }
263 
264     /**
265      * Pushes a new lexical unit.
266      * @param unit the new lexical unit
267      */
268     protected void pushUnit(final LexicalUnit unit) {
269         blockScopes.put(unit, scope);
270         if (block != null) {
271             blocks.push(block);
272         }
273         block = unit;
274     }
275 
276     /**
277      * Restores the previous lexical unit.
278      * @param unit restores the previous lexical scope
279      */
280     protected void popUnit(final LexicalUnit unit) {
281         if (block == unit){
282             blockScopes.remove(unit);
283             if (!blocks.isEmpty()) {
284                 block = blocks.pop();
285             } else {
286                 block = null;
287             }
288         }
289     }
290 
291     /**
292      * Checks if a symbol is defined in lexical scopes.
293      * <p>This works with parsed scripts in template resolution only.
294      * @param info an info linked to a node
295      * @param symbol the symbol number
296      * @return true if symbol accessible in lexical scope
297      */
298     private boolean isSymbolDeclared(final JexlNode.Info info, final int symbol) {
299         JexlNode walk = info.getNode();
300         while(walk != null) {
301             if (walk instanceof JexlParser.LexicalUnit) {
302                 final LexicalScope scope = ((JexlParser.LexicalUnit) walk).getLexicalScope();
303                 if (scope != null && scope.hasSymbol(symbol)) {
304                     return true;
305                 }
306                 // stop at first new scope reset, aka lambda
307                 if (walk instanceof ASTJexlLambda) {
308                     break;
309                 }
310             }
311             walk = walk.jjtGetParent();
312         }
313         return false;
314     }
315 
316     /**
317      * Checks whether an identifier is a local variable or argument.
318      * @param name the variable name
319      * @return true if a variable with that name was declared
320      */
321     protected boolean isVariable(final String name) {
322         return scope != null && scope.getSymbol(name) != null;
323     }
324 
325     /**
326      * Checks whether an identifier is a local variable or argument, ie a symbol, stored in a register.
327      * @param identifier the identifier
328      * @param name      the identifier name
329      * @return the image
330      */
331     protected String checkVariable(final ASTIdentifier identifier, final String name) {
332         if (scope != null) {
333             final Integer symbol = scope.getSymbol(name);
334             if (symbol != null) {
335                 identifier.setLexical(scope.isLexical(symbol));
336                 boolean declared = true;
337                 if (scope.isCapturedSymbol(symbol)) {
338                     // captured are declared in all cases
339                     identifier.setCaptured(true);
340                 } else {
341                     LexicalUnit unit = block;
342                     declared = unit.hasSymbol(symbol);
343                     // one of the lexical blocks above should declare it
344                     if (!declared) {
345                         for (final LexicalUnit u : blocks) {
346                             if (u.hasSymbol(symbol)) {
347                                 unit = u;
348                                 declared = true;
349                                 break;
350                             }
351                         }
352                     }
353                     if (declared) {
354                         // track if const is defined or not
355                         if (unit.isConstant(symbol)) {
356                             identifier.setConstant(true);
357                         }
358                     } else if (info instanceof JexlNode.Info) {
359                         declared = isSymbolDeclared((JexlNode.Info) info, symbol);
360                     }
361                 }
362                 identifier.setSymbol(symbol, name);
363                 if (!declared) {
364                     identifier.setShaded(true);
365                     if (/*identifier.isLexical() ||*/ getFeatures().isLexicalShade()) {
366                         // can not reuse a local as a global
367                         throw new JexlException.Parsing(info, name + ": variable is not declared").clean();
368                     }
369                 }
370             }
371         }
372         return name;
373     }
374 
375     /**
376      * Whether a given variable name is allowed.
377      * @param image the name
378      * @return true if allowed, false if reserved
379      */
380     protected boolean allowVariable(final String image) {
381         final JexlFeatures features = getFeatures();
382         if (!features.supportsLocalVar()) {
383             return false;
384         }
385         if (features.isReservedName(image)) {
386             return false;
387         }
388         return true;
389     }
390 
391     /**
392      * Declares a symbol.
393      * @param symbol the symbol index
394      * @return true if symbol can be declared in lexical scope, false (error)
395      * if it is already declared
396      */
397     private boolean declareSymbol(final int symbol) {
398         for (final LexicalUnit lu : blocks) {
399             if (lu.hasSymbol(symbol)) {
400                 return false;
401             }
402             // stop at first new scope reset, aka lambda
403             if (lu instanceof ASTJexlLambda) {
404                 break;
405             }
406         }
407         return block == null || block.declareSymbol(symbol);
408     }
409 
410     /**
411      * Declares a local function.
412      * @param variable the identifier used to declare
413      * @param token      the variable name toekn
414      */
415     protected void declareFunction(final ASTVar variable, final Token token) {
416         final String name = token.image;
417         // function foo() ... <=> const foo = ()->...
418         if (scope == null) {
419             scope = new Scope(null);
420         }
421         final int symbol = scope.declareVariable(name);
422         variable.setSymbol(symbol, name);
423         variable.setLexical(true);
424         if (scope.isCapturedSymbol(symbol)) {
425             variable.setCaptured(true);
426         }
427         // function is const fun...
428         if (declareSymbol(symbol)) {
429             scope.addLexical(symbol);
430             block.setConstant(symbol);
431         } else {
432             if (getFeatures().isLexical()) {
433                 throw new JexlException(variable, name + ": variable is already declared");
434             }
435             variable.setRedefined(true);
436         }
437     }
438 
439     /**
440      * Declares a local variable.
441      * <p> This method creates an new entry in the symbol map. </p>
442      * @param variable the identifier used to declare
443      * @param lexical whether the symbol is lexical
444      * @param constant whether the symbol is constant
445      * @param token      the variable name toekn
446      */
447     protected void declareVariable(final ASTVar variable, final Token token, final boolean lexical, final boolean constant) {
448         final String name = token.image;
449         if (!allowVariable(name)) {
450             throwFeatureException(JexlFeatures.LOCAL_VAR, token);
451         }
452         if (scope == null) {
453             scope = new Scope(null);
454         }
455         final int symbol = scope.declareVariable(name);
456         variable.setSymbol(symbol, name);
457         variable.setLexical(lexical);
458         variable.setConstant(constant);
459         if (scope.isCapturedSymbol(symbol)) {
460             variable.setCaptured(true);
461         }
462         // if not the first time we declare this symbol...
463         if (!declareSymbol(symbol)) {
464             if (lexical || scope.isLexical(symbol) || getFeatures().isLexical()) {
465                 throw new JexlException.Parsing(variable.jexlInfo(), name + ": variable is already declared").clean();
466             }
467             // not lexical, redefined nevertheless
468             variable.setRedefined(true);
469         } else if (lexical) {
470             scope.addLexical(symbol);
471             if (constant) {
472                 block.setConstant(symbol);
473             }
474         }
475     }
476 
477     /**
478      * Declares a local parameter.
479      * <p> This method creates an new entry in the symbol map. </p>
480      * @param token the parameter name token
481      * @param lexical whether the parameter is lexical or not
482      * @param constant whether the parameter is constant or not
483      */
484     protected void declareParameter(final Token token, final boolean lexical, final boolean constant) {
485         final String identifier =  token.image;
486         if (!allowVariable(identifier)) {
487             throwFeatureException(JexlFeatures.LOCAL_VAR, token);
488         }
489         if (scope == null) {
490             scope = new Scope(null, (String[]) null);
491         }
492         final int symbol = scope.declareParameter(identifier);
493         // not sure how declaring a parameter could fail...
494         // lexical feature error
495         if (!block.declareSymbol(symbol)) {
496             if (lexical || getFeatures().isLexical()) {
497                 final JexlInfo xinfo = info.at(token.beginLine, token.beginColumn);
498                 throw new JexlException.Parsing(xinfo, identifier + ": parameter is already declared").clean();
499             }
500         } else if (lexical) {
501             scope.addLexical(symbol);
502             if (constant) {
503                 block.setConstant(symbol);
504             }
505         }
506     }
507 
508     /**
509      * The name of the options pragma.
510      */
511     public static final String PRAGMA_OPTIONS = "jexl.options";
512     /**
513      * The prefix of a namespace pragma.
514      */
515     public static final String PRAGMA_JEXLNS = "jexl.namespace.";
516     /**
517      * The prefix of a module pragma.
518      */
519     public static final String PRAGMA_MODULE = "jexl.module.";
520     /**
521      * The import pragma.
522      */
523     public static final String PRAGMA_IMPORT = "jexl.import";
524 
525     /**
526      * Adds a pragma declaration.
527      * @param key the pragma key
528      * @param value the pragma value
529      */
530     protected void declarePragma(final String key, final Object value) {
531         final JexlFeatures features = getFeatures();
532         if (!features.supportsPragma()) {
533             throwFeatureException(JexlFeatures.PRAGMA, getToken(0));
534         }
535         if (PRAGMA_IMPORT.equals(key) && !features.supportsImportPragma()) {
536             throwFeatureException(JexlFeatures.IMPORT_PRAGMA, getToken(0));
537         }
538         if (pragmas == null) {
539             pragmas = new TreeMap<>();
540         }
541         // declaring a namespace
542         final Predicate<String> ns = features.namespaceTest();
543         if (ns != null && key.startsWith(PRAGMA_JEXLNS)) {
544             if (!features.supportsNamespacePragma()) {
545                 throwFeatureException(JexlFeatures.NS_PRAGMA, getToken(0));
546             }
547             // jexl.namespace.***
548             final String nsname = key.substring(PRAGMA_JEXLNS.length());
549             if (!nsname.isEmpty()) {
550                 if (namespaces == null) {
551                     namespaces = new HashSet<>();
552                 }
553                 namespaces.add(nsname);
554             }
555         }
556         // merge new value into a set created on the fly if key is already mapped
557         pragmas.merge(key, value, (previous, newValue)->{
558             if (previous instanceof Set<?>) {
559                 ((Set<Object>) previous).add(newValue);
560                 return previous;
561             }
562             final Set<Object> values = new LinkedHashSet<>();
563             values.add(previous);
564             values.add(newValue);
565             return values;
566         });
567     }
568 
569     /**
570      * Checks whether a name identifies a declared namespace.
571      * @param token the namespace token
572      * @return true if the name qualifies a namespace
573      */
574     protected boolean isDeclaredNamespace(final Token token, final Token colon) {
575         // syntactic hint, the namespace sticks to the colon
576         if (colon != null && ":".equals(colon.image) && colon.beginColumn - 1 == token.endColumn) {
577             return true;
578         }
579         // if name is shared with a variable name, use syntactic hint
580         final String name = token.image;
581         if (!isVariable(name)) {
582             final Set<String> ns = namespaces;
583             // declared through local pragma ?
584             if (ns != null && ns.contains(name)) {
585                 return true;
586             }
587             // declared through engine features ?
588             if (getFeatures().namespaceTest().test(name)) {
589                 return true;
590             }
591         }
592         return false;
593     }
594 
595     /**
596      * Default implementation does nothing but is overridden by generated code.
597      * @param top whether the identifier is beginning an l/r value
598      * @throws ParseException subclasses may throw this
599      */
600     protected void Identifier(final boolean top) throws ParseException {
601         // Overridden by generated code
602     }
603 
604     /**
605      * Overridden in actual parser to access tokens stack.
606      * @param index 0 to get current token
607      * @return the token on the stack
608      */
609     protected abstract Token getToken(int index);
610 
611     /**
612      * The set of assignment operators as classes.
613      */
614     private static final Set<Class<? extends JexlNode>> ASSIGN_NODES = new HashSet<>(
615         Arrays.asList(
616             ASTAssignment.class,
617             ASTSetAddNode.class,
618             ASTSetSubNode.class,
619             ASTSetMultNode.class,
620             ASTSetDivNode.class,
621             ASTSetModNode.class,
622             ASTSetAndNode.class,
623             ASTSetOrNode.class,
624             ASTSetXorNode.class,
625             ASTSetShiftLeftNode.class,
626             ASTSetShiftRightNode.class,
627             ASTSetShiftRightUnsignedNode.class,
628             ASTIncrementGetNode.class,
629             ASTDecrementGetNode.class,
630             ASTGetDecrementNode.class,
631             ASTGetIncrementNode.class
632         )
633     );
634 
635     /**
636      * Called by parser at beginning of node construction.
637      * @param node the node
638      */
639     protected void jjtreeOpenNodeScope(final JexlNode node) {
640 //        if (node instanceof ASTBlock || node instanceof ASTForeachStatement) {
641 //            final LexicalUnit unit = (LexicalUnit) node;
642 //            unit.setScope(scope);
643 //        }
644     }
645 
646     /**
647      * Called by parser at end of node construction.
648      * <p>
649      * Detects "Ambiguous statement" and 'non-left value assignment'.</p>
650      * @param node the node
651      * @throws JexlException.Parsing when parsing fails
652      */
653     protected void jjtreeCloseNodeScope(final JexlNode node) {
654         if (node instanceof ASTAmbiguous) {
655             throwAmbiguousException(node);
656         }
657         if (node instanceof ASTJexlScript) {
658             if (node instanceof ASTJexlLambda && !getFeatures().supportsLambda()) {
659                 throwFeatureException(JexlFeatures.LAMBDA, node.jexlInfo());
660             }
661             final ASTJexlScript script = (ASTJexlScript) node;
662             // reaccess in case local variables have been declared
663             if (script.getScope() != scope) {
664                 script.setScope(scope);
665             }
666         } else if (ASSIGN_NODES.contains(node.getClass())) {
667             final JexlNode lv = node.jjtGetChild(0);
668             if (!lv.isLeftValue()) {
669                 JexlInfo xinfo = lv.jexlInfo();
670                 xinfo = info.at(xinfo.getLine(), xinfo.getColumn());
671                 final String msg = readSourceLine(source, xinfo.getLine());
672                 throw new JexlException.Assignment(xinfo, msg).clean();
673             }
674             if (lv instanceof ASTIdentifier && !(lv instanceof ASTVar)) {
675                 final ASTIdentifier var = (ASTIdentifier) lv;
676                 if (isConstant(var.getSymbol())) { // if constant, fail...
677                     JexlInfo xinfo = lv.jexlInfo();
678                     xinfo = info.at(xinfo.getLine(), xinfo.getColumn());
679                     throw new JexlException.Assignment(xinfo, var.getName()).clean();
680                 }
681             }
682         }
683         // heavy check
684         featureController.controlNode(node);
685     }
686 
687     /**
688      * Checks whether a symbol has been declared as a const in the current stack of lexical units.
689      * @param symbol the symbol
690      * @return true if constant, false otherwise
691      */
692     private boolean isConstant(int symbol) {
693         if (symbol >= 0) {
694             if (block != null && block.hasSymbol(symbol)) {
695                 return block.isConstant(symbol);
696             }
697             Scope blockScope = blockScopes.get(block);
698             int lexical = symbol;
699             for (LexicalUnit unit : blocks) {
700                 Scope unitScope = blockScopes.get(unit);
701                 // follow through potential capture
702                 if (blockScope != unitScope) {
703                     int declared = blockScope.getCaptureDeclaration(lexical);
704                     if (declared >= 0) {
705                         lexical = declared;
706                     }
707                     if (unitScope != null) {
708                         blockScope = unitScope;
709                     }
710                 }
711                 if (unit.hasSymbol(lexical)) {
712                     return unit.isConstant(lexical);
713                 }
714             }
715         }
716         return false;
717     }
718 
719     /**
720      * Check fat vs thin arrow syntax feature.
721      * @param token the arrow token
722      */
723     protected void checkLambda(final Token token) {
724         final String arrow = token.image;
725         if ("->".equals(arrow)) {
726             if (!getFeatures().supportsThinArrow()) {
727                 throwFeatureException(JexlFeatures.THIN_ARROW, token);
728             }
729             return;
730         }
731         if ("=>".equals(arrow) && !getFeatures().supportsFatArrow()) {
732             throwFeatureException(JexlFeatures.FAT_ARROW, token);
733         }
734     }
735 
736     /**
737      * Throws Ambiguous exception.
738      * <p>Seeks the end of the ambiguous statement to recover.
739      * @param node the first token in ambiguous expression
740      * @throws JexlException.Ambiguous in all cases
741      */
742     protected void throwAmbiguousException(final JexlNode node) {
743         final JexlInfo begin = node.jexlInfo();
744         final Token t = getToken(0);
745         final JexlInfo end = info.at(t.beginLine, t.endColumn);
746         final String msg = readSourceLine(source, end.getLine());
747         throw new JexlException.Ambiguous(begin, end, msg).clean();
748     }
749 
750     /**
751      * Throws a feature exception.
752      * @param feature the feature code
753      * @param info the exception surroundings
754      * @throws JexlException.Feature in all cases
755      */
756     protected void throwFeatureException(final int feature, final JexlInfo info) {
757         final String msg = info != null? readSourceLine(source, info.getLine()) : null;
758         throw new JexlException.Feature(info, feature, msg).clean();
759     }
760 
761     /**
762      * Throws a feature exception.
763      * @param feature the feature code
764      * @param trigger the token that triggered it
765      * @throws JexlException.Parsing if actual error token can not be found
766      * @throws JexlException.Feature in all other cases
767      */
768     protected void throwFeatureException(final int feature, final Token trigger) {
769         Token token = trigger;
770         if (token == null) {
771             token = this.getToken(0);
772             if (token == null) {
773                 throw new JexlException.Parsing(null, JexlFeatures.stringify(feature)).clean();
774             }
775         }
776         final JexlInfo xinfo = info.at(token.beginLine, token.beginColumn);
777         throwFeatureException(feature, xinfo);
778     }
779 
780     /**
781      * Throws a parsing exception.
782      * @param parsed the token to report
783      * @throws JexlException.Parsing in all cases
784      */
785     protected void throwParsingException(final Token parsed) {
786         JexlInfo xinfo  = null;
787         String msg = "unrecoverable state";
788         Token token = parsed;
789         if (token == null) {
790             token = this.getToken(0);
791         }
792         if (token != null) {
793             xinfo = info.at(token.beginLine, token.beginColumn);
794             msg = token.image;
795         }
796         throw new JexlException.Parsing(xinfo, msg).clean();
797     }
798 
799     /**
800      * Pick the most significant token for error reporting.
801      * @param tokens the tokens to choose from
802      * @return the token
803      */
804     protected static Token errorToken(final Token... tokens) {
805         for (final Token token : tokens) {
806             if (token != null && token.image != null && !token.image.isEmpty()) {
807                 return token;
808             }
809         }
810         return null;
811     }
812 }