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;
18  
19  import static org.apache.commons.jexl3.JexlTestCase.createEngine;
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertFalse;
22  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
23  import static org.junit.jupiter.api.Assertions.assertNotEquals;
24  import static org.junit.jupiter.api.Assertions.assertNotNull;
25  import static org.junit.jupiter.api.Assertions.assertNull;
26  import static org.junit.jupiter.api.Assertions.assertThrows;
27  import static org.junit.jupiter.api.Assertions.assertTrue;
28  import static org.junit.jupiter.api.Assertions.fail;
29  
30  import java.io.StringReader;
31  import java.io.StringWriter;
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.BitSet;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Set;
38  import java.util.concurrent.Callable;
39  
40  import org.apache.commons.jexl3.internal.LexicalScope;
41  import org.apache.commons.jexl3.internal.Script;
42  import org.junit.jupiter.api.Test;
43  
44  /**
45   * Test cases for lexical option and feature.
46   */
47  class LexicalTest {
48  
49      public static class DebugContext extends MapContext {
50          public DebugContext() {
51          }
52          public Object debug(final Object arg) {
53              return arg;
54          }
55      }
56  
57      public static class OptAnnotationContext extends JexlEvalContext implements JexlContext.AnnotationProcessor {
58          @Override
59          public Object processAnnotation(final String name, final Object[] args, final Callable<Object> statement) throws Exception {
60              // transient side effect for strict
61              if ("scale".equals(name)) {
62                  final JexlOptions options = getEngineOptions();
63                  final int scale = options.getMathScale();
64                  final int newScale = (Integer) args[0];
65                  options.setMathScale(newScale);
66                  try {
67                      return statement.call();
68                  } finally {
69                      options.setMathScale(scale);
70                  }
71              }
72              return statement.call();
73          }
74      }
75  
76      /**
77       * Context augmented with a tryCatch.
78       */
79      public static class TestContext extends JexlEvalContext {
80          public TestContext() {}
81          public TestContext(final Map<String, Object> map) {
82              super(map);
83          }
84  
85          /**
86           * Allows calling a script and catching its raised exception.
87           * @param tryFn the lambda to call
88           * @param catchFn the lambda catching the exception
89           * @param args the arguments to the lambda
90           * @return the tryFn result or the catchFn if an exception was raised
91           */
92          public Object tryCatch(final JexlScript tryFn, final JexlScript catchFn, final Object... args) {
93              Object result;
94              try {
95                  result = tryFn.execute(this, args);
96              } catch (final Throwable xthrow) {
97                  result = catchFn != null ? catchFn.execute(this, xthrow) : xthrow;
98              }
99              return result;
100         }
101     }
102 
103     public static class VarContext extends MapContext implements JexlContext.PragmaProcessor, JexlContext.OptionsHandle {
104         private JexlOptions options = new JexlOptions();
105 
106         @Override
107         public JexlOptions getEngineOptions() {
108             return options;
109         }
110 
111         @Override
112         public void processPragma(final String key, final Object value) {
113             if ("jexl.options".equals(key) && "canonical".equals(value)) {
114                 options.setStrict(true);
115                 options.setLexical(true);
116                 options.setLexicalShade(true);
117                 options.setSafe(false);
118             }
119         }
120 
121         JexlOptions snatchOptions() {
122             final JexlOptions o = options;
123             options = new JexlOptions();
124             return o;
125         }
126     }
127 
128     private void checkParse(final JexlFeatures f, final List<String> srcs, final boolean expected) {
129         final JexlEngine jexl = new JexlBuilder().features(f).strict(true).create();
130         for(final String src : srcs) {
131             if (!src.isEmpty()) {
132                 try {
133                     final JexlScript script = jexl.createScript(src);
134                     if (!expected) {
135                         fail(src);
136                     }
137                 } catch (final JexlException.Parsing xlexical) {
138                     if (expected) {
139                         fail(src);
140                     }
141                     //assertTrue(xlexical.detailedMessage().contains("x"));
142                 }
143             }
144         }
145 
146     }
147 
148     private void checkParse(final List<String> srcs, final boolean expected) {
149         checkParse(null, srcs, expected);
150     }
151 
152     void runLexical0(final boolean feature) {
153         final JexlFeatures f = new JexlFeatures();
154         f.lexical(feature);
155         final JexlEngine jexl = new JexlBuilder().strict(true).features(f).create();
156         final JexlEvalContext ctxt = new JexlEvalContext();
157         final JexlOptions options = ctxt.getEngineOptions();
158         // ensure errors will throw
159         options.setLexical(true);
160         runLexical0(jexl, ctxt, "var x = 0; var x = 1;", feature);
161         runLexical0(jexl, ctxt, "var x = 0; for(var y : null) { let y = 1; }", feature);
162         runLexical0(jexl, ctxt, "var x = 0; for(var x : null) {};", feature);
163         runLexical0(jexl, ctxt, "(x)->{ var x = 0; x; }", feature);
164         runLexical0(jexl, ctxt, "var x; if (true) { if (true) { var x = 0; x; } }", feature);
165         runLexical0(jexl, ctxt, "if (a) { var y = (x)->{ var x = 0; x; }; y(2) }", feature);
166         final JexlException xany = assertThrows(JexlException.class, () -> {
167             final JexlScript script = jexl.createScript("(x)->{ for(var x : null) { x; } }");
168             if (!feature) {
169                 script.execute(ctxt, 42);
170             }
171         });
172         assertNotNull(xany.toString());
173         // no fail
174         final JexlScript script = jexl.createScript("var x = 32; y = (()->{ for(var x : [42]) { x; }})(); x");
175         if (!feature) {
176             assertEquals(32, script.execute(ctxt, 42));
177             assertEquals(42, ctxt.get("y"));
178         }
179     }
180 
181     private void runLexical0(final JexlEngine jexl, final JexlEvalContext ctxt, final String source, final boolean feature) {
182         final JexlException xany = assertThrows(JexlException.class, () -> {
183             final JexlScript script = jexl.createScript(source);
184             if (!feature) {
185                 script.execute(ctxt);
186             }
187         });
188         assertNotNull(xany.toString());
189     }
190 
191     void runLexical1(final boolean shade) {
192         final JexlEngine jexl = new JexlBuilder().strict(true).create();
193         final JexlEvalContext ctxt = new JexlEvalContext();
194         Object result;
195         ctxt.set("x", 4242);
196         final JexlOptions options = ctxt.getEngineOptions();
197         // ensure errors will throw
198         options.setLexical(true);
199         options.setLexicalShade(shade);
200         JexlScript script;
201         try {
202             // if local shade, x is undefined
203             script = jexl.createScript("{ var x = 0; } x");
204             script.execute(ctxt);
205             if (shade) {
206                 fail("local shade means 'x' should be undefined");
207             }
208         } catch (final JexlException xany) {
209             if (!shade) {
210                 throw xany;
211             }
212         }
213         try {
214             // if local shade, x = 42 is undefined
215             script = jexl.createScript("{ var x = 0; } x = 42");
216             script.execute(ctxt);
217             if (shade) {
218                 fail("local shade means 'x = 42' should be undefined");
219             }
220         } catch (final JexlException xany) {
221             if (!shade) {
222                 throw xany;
223             }
224         }
225         try {
226             // if local shade, x = 42 is undefined
227             script = jexl.createScript("{ var x = 0; } y = 42");
228             script.execute(ctxt);
229             if (shade) {
230                 fail("local shade means 'y = 42' should be undefined (y is undefined)");
231             }
232         } catch (final JexlException xany) {
233             if (!shade) {
234                 throw xany;
235             }
236         }
237         // no fail
238         script = jexl.createScript("var x = 32; (()->{ for(var x : null) { x; }})();");
239         //if (!feature) {
240             script.execute(ctxt, 42);
241         //}
242         // y being defined as global
243         ctxt.set("y", 4242);
244         try {
245             // if no shade and global y being defined,
246             script = jexl.createScript("{ var y = 0; } y = 42");
247             result = script.execute(ctxt);
248             if (!shade) {
249                 assertEquals(42, result);
250             } else {
251                 fail("local shade means 'y = 42' should be undefined");
252             }
253         } catch (final JexlException xany) {
254             if (!shade) {
255                 throw xany;
256             }
257         }
258     }
259 
260     protected void runLexical2(final boolean lexical) {
261         final JexlEngine jexl = new JexlBuilder().strict(true).lexical(lexical).create();
262         final JexlContext ctxt = new MapContext();
263         final JexlScript script = jexl.createScript("{var x = 42}; {var x; return x; }");
264         final Object result = script.execute(ctxt);
265         if (lexical) {
266             assertNull(result);
267         } else {
268             assertEquals(42, result);
269         }
270     }
271 
272     void runTestScope(final LexicalScope scope, final int init, final int count, final int step) {
273         final int size = (count - init) / step;
274         for(int i = init; i < count; i += step) {
275             assertTrue(scope.addSymbol(i));
276             if (i % (step + 1) == 1) {
277                 assertTrue(scope.addConstant(i));
278             }
279             assertFalse(scope.addSymbol(i));
280         }
281         for(int i = init; i < count; i += step) {
282             assertTrue(scope.hasSymbol(i));
283             for(int s = 1; s < step; ++s) {
284                 assertFalse(scope.hasSymbol(i + s));
285             }
286             if (i % (step + 1) == 1) {
287                 assertTrue(scope.isConstant(i));
288             }
289         }
290         assertEquals(size, scope.getSymbolCount());
291         final BitSet collect = new BitSet();
292         scope.clearSymbols(b -> collect.set(b));
293         for(int i = init; i < count; i += step) {
294             assertTrue(collect.get(i), "missing " + i);
295         }
296         assertEquals(0, scope.getSymbolCount());
297     }
298 
299     private JexlFeatures runVarLoop(final boolean flag, final String src) {
300         final VarContext vars = new VarContext();
301         final JexlOptions options = vars.getEngineOptions();
302         options.setLexical(true);
303         options.setLexicalShade(true);
304         options.setSafe(false);
305         final JexlFeatures features = new JexlFeatures();
306         if (flag) {
307             features.lexical(true).lexicalShade(true);
308         }
309         final JexlEngine jexl = new JexlBuilder().features(features).create();
310         final JexlScript script = jexl.createScript(src);
311         final List<Integer> out = new ArrayList<>(10);
312         vars.set("$out", out);
313         final Object result = script.execute(vars);
314         assertEquals(true, result);
315         assertEquals(10, out.size());
316         return features;
317     }
318 
319     @Test
320     void testAnnotation() {
321         final JexlFeatures f = new JexlFeatures();
322         f.lexical(true);
323         final JexlEngine jexl = new JexlBuilder().strict(true).features(f).create();
324         final JexlScript script = jexl.createScript("@scale(13) @test var i = 42");
325         final JexlContext jc = new OptAnnotationContext();
326         final Object result = script.execute(jc);
327         assertEquals(42, result);
328     }
329 
330     @Test
331     void testCaptured0() {
332         final JexlFeatures f = new JexlFeatures();
333         f.lexical(true);
334         final JexlEngine jexl = new JexlBuilder().strict(true).features(f).create();
335         final JexlScript script = jexl.createScript(
336                 "var x = 10; (b->{ x + b })(32)");
337         final JexlContext jc = new MapContext();
338         final Object result = script.execute(jc);
339         assertEquals(42, result);
340     }
341 
342     @Test
343     void testCaptured1() {
344         final JexlFeatures f = new JexlFeatures();
345         f.lexical(true);
346         final JexlEngine jexl = new JexlBuilder().strict(true).features(f).create();
347         final JexlScript script = jexl.createScript(
348                 "{ var x = 10; } (b->{ x + b })(32)");
349         final JexlContext jc = new MapContext();
350         jc.set("x", 11);
351         final Object result = script.execute(jc);
352         assertEquals(43, result);
353     }
354 
355     @Test
356     void testConst0a() {
357         final JexlFeatures f = new JexlFeatures();
358         final JexlEngine jexl = new JexlBuilder().strict(true).create();
359         final JexlScript script = jexl.createScript(
360                 "{ const x = 10; x + 1 }; { let x = 20; x = 22}");
361         final JexlContext jc = new MapContext();
362         final Object result = script.execute(jc);
363         assertEquals(22, result);
364     }
365 
366     @Test
367     void testConst0b() {
368         final JexlFeatures f = new JexlFeatures();
369         final JexlEngine jexl = new JexlBuilder().strict(true).create();
370         final JexlScript script = jexl.createScript(
371                 "{ const x = 10; }{ const x = 20; }");
372         final JexlContext jc = new MapContext();
373         final Object result = script.execute(jc);
374         assertEquals(20, result);
375     }
376 
377     @Test
378     void testConst1() {
379         final JexlFeatures f = new JexlFeatures();
380         final JexlEngine jexl = new JexlBuilder().strict(true).create();
381         final JexlException.Parsing xparse = assertThrows(JexlException.Parsing.class, () -> jexl.createScript("const foo;  foo"),
382                 "should fail, const foo must be followed by assign.");
383         assertTrue(xparse.getMessage().contains("const"));
384 
385     }
386 
387     @Test
388     void testConst2a() {
389         final JexlEngine jexl = new JexlBuilder().strict(true).create();
390         for (final String op : Arrays.asList("=", "+=", "-=", "/=", "*=", "%=", "<<=", ">>=", ">>>=", "^=", "&=", "|=")) {
391             final JexlException.Parsing xparse = assertThrows(JexlException.Parsing.class, () -> jexl.createScript("const foo = 42;  foo " + op + " 1;"),
392                     "should fail, const precludes assignment");
393             assertTrue(xparse.getMessage().contains("foo"));
394         }
395     }
396 
397     @Test
398     void testConst2b() {
399         final JexlEngine jexl = new JexlBuilder().strict(true).create();
400         for (final String op : Arrays.asList("=", "+=", "-=", "/=", "*=", "%=", "<<=", ">>=", ">>>=", "^=", "&=", "|=")) {
401             final JexlException.Parsing xparse = assertThrows(JexlException.Parsing.class,
402                     () -> jexl.createScript("const foo = 42;  if (true) { foo " + op + " 1; }"), "should fail, const precludes assignment");
403             assertTrue(xparse.getMessage().contains("foo"));
404         }
405     }
406 
407     @Test
408     void testConst2c() {
409         final JexlEngine jexl = new JexlBuilder().strict(true).create();
410         for (final String op : Arrays.asList("=", "+=", "-=", "/=", "*=", "%=", "<<=", ">>=", ">>>=", "^=", "&=", "|=")) {
411             final JexlScript script = jexl.createScript("{ const foo = 42; } { let foo  = 0; foo " + op + " 1; }");
412             assertNotNull(script);
413         }
414     }
415 
416     @Test
417     void testConst3a() {
418         final JexlEngine jexl = new JexlBuilder().create();
419         // @formatter:off
420         final List<String> srcs = Arrays.asList(
421                 "const f = ()->{ var foo = 3; foo = 5; }",
422                 "const y = '42'; const f = (let y)->{ var foo = 3; foo = 5; }",
423                 "const foo = '34'; const f = ()->{ var foo = 3; foo = 5; };",
424                 "const bar = '34'; const f = ()->{ var f = 3; f = 5; };",
425                 "const bar = '34'; const f = ()->{ var bar = 3; z ->{ bar += z; } };");
426         // @formatter:on
427         for (final String src : srcs) {
428             final JexlScript script = jexl.createScript(src);
429             final Object result = script.execute(null);
430             assertNotNull(result, src);
431         }
432     }
433 
434     @Test
435     void testConst3b() {
436         final JexlEngine jexl = new JexlBuilder().create();
437         // @formatter:off
438         final List<String> srcs = Arrays.asList(
439                 "const f = ()->{ var foo = 3; f = 5; }",
440                 "const y = '42'; const f = (let z)->{ y += z; }",
441                 "const foo = '34'; const f = ()->{ foo = 3; };",
442                 "const bar = '34'; const f = ()->{  bar = 3; z ->{ bar += z; } };",
443                 "let bar = '34'; const f = ()->{  const bar = 3; z ->{ bar += z; } };");
444         // @formatter:on
445         for (final String src : srcs) {
446             final JexlException.Assignment xassign = assertThrows(JexlException.Assignment.class, () -> jexl.createScript(src), src);
447             assertNotNull(xassign, src); // debug breakpoint
448         }
449     }
450 
451     @Test
452     void testConstCaptures() {
453         // @formatter:off
454         final List<String> srcsFalse = Arrays.asList(
455                 "const x = 0;  x = 1;",
456                 "const x = 0; x *= 1;",
457                 "const x = 0; var x = 1;",
458                 "const x = 0; if (true) { var x = 1;}" ,
459                 "const x = 0; if (true) { x = 1;}" ,
460                 "const x = 0; if (true) { var f  = y -> { x = y + 1; x } }" ,
461                 "const x = 0; if (true) { var f  = y -> { z -> { x = y + 1; x } } }" ,
462                 "const x = 0; if (true) { if (false) { y -> { x = y + 1; x } } }" ,
463                 "const x = 0; if (true) { if (false) { y -> { z -> { x = y + 1; x } } }" ,
464                 ""
465         );
466         // @formatter:on
467         checkParse(srcsFalse, false);
468         // @formatter:off
469         final List<String> srcsTrue = Arrays.asList(
470             "const x = 0; if (true) { var f  = x -> x + 1;}" ,
471             "const x = 0; if (true) { var f  = y -> { var x = y + 1; x } }" ,
472             "const x = 0; if (true) { var f  = y -> { const x = y + 1; x } }" ,
473             "const x = 0; if (true) { var f  = y -> { z -> { let x = y + 1; x } } }" ,
474             "");
475         // @formatter:on
476         checkParse(srcsTrue, true);
477     }
478 
479     @Test
480     void testContextualOptions0() {
481         final JexlFeatures f = new JexlFeatures();
482         final JexlEngine jexl = new JexlBuilder().features(f).strict(true).create();
483         final JexlEvalContext ctxt = new JexlEvalContext();
484         final JexlOptions options = ctxt.getEngineOptions();
485         options.setSharedInstance(false);
486         options.setLexical(true);
487         options.setLexicalShade(true);
488         ctxt.set("options", options);
489         final JexlScript script = jexl.createScript("{var x = 42;} options.lexical = false; options.lexicalShade=false; x");
490         assertThrows(JexlException.class, () -> script.execute(ctxt), "setting options.lexical should have no effect during execution");
491     }
492 
493     @Test
494     void testContextualOptions1() {
495         final JexlFeatures f = new JexlFeatures();
496         final JexlEngine jexl = new JexlBuilder().features(f).strict(true).create();
497         final JexlEvalContext ctxt = new TestContext();
498         final JexlOptions options = ctxt.getEngineOptions();
499         options.setSharedInstance(true);
500         options.setLexical(true);
501         options.setLexicalShade(true);
502         ctxt.set("options", options);
503         // @formatter:off
504         final JexlScript runner = jexl.createScript(
505                 "options.lexical = flag; options.lexicalShade = flag;"
506               + "tryCatch(test, catcher, 42);",
507                 "flag", "test", "catcher");
508         // @formatter:on
509         final JexlScript tested = jexl.createScript("(y)->{ {var x = y;} x }");
510         final JexlScript catchFn = jexl.createScript("(xany)-> { xany }");
511         Object result;
512         // run it once, old 3.1 semantics, lexical/shade = false
513         result = runner.execute(ctxt, false, tested, catchFn);
514         // result 42
515         assertEquals(42, result);
516         // run it a second time, new 3.2 semantics, lexical/shade = true
517         result = runner.execute(ctxt, true, tested, catchFn);
518         // result is exception!
519         assertInstanceOf(JexlException.Variable.class, result);
520     }
521 
522     @Test
523     void testForVariable0a() {
524         final JexlFeatures f = new JexlFeatures();
525         f.lexical(true);
526         f.lexicalShade(true);
527         final JexlEngine jexl = createEngine(f);
528         assertThrows(JexlException.class, () -> jexl.createScript("for(let x : 1..3) { let c = 0}; return x", "Should not have been parsed"));
529     }
530 
531     @Test
532     void testForVariable0b() {
533         final JexlFeatures f = new JexlFeatures();
534         f.lexical(true);
535         f.lexicalShade(true);
536         final JexlEngine jexl = createEngine(f);
537         assertThrows(JexlException.class, () -> jexl.createScript("for(var x : 1..3) { var c = 0}; return x", "Should not have been parsed"));
538     }
539 
540     @Test
541     void testForVariable1a() {
542         final JexlFeatures f = new JexlFeatures();
543         f.lexical(true);
544         f.lexicalShade(true);
545         final JexlEngine jexl = createEngine(f);
546         assertThrows(JexlException.class, () -> jexl.createScript("for(var x : 1..3) { var c = 0} for(var x : 1..3) { var c = 0}; return x"),
547                 "Should not have been parsed");
548     }
549 
550     @Test
551     void testForVariable1b() {
552         final JexlFeatures f = new JexlFeatures();
553         f.lexical(true);
554         f.lexicalShade(true);
555         final JexlEngine jexl = createEngine(f);
556         assertThrows(JexlException.class, () -> jexl.createScript("for(let x : 1..3) { let c = 0} for(let x : 1..3) { var c = 0}; return x"),
557                 "Should not have been parsed");
558     }
559 
560     @Test
561     void testInnerAccess0() {
562         final JexlFeatures f = new JexlFeatures();
563         f.lexical(true);
564         final JexlEngine jexl = new JexlBuilder().strict(true).features(f).create();
565         // @formatter:off
566         final JexlScript script = jexl.createScript(
567                 "var x = 32; ("
568                         + "()->{ for(var x : null) { var c = 0; {return x; }} })"
569                         + "();");
570         // @formatter:on
571         assertNull(script.execute(null));
572     }
573 
574     @Test
575     void testInnerAccess1a() {
576         final JexlEngine jexl = new JexlBuilder().strict(true).lexical(true).create();
577         final JexlScript script = jexl.createScript("var x = 32; (()->{ for(var x : null) { var c = 0; {return x; }} })();");
578         assertNotNull(script);
579     }
580 
581     @Test
582     void testInnerAccess1b() {
583         final JexlEngine jexl = new JexlBuilder().strict(true).create();
584         final JexlScript script = jexl.createScript("let x = 32; (()->{ for(let x : null) { let c = 0; { return x; } } } )(); ");
585         assertNotNull(script);
586         final String dbg = JexlTestCase.toString(script);
587         final String src = script.getSourceText();
588         assertTrue(JexlTestCase.equalsIgnoreWhiteSpace(src, dbg));
589     }
590 
591     @Test
592     void testInternalLexicalFeatures() {
593         final String str = "42";
594         final JexlFeatures f = new JexlFeatures();
595         f.lexical(true);
596         f.lexicalShade(true);
597         final JexlEngine jexl = new JexlBuilder().features(f).create();
598         final JexlScript e = jexl.createScript(str);
599         final VarContext vars = new VarContext();
600         final JexlOptions opts = vars.getEngineOptions();
601         // so we can see the effect of features on options
602         opts.setSharedInstance(true);
603         final Script script = (Script) e;
604         final JexlFeatures features = script.getFeatures();
605         assertTrue(features.isLexical());
606         assertTrue(features.isLexicalShade());
607         final Object result = e.execute(vars);
608         assertEquals(42, result);
609         assertTrue(opts.isLexical());
610         assertTrue(opts.isLexicalShade());
611     }
612 
613     @Test
614     void testLet0() {
615         final JexlFeatures f = new JexlFeatures();
616         final JexlEngine jexl = new JexlBuilder().strict(true).create();
617         final JexlScript script = jexl.createScript(
618                 "{ let x = 10; } (b->{ x + b })(32)");
619         final JexlContext jc = new MapContext();
620         jc.set("x", 11);
621         final Object result = script.execute(jc);
622         assertEquals(43, result);
623 
624     }
625 
626     @Test
627     void testLetFail() {
628         final List<String> srcs = Arrays.asList(
629             "let x = 0; var x = 1;",
630             "var x = 0; let x = 1;",
631             "let x = 0; let x = 1;",
632             "var x = 0; const f = (var x) -> { let x = 1; } f()",
633             "var x = 0; const f = (let x) -> { let x = 1; } f()",
634             "var x = 0; const f = (let x) -> { var x = 1; } f()",
635             ""
636         );
637         checkParse(srcs, false);
638     }
639 
640     @Test
641     void testLetSucceed() {
642         final List<String> srcs = Arrays.asList(
643             "var x = 1; var x = 0;",
644             "{ let x = 0; } var x = 1;",
645             "var x = 0; var f = () -> { let x = 1; } f()",
646             //"let x = 0; function f() { let x = 1; }; f()" ,
647             "var x = 0; var f = (let x) -> { x = 1; } f()",
648             "var x = 0; let f = (let x) -> { x = 1; } f()",
649             "var x = 0; const f = (let x) -> { x = 1; } f()",
650             ""
651         );
652         checkParse(srcs, true);
653     }
654 
655     @Test
656     void testLexical0a() {
657         runLexical0(false);
658     }
659 
660     @Test
661     void testLexical0b() {
662         runLexical0(true);
663     }
664 
665     @Test
666     void testLexical1() {
667         final JexlEngine jexl = new JexlBuilder().strict(true).create();
668         final JexlEvalContext ctxt = new JexlEvalContext();
669         final JexlOptions options = ctxt.getEngineOptions();
670         // ensure errors will throw
671         options.setLexical(true);
672         Object result;
673 
674         final JexlScript script = jexl.createScript("var x = 0; for(var y : [1]) { var x = 42; return x; };");
675         JexlException xany = assertThrows(JexlException.Variable.class, () -> script.execute(ctxt));
676         assertNotNull(xany.toString());
677 
678         final JexlScript script1 = jexl.createScript("(x)->{ if (x) { var x = 7 * (x + x); x; } }");
679         xany = assertThrows(JexlException.Variable.class, () -> script.execute(ctxt, 3));
680         assertNotNull(xany.toString());
681 
682         final JexlScript script3 = jexl.createScript("{ var x = 0; } var x = 42; x");
683         result = script3.execute(ctxt, 21);
684         assertEquals(42, result);
685     }
686 
687     @Test
688     void testLexical1a() {
689         runLexical1(false);
690     }
691 
692     @Test
693     void testLexical1b() {
694         runLexical1(true);
695     }
696 
697     @Test
698     void testLexical2a() {
699         runLexical2(true);
700     }
701 
702     @Test
703     void testLexical2b() {
704         runLexical2(false);
705     }
706 
707     @Test
708     void testLexical3() {
709         final String str = "var s = {}; for (var i : [1]) s.add(i); s";
710         final JexlEngine jexl = new JexlBuilder().strict(true).lexical(true).create();
711         JexlScript e = jexl.createScript(str);
712         final JexlContext jc = new MapContext();
713         Object o = e.execute(jc);
714         assertTrue(((Set)o).contains(1));
715 
716         e = jexl.createScript(str);
717         o = e.execute(jc);
718         assertTrue(((Set)o).contains(1));
719     }
720 
721     @Test
722     void testLexical4() {
723         final JexlEngine Jexl = new JexlBuilder().silent(false).strict(true).lexical(true).create();
724         final JxltEngine Jxlt = Jexl.createJxltEngine();
725         final JexlContext ctxt = new MapContext();
726         final String rpt
727                 = "<report>\n"
728                 + "\n$$var y = 1; var x = 2;"
729                 + "\n${x + y}"
730                 + "\n</report>\n";
731         final JxltEngine.Template t = Jxlt.createTemplate("$$", new StringReader(rpt));
732         final StringWriter strw = new StringWriter();
733         t.evaluate(ctxt, strw);
734         final String output = strw.toString();
735         final String ctl = "<report>\n\n3\n</report>\n";
736         assertEquals(ctl, output);
737     }
738 
739     @Test
740     void testLexical5() {
741         final JexlEngine jexl = new JexlBuilder().strict(true).lexical(true).create();
742         final JexlContext ctxt = new DebugContext();
743         Object result;
744         final JexlScript script = jexl.createScript("var x = 42; var y = () -> { {var x = debug(-42); }; return x; }; y()");
745         result = script.execute(ctxt);
746         assertEquals(42, result);
747     }
748 
749     @Test
750     void testLexical6a() {
751         final String str = "i = 0; { var i = 32; }; i";
752         final JexlEngine jexl = new JexlBuilder().strict(true).lexical(true).create();
753         final JexlScript e = jexl.createScript(str);
754         final JexlContext ctxt = new MapContext();
755         final Object o = e.execute(ctxt);
756         assertEquals(0, o);
757     }
758 
759     @Test
760     void testLexical6a1() {
761         final String str = "i = 0; { var i = 32; }; i";
762         final JexlFeatures f = new JexlFeatures();
763         f.lexical(true);
764         final JexlEngine jexl = createEngine(f);
765         final JexlScript e = jexl.createScript(str);
766         final JexlContext ctxt = new MapContext();
767         final Object o = e.execute(ctxt);
768         assertEquals(0, o);
769     }
770 
771     @Test
772     void testLexical6b() {
773         final String str = "i = 0; { var i = 32; }; i";
774         final JexlEngine jexl = new JexlBuilder().strict(true).lexical(true).lexicalShade(true).create();
775         final JexlScript e = jexl.createScript(str);
776         final JexlContext ctxt = new MapContext();
777         final JexlException  xany = assertThrows(JexlException.class, () -> e.execute(ctxt), "i should be shaded");
778         assertNotNull(xany.toString());
779     }
780 
781     @Test
782     void testLexical6c() {
783         final String str = "i = 0; for (var i : [42]) i; i";
784         final JexlEngine jexl = new JexlBuilder().strict(true).lexical(true).lexicalShade(false).create();
785         final JexlScript e = jexl.createScript(str);
786         final JexlContext ctxt = new MapContext();
787         final Object o = e.execute(ctxt);
788         assertEquals(0, o);
789     }
790 
791     @Test
792     void testLexical6d() {
793         final String str = "i = 0; for (var i : [42]) i; i";
794         final JexlEngine jexl = new JexlBuilder().strict(true).lexical(true).lexicalShade(true).create();
795         final JexlScript e = jexl.createScript(str);
796         final JexlContext ctxt = new MapContext();
797         final JexlException  xany = assertThrows(JexlException.class, () -> e.execute(ctxt), "i should be shaded");
798         assertNotNull(xany.toString());
799     }
800 
801     @Test void testManyConst() {
802         final String text = "const x = 1, y = 41; x + y";
803         final JexlEngine jexl = new JexlBuilder().safe(true).create();
804         final JexlScript script = jexl.createScript(text);
805         final Object result = script.execute(null);
806         assertEquals(42, result);
807         final String s0 = script.getParsedText();
808         final String s1 = script.getSourceText();
809         assertNotEquals(s0, s1);
810     }
811 
812     @Test void testManyLet() {
813         final String text = "let x = 1, y = 41, z; x + y";
814         final JexlEngine jexl = new JexlBuilder().safe(true).create();
815         final JexlScript script = jexl.createScript(text);
816         final Object result = script.execute(null);
817         assertEquals(42, result);
818         final String s0 = script.getParsedText();
819         final String s1 = script.getSourceText();
820         assertNotEquals(s0, s1);
821     }
822 
823     @Test
824     void testNamed() {
825         final JexlFeatures f = new JexlFeatures();
826         f.lexical(true);
827         final JexlEngine jexl = new JexlBuilder().strict(true).features(f).create();
828         final JexlScript script = jexl.createScript("var i = (x, y, z)->{return x + y + z}; i(22,18,2)");
829         final JexlContext jc = new MapContext();
830         final Object result = script.execute(jc);
831         assertEquals(42, result);
832     }
833 
834     @Test
835     void testOptionsPragma() {
836         try {
837             JexlOptions.setDefaultFlags("+safe", "-lexical", "-lexicalShade");
838             final VarContext vars = new VarContext();
839             final JexlEngine jexl = new JexlBuilder().create();
840             int n42;
841             JexlOptions o;
842 
843             n42 = (Integer) jexl.createScript("#pragma jexl.options none\n-42").execute(vars);
844             assertEquals(-42, n42);
845             o = vars.snatchOptions();
846             assertNotNull(o);
847             assertTrue(o.isStrict());
848             assertTrue(o.isSafe());
849             assertTrue(o.isCancellable());
850             assertFalse(o.isLexical());
851             assertFalse(o.isLexicalShade());
852 
853             n42 = (Integer) jexl.createScript("#pragma jexl.options canonical\n42").execute(vars);
854             assertEquals(42, n42);
855             o = vars.snatchOptions();
856             assertNotNull(o);
857             assertTrue(o.isStrict());
858             assertFalse(o.isSafe());
859             assertTrue(o.isCancellable());
860             assertTrue(o.isLexical());
861             assertTrue(o.isLexicalShade());
862             assertFalse(o.isSharedInstance());
863         } finally {
864             JexlOptions.setDefaultFlags("-safe", "+lexical");
865         }
866     }
867 
868     @Test
869     void testParameter0() {
870         final String str = "function(u) {}";
871         final JexlEngine jexl = new JexlBuilder().create();
872         JexlScript e = jexl.createScript(str);
873         assertEquals(1, e.getParameters().length);
874         e = jexl.createScript(new JexlInfo("TestScript", 1, 1), str);
875         assertEquals(1, e.getParameters().length);
876     }
877 
878     @Test
879     void testParameter1() {
880         final JexlEngine jexl = new JexlBuilder().strict(true).lexical(true).create();
881         final JexlContext jc = new MapContext();
882         final String strs = "var s = function(x) { for (var i : 1..3) {if (i > 2) return x}}; s(42)";
883         final JexlScript s42 = jexl.createScript(strs);
884         final Object result = s42.execute(jc);
885         assertEquals(42, result);
886     }
887 
888     @Test
889     void testPragmaNoop() {
890         // unknow pragma
891         final String str = "#pragma jexl.options 'no effect'\ni = -42; for (var i : [42]) i; i";
892         final JexlEngine jexl = new JexlBuilder().lexical(false).strict(true).create();
893         final JexlScript e = jexl.createScript(str);
894         final JexlContext ctxt = new MapContext();
895         final Object result = e.execute(ctxt);
896         assertEquals(42, result);
897     }
898 
899     @Test
900     void testPragmaOptions() {
901         // same as 6d but using a pragma
902         final String str = "#pragma jexl.options '+strict +lexical +lexicalShade -safe'\n"
903                 + "i = 0; for (var i : [42]) i; i";
904         final JexlEngine jexl = new JexlBuilder().strict(false).create();
905         final JexlScript e = jexl.createScript(str);
906         final JexlContext ctxt = new MapContext();
907         final JexlException  xany = assertThrows(JexlException.class, () -> e.execute(ctxt), "i should be shaded");
908         assertNotNull(xany.toString());
909     }
910 
911     @Test
912     void testScopeFrame() {
913         final LexicalScope scope = new LexicalScope();
914         runTestScope(scope, 0, 128, 2);
915         runTestScope(scope, 33, 55, 1);
916         runTestScope(scope, 15, 99, 3);
917         runTestScope(scope, 3, 123, 5);
918     }
919 
920     @Test
921     void testSingleStatementDeclFail() {
922         final List<String> srcs = Arrays.asList(
923                 "if (true) let x ;",
924                 "if (true) let x = 1;",
925                 "if (true) var x = 1;",
926                 "if (true) { 1 } else let x ;",
927                 "if (true) { 1 } else let x = 1;",
928                 "if (true) { 1 } else var x = 1;",
929                 "while (true) let x ;",
930                 "while (true) let x = 1;",
931                 "while (true) var x = 1;",
932                 "do let x ; while (true)",
933                 "do let x = 1; while (true)",
934                 "do var x = 1; while (true)",
935                 "for (let i:ii) let x ;",
936                 "for (let i:ii) let x = 1;",
937                 "for (let i:ii) var x = 1;",
938                 ""
939         );
940         final JexlFeatures f=  new JexlFeatures();
941         f.lexical(true).lexicalShade(true);
942         checkParse(f, srcs, false);
943     }
944 
945     @Test
946     void testSingleStatementVarSucceed() {
947         final List<String> srcs = Arrays.asList(
948                 "if (true) var x = 1;",
949                 "if (true) { 1 } else var x = 1;",
950                 "while (true) var x = 1;",
951                 "do var x = 1 while (true)",
952                 "for (let i:ii) var x = 1;",
953                 ""
954         );
955         checkParse(srcs, true);
956     }
957 
958     @Test
959     void testUndeclaredVariable() {
960         final JexlFeatures f = new JexlFeatures();
961         f.lexical(true);
962         f.lexicalShade(true);
963         final JexlEngine jexl = createEngine(f);
964         assertThrows(JexlException.class, () -> jexl.createScript("{var x = 0}; return x"), "Should not have been parsed");
965     }
966 
967     @Test
968     void testVarFail() {
969         final List<String> srcs = Arrays.asList(
970                 "var x = 0; var x = 1;",
971                 "var x = 0; let x = 1;",
972                 "let x = 0; var x = 1;",
973                 "var x = 0; const f = (var x) -> { let x = 1; } f()",
974                 "var x = 0; const f = (let x) -> { var x = 1; } f()",
975                 "var x = 0; const f = (var x) -> { var x = 1; } f()",
976                 ""
977         );
978         final JexlFeatures f=  new JexlFeatures();
979         f.lexical(true).lexicalShade(true);
980         checkParse(f, srcs, false);
981     }
982 
983     @Test
984     void testVarLoop0() {
985         final String src0 = "var count = 10;\n"
986                 + "for (var i : 0 .. count-1) {\n"
987                 + "  $out.add(i);\n"
988                 + "}";
989         final String src1 = "var count = [0,1,2,3,4,5,6,7,8,9];\n"
990                 + "for (var i : count) {\n"
991                 + "  $out.add(i);\n"
992                 + "}";
993         final String src2 = "var count = 10;\n" +
994                 "  var outer = 0;\n"
995                 + "for (var i : 0 .. count-1) {\n"
996                 + "  $out.add(i);\n"
997                 + "  outer = i;"
998                 + "}\n"
999                 + "outer == 9";
1000         final JexlFeatures ff0 = runVarLoop(false, src0);
1001         final JexlFeatures ft0= runVarLoop(true, src0);
1002         final JexlFeatures ff1 = runVarLoop(false, src1);
1003         final JexlFeatures ft1= runVarLoop(true, src1);
1004         final JexlFeatures ff2 = runVarLoop(false, src2);
1005         final JexlFeatures ft2= runVarLoop(true, src2);
1006 
1007         // and check some features
1008         assertEquals(ff0, ff1);
1009         assertEquals(ft0, ft1);
1010         assertNotEquals(ff0, ft0);
1011         final String sff0 = ff0.toString();
1012         final String sff1 = ff1.toString();
1013         assertEquals(sff0, sff1);
1014         final String sft1 = ft1.toString();
1015         assertNotEquals(sff0, sft1);
1016     }
1017 }