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