You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by he...@apache.org on 2026/10/15 18:09:46 UTC
(commons-jexl) branch master updated: JEXL-473 : Add feature to restrict allowed pragmas
This is an automated email from the ASF dual-hosted git repository.
henrib pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
The following commit(s) were added to refs/heads/master by this push:
new a1bd8054 JEXL-473 : Add feature to restrict allowed pragmas
a1bd8054 is described below
commit a1bd805466173a1394e9f130d7b1c295aaff6525
Author: Henrib <hb...@gmail.com>
AuthorDate: Thu Oct 15 20:08:01 2026 +0200
JEXL-473 : Add feature to restrict allowed pragmas
Co-Authored-By: Claude Code <no...@anthropic.com>
---
src/changes/changes.xml | 1 +
.../org/apache/commons/jexl3/JexlConfigLoader.java | 56 +++++++
.../org/apache/commons/jexl3/JexlFeatures.java | 142 ++++++++++++++++++
.../apache/commons/jexl3/parser/JexlParser.java | 26 ++++
.../apache/commons/jexl3/JexlConfigLoaderTest.java | 166 +++++++++++++++++++++
5 files changed, 391 insertions(+)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 6d1b8ea3..fb7c3bf7 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -29,6 +29,7 @@
<body>
<release version="3.7.1" date="YYYY-MM-DD" description="This is a feature and maintenance release. Java 8 or later is required.">
<!-- ADD -->
+ <action dev="henrib" due-to="henrib" type="add" issue="JEXL-473">Add feature to restrict allowed pragmas.</action>
<action dev="henrib" due-to="Aurelien Mino" type="add" issue="JEXL-472">Add property access support for Java record component accessors.</action>
<action dev="henrib" due-to="Claude" type="add" issue="JEXL-469">Add JexlFeatures.namespaceInstantiation(boolean) to control whether a namespace bound to a Class or class-name string is reflectively auto-instantiated into a functor; string namespaces now resolve through the permission-aware uberspect.</action>
<action dev="henrib" type="add" issue="JEXL-467">IntelliJ and VSCode editors (TextMate bundle) support for JEXL.</action>
diff --git a/src/main/java/org/apache/commons/jexl3/JexlConfigLoader.java b/src/main/java/org/apache/commons/jexl3/JexlConfigLoader.java
index ac8bcb3d..31d342db 100644
--- a/src/main/java/org/apache/commons/jexl3/JexlConfigLoader.java
+++ b/src/main/java/org/apache/commons/jexl3/JexlConfigLoader.java
@@ -27,9 +27,11 @@ import java.math.MathContext;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Predicate;
import org.apache.commons.jexl3.introspection.JexlPermissions;
import org.apache.commons.jexl3.introspection.JexlUberspect;
@@ -172,6 +174,25 @@ import org.apache.commons.jexl3.introspection.JexlUberspect;
* - com.example.api
* </pre>
*
+ * <h2>{@code pragmas:} section</h2>
+ * <p>A list of allowed pragma key patterns, turned into the predicate passed to
+ * {@link JexlFeatures#pragmaTest(java.util.function.Predicate)}. A pattern ending with {@code *}
+ * matches any key starting with what precedes it; any other pattern must match the key exactly.
+ * Scripts declaring a pragma whose key matches no pattern fail to parse with a
+ * {@link JexlException.Feature}. The section is an allow-list: when present with no items, no pragma is
+ * allowed; when absent, all pragmas are allowed (subject to the {@code pragma}, {@code importPragma} and
+ * {@code namespacePragma} features). The list applies to JEXL's own pragmas too: {@code jexl.options},
+ * {@code jexl.import}, {@code jexl.namespace.*} and {@code jexl.module.*} must be listed to be allowed.
+ * A single pattern may also be written inline
+ * ({@code pragmas: dlm.*}); entries that are not {@code - pattern} items, or flow-style lists, are
+ * rejected with an {@link IllegalArgumentException}.</p>
+ * <pre>
+ * pragmas:
+ * - dlm.* # any pragma key starting with 'dlm.'
+ * - jexl.import # exactly 'jexl.import'
+ * - jexl.namespace.*
+ * </pre>
+ *
* <h2>Complete annotated example</h2>
* <p>Every flag is listed explicitly so the configuration does not depend on any library default
* (which may change between releases). The {@code features:} block below reproduces the pre-3.7
@@ -313,6 +334,9 @@ public final class JexlConfigLoader {
if (val.isEmpty()) {
section = key;
sectionData = new LinkedHashMap<>();
+ } else if ("pragmas".equals(key)) {
+ // a single pattern on the same line
+ features.pragmaTest(parsePragmaPatterns(Collections.singletonList(unquote(val))));
} else {
applyTopLevel(key, val, builder);
}
@@ -394,6 +418,21 @@ public final class JexlConfigLoader {
}
break;
}
+ case "pragmas": {
+ // only list items are expected; anything else is a configuration mistake
+ for (final String key : data.keySet()) {
+ if (!section.equals(key)) {
+ throw new IllegalArgumentException(
+ "pragmas: expects a list of '- pattern' items, found '" + key + "'");
+ }
+ }
+ // an allow-list: a section with no items allows no pragma
+ final Object list = data.get(section);
+ features.pragmaTest(parsePragmaPatterns(list instanceof List
+ ? (List<String>) list
+ : Collections.emptyList()));
+ break;
+ }
default:
break;
}
@@ -534,6 +573,23 @@ public final class JexlConfigLoader {
: JexlUberspect.JEXL_STRATEGY;
}
+ /**
+ * Creates a pragma key predicate from a list of patterns.
+ *
+ * @see JexlFeatures#pragmaPatterns(java.util.Collection)
+ */
+ private static Predicate<String> parsePragmaPatterns(final List<String> patterns) {
+ for (final String pattern : patterns) {
+ final String trimmed = pattern.trim();
+ // flow-style lists ([a, b] or a, b) are not supported
+ if (trimmed.startsWith("[") || trimmed.indexOf(',') >= 0) {
+ throw new IllegalArgumentException(
+ "pragmas: expects one pattern per '- pattern' item, found '" + trimmed + "'");
+ }
+ }
+ return JexlFeatures.pragmaPatterns(patterns);
+ }
+
private static boolean parseBool(final String s) {
return "true".equalsIgnoreCase(s) || "yes".equalsIgnoreCase(s) || "on".equalsIgnoreCase(s);
}
diff --git a/src/main/java/org/apache/commons/jexl3/JexlFeatures.java b/src/main/java/org/apache/commons/jexl3/JexlFeatures.java
index f7802a06..55b5f401 100644
--- a/src/main/java/org/apache/commons/jexl3/JexlFeatures.java
+++ b/src/main/java/org/apache/commons/jexl3/JexlFeatures.java
@@ -70,6 +70,13 @@ public final class JexlFeatures {
/** The false predicate. */
public static final Predicate<String> TEST_STR_FALSE = s -> false;
+ /**
+ * The true predicate.
+ *
+ * @since 3.7.1
+ */
+ public static final Predicate<String> TEST_STR_TRUE = s -> true;
+
/** The feature names (for toString()). */
private static final String[] F_NAMES = {
"register", "reserved variable", "local variable", "assign/modify",
@@ -322,6 +329,9 @@ public final class JexlFeatures {
/** The namespace names. */
private Predicate<String> nameSpaces;
+ /** The allowed pragma keys. */
+ private Predicate<String> pragmas = TEST_STR_TRUE;
+
/**
* Creates default instance, equivalent to the result of calling the preferred alternative
* {@link JexlFeatures#createDefault()}
@@ -337,6 +347,7 @@ public final class JexlFeatures {
*/
public JexlFeatures(final JexlFeatures features) {
this(features.flags, features.reservedNames, features.nameSpaces);
+ this.pragmas = features.pragmas;
}
/**
@@ -470,6 +481,9 @@ public final class JexlFeatures {
if (this.nameSpaces != other.nameSpaces) {
return false;
}
+ if (!this.pragmas.equals(other.pragmas)) {
+ return false;
+ }
if (!Objects.equals(this.reservedNames, other.reservedNames)) {
return false;
}
@@ -525,6 +539,7 @@ public final class JexlFeatures {
int hash = 3;
hash = 53 * hash + Long.hashCode(this.flags);
hash = 53 * hash + (this.reservedNames != null ? this.reservedNames.hashCode() : 0);
+ hash = 53 * hash + this.pragmas.hashCode();
return hash;
}
@@ -779,6 +794,133 @@ public final class JexlFeatures {
return this;
}
+ /**
+ * Gets the allowed pragma keys test.
+ *
+ * @return The allowed pragma keys test, never null.
+ * @since 3.7.1
+ */
+ public Predicate<String> pragmaTest() {
+ return pragmas;
+ }
+
+ /**
+ * Sets a test to determine which pragma keys are allowed.
+ * <p>
+ * When set, parsing a script/expression declaring a pragma (#pragma key value) whose key does not satisfy
+ * the predicate will throw a parsing exception. This applies to all pragmas, including
+ * <code>jexl.options</code>, <code>jexl.import</code>, <code>jexl.namespace.*</code> and
+ * <code>jexl.module.*</code>.
+ * </p>
+ * <p>
+ * An engine built with these features applies the test to every script, expression and template it parses.
+ * As with every other feature, scripts created with explicit (non-null) features through
+ * {@link JexlEngine#createScript(JexlFeatures, JexlInfo, String, String...)} use those features' pragma
+ * test, not the engine's.
+ * </p>
+ *
+ * @param keys The pragma key predicate; must be stateless or thread-safe; null allows all pragma keys
+ * @return this features instance
+ * @since 3.7.1
+ */
+ public JexlFeatures pragmaTest(final Predicate<String> keys) {
+ pragmas = keys == null ? TEST_STR_TRUE : keys;
+ return this;
+ }
+
+ /**
+ * Creates a pragma key test from a collection of patterns, meant for {@link #pragmaTest(Predicate)}.
+ * <p>
+ * A pattern ending with <code>*</code> matches any key starting with what precedes it, any other
+ * pattern matches the key exactly. For instance, <code>dlm.*</code> matches <code>dlm.foo</code> and
+ * <code>dlm.foo.bar</code>; <code>*</code> matches any key. Patterns are trimmed, null or empty
+ * patterns are ignored.
+ * </p>
+ * <p>Tests created from the same patterns are equal, so are the features using them.</p>
+ *
+ * @param patterns The allowed pragma key patterns; null or empty allows no pragma key
+ * @return The pragma key test
+ * @since 3.7.1
+ */
+ public static Predicate<String> pragmaPatterns(final Collection<String> patterns) {
+ final Set<String> exact = new TreeSet<>();
+ final Set<String> prefixes = new TreeSet<>();
+ if (patterns != null) {
+ for (final String pattern : patterns) {
+ final String trimmed = pattern == null ? "" : pattern.trim();
+ if ("*".equals(trimmed)) {
+ return TEST_STR_TRUE;
+ }
+ if (trimmed.endsWith("*")) {
+ prefixes.add(trimmed.substring(0, trimmed.length() - 1));
+ } else if (!trimmed.isEmpty()) {
+ exact.add(trimmed);
+ }
+ }
+ }
+ return exact.isEmpty() && prefixes.isEmpty() ? TEST_STR_FALSE : new PragmaPatterns(exact, prefixes);
+ }
+
+ /**
+ * A pragma key test made of exact keys and key prefixes.
+ */
+ private static final class PragmaPatterns implements Predicate<String> {
+ /** The exactly matched keys. */
+ private final Set<String> exact;
+ /** The matched key prefixes. */
+ private final Set<String> prefixes;
+
+ PragmaPatterns(final Set<String> exact, final Set<String> prefixes) {
+ this.exact = exact;
+ this.prefixes = prefixes;
+ }
+
+ @Override
+ public boolean test(final String key) {
+ if (key == null) {
+ return false;
+ }
+ if (exact.contains(key)) {
+ return true;
+ }
+ for (final String prefix : prefixes) {
+ if (key.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof PragmaPatterns)) {
+ return false;
+ }
+ final PragmaPatterns other = (PragmaPatterns) obj;
+ return exact.equals(other.exact) && prefixes.equals(other.prefixes);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * exact.hashCode() + prefixes.hashCode(); //CSOFF: MagicNumber
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder strb = new StringBuilder();
+ for (final String key : exact) {
+ strb.append(strb.length() > 0 ? ", " : "").append(key);
+ }
+ for (final String prefix : prefixes) {
+ strb.append(strb.length() > 0 ? ", " : "").append(prefix).append('*');
+ }
+ return strb.toString();
+ }
+ }
+
/**
* Sets whether register are enabled.
* <p>
diff --git a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java
index 8996bd53..c4511574 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java
@@ -105,6 +105,26 @@ public abstract class JexlParser extends StringParser implements JexlScriptParse
*/
public static final String PRAGMA_IMPORT = "jexl.import";
+ /**
+ * Thrown when a pragma key is not allowed by the features pragma test.
+ */
+ private static final class PragmaKeyException extends JexlException.Feature {
+ private static final long serialVersionUID = 20261015L;
+
+ /** The refused pragma key. */
+ private final String key;
+
+ PragmaKeyException(final JexlInfo info, final String key, final String expr) {
+ super(info, JexlFeatures.PRAGMA, expr);
+ this.key = key;
+ }
+
+ @Override
+ protected String detailedMessage() {
+ return parserError("pragma '" + key + "' not allowed,", getDetail());
+ }
+ }
+
/**
* The set of assignment operators as classes.
*/
@@ -633,6 +653,12 @@ public abstract class JexlParser extends StringParser implements JexlScriptParse
if (PRAGMA_IMPORT.equals(key) && !features.supportsImportPragma()) {
throwFeatureException(JexlFeatures.IMPORT_PRAGMA, getToken(0));
}
+ if (!features.pragmaTest().test(key)) {
+ final Token token = getToken(0);
+ final JexlInfo xinfo = token != null ? info.at(token.beginLine, token.beginColumn) : info;
+ final String line = xinfo != null ? readSourceLine(source, xinfo.getLine()) : null;
+ throw new PragmaKeyException(xinfo, key, line).clean();
+ }
if (pragmas == null) {
pragmas = new TreeMap<>();
}
diff --git a/src/test/java/org/apache/commons/jexl3/JexlConfigLoaderTest.java b/src/test/java/org/apache/commons/jexl3/JexlConfigLoaderTest.java
index db6c01c4..0c4e1837 100644
--- a/src/test/java/org/apache/commons/jexl3/JexlConfigLoaderTest.java
+++ b/src/test/java/org/apache/commons/jexl3/JexlConfigLoaderTest.java
@@ -20,6 +20,9 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.math.MathContext;
import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.function.Predicate;
import org.apache.commons.jexl3.introspection.JexlUberspect;
import org.junit.jupiter.api.Assertions;
@@ -340,4 +343,167 @@ public class JexlConfigLoaderTest {
Assertions.assertNull(JexlConfigLoader.load(in).strategy());
}
}
+
+ @Test
+ void testPragmasFromYaml() throws Exception {
+ final String config = "pragmas:\n - dlm.* # any dlm pragma\n - jexl.import\n";
+ try (InputStream in = yaml(config)) {
+ final JexlBuilder builder = JexlConfigLoader.load(in);
+ final Predicate<String> test = builder.features().pragmaTest();
+ Assertions.assertNotSame(JexlFeatures.TEST_STR_TRUE, test);
+ Assertions.assertTrue(test.test("dlm.foo"));
+ Assertions.assertTrue(test.test("dlm.foo.bar"));
+ Assertions.assertTrue(test.test("jexl.import"));
+ Assertions.assertFalse(test.test("dlm"));
+ Assertions.assertFalse(test.test("jexl.importer"));
+ Assertions.assertFalse(test.test("jexl.options"));
+ final JexlEngine engine = builder.create();
+ final JexlScript script = engine.createScript("#pragma dlm.foo 42\n#pragma jexl.import 'java.util'\n1");
+ Assertions.assertEquals(42, script.getPragmas().get("dlm.foo"));
+ final JexlException.Feature xfeature = Assertions.assertThrows(JexlException.Feature.class,
+ () -> engine.createScript("#pragma dlm.foo 42\n#pragma jexl.options '+strict'\n1"));
+ // the parser reports the offending pragma line and key
+ Assertions.assertEquals(2, xfeature.getInfo().getLine());
+ Assertions.assertTrue(xfeature.getMessage().contains("pragma 'jexl.options' not allowed"),
+ xfeature.getMessage());
+ Assertions.assertThrows(JexlException.Feature.class,
+ () -> engine.createExpression("#pragma other.foo 42\n1"));
+ }
+ }
+
+ @Test
+ void testInlinePragmaFromYaml() throws Exception {
+ try (InputStream in = yaml("pragmas: dlm.* # a single pattern\nstrict: true\n")) {
+ final Predicate<String> test = JexlConfigLoader.load(in).features().pragmaTest();
+ Assertions.assertTrue(test.test("dlm.foo"));
+ Assertions.assertFalse(test.test("jexl.options"));
+ }
+ try (InputStream in = yaml("pragmas: 'jexl.import'\n")) {
+ final Predicate<String> test = JexlConfigLoader.load(in).features().pragmaTest();
+ Assertions.assertTrue(test.test("jexl.import"));
+ Assertions.assertFalse(test.test("dlm.foo"));
+ }
+ }
+
+ @Test
+ void testInvalidPragmasFromYaml() throws Exception {
+ final String[] configs = {
+ "pragmas:\n dlm: true\n",
+ "pragmas:\n - dlm.*\n rules:\n - jexl.import\n",
+ "pragmas: [dlm.*, jexl.import]\n",
+ "pragmas: dlm.*, jexl.import\n",
+ "pragmas:\n - dlm.*, jexl.import\n"
+ };
+ for (final String config : configs) {
+ try (InputStream in = yaml(config)) {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> JexlConfigLoader.load(in), config);
+ }
+ }
+ }
+
+ @Test
+ void testEmptyPragmaItemFromYaml() throws Exception {
+ // an empty item allows nothing
+ try (InputStream in = yaml("pragmas:\n - \n")) {
+ Assertions.assertSame(JexlFeatures.TEST_STR_FALSE, JexlConfigLoader.load(in).features().pragmaTest());
+ }
+ // and is ignored among other items
+ try (InputStream in = yaml("pragmas:\n - dlm.*\n - \n")) {
+ final Predicate<String> test = JexlConfigLoader.load(in).features().pragmaTest();
+ Assertions.assertTrue(test.test("dlm.foo"));
+ Assertions.assertFalse(test.test(""));
+ }
+ }
+
+ @Test
+ void testPragmasAnywhereFromYaml() throws Exception {
+ final String config = "features:\n pragmaAnywhere: true\npragmas:\n - dlm.*\n";
+ try (InputStream in = yaml(config)) {
+ final JexlEngine engine = JexlConfigLoader.load(in).create();
+ final JexlScript script = engine.createScript("var x = 1;\n#pragma dlm.foo 42\nx");
+ Assertions.assertEquals(42, script.getPragmas().get("dlm.foo"));
+ final JexlException.Feature xfeature = Assertions.assertThrows(JexlException.Feature.class,
+ () -> engine.createScript("var x = 1;\n#pragma dlm.foo 42\nx;\n#pragma other.foo 43\nx"));
+ Assertions.assertEquals(4, xfeature.getInfo().getLine());
+ Assertions.assertTrue(xfeature.getMessage().contains("pragma 'other.foo' not allowed"),
+ xfeature.getMessage());
+ }
+ }
+
+ @Test
+ void testStarPragmaFromYamlAllowsAll() throws Exception {
+ try (InputStream in = yaml("pragmas:\n - '*'\n")) {
+ Assertions.assertSame(JexlFeatures.TEST_STR_TRUE, JexlConfigLoader.load(in).features().pragmaTest());
+ }
+ }
+
+ @Test
+ void testEmptyPragmasFromYamlAllowsNone() throws Exception {
+ try (InputStream in = yaml("pragmas:\nstrict: true\n")) {
+ final JexlEngine engine = JexlConfigLoader.load(in).create();
+ Assertions.assertThrows(JexlException.Feature.class,
+ () -> engine.createScript("#pragma dlm.foo 42\n1"));
+ Assertions.assertEquals(1, engine.createScript("1").execute(null));
+ }
+ }
+
+ @Test
+ void testNoPragmasFromYamlAllowsAll() throws Exception {
+ try (InputStream in = yaml("strict: true\n")) {
+ final JexlBuilder builder = JexlConfigLoader.load(in);
+ Assertions.assertSame(JexlFeatures.TEST_STR_TRUE, builder.features().pragmaTest());
+ final JexlScript script = builder.create().createScript("#pragma any.thing 42\n1");
+ Assertions.assertEquals(42, script.getPragmas().get("any.thing"));
+ }
+ }
+
+ @Test
+ void testPragmasFromYamlEquality() throws Exception {
+ final String config = "pragmas:\n - dlm.*\n - jexl.import\n";
+ final JexlFeatures f0;
+ final JexlFeatures f1;
+ try (InputStream in = yaml(config)) {
+ f0 = JexlConfigLoader.load(in).features();
+ }
+ try (InputStream in = yaml(config)) {
+ f1 = JexlConfigLoader.load(in).features();
+ }
+ // same patterns, same features
+ Assertions.assertNotSame(f0.pragmaTest(), f1.pragmaTest());
+ Assertions.assertEquals(f0, f1);
+ Assertions.assertEquals(f0.hashCode(), f1.hashCode());
+ // pattern order and spacing do not matter, the patterns do
+ final JexlFeatures f2 = new JexlFeatures(f0)
+ .pragmaTest(JexlFeatures.pragmaPatterns(Arrays.asList(" jexl.import", "dlm.* ")));
+ Assertions.assertEquals(f0, f2);
+ final JexlFeatures f3 = new JexlFeatures(f0)
+ .pragmaTest(JexlFeatures.pragmaPatterns(Collections.singletonList("dlm.*")));
+ Assertions.assertNotEquals(f0, f3);
+ }
+
+ @Test
+ void testPragmaPatterns() {
+ Assertions.assertSame(JexlFeatures.TEST_STR_FALSE, JexlFeatures.pragmaPatterns(null));
+ Assertions.assertSame(JexlFeatures.TEST_STR_FALSE, JexlFeatures.pragmaPatterns(Collections.emptyList()));
+ Assertions.assertSame(JexlFeatures.TEST_STR_FALSE, JexlFeatures.pragmaPatterns(Arrays.asList("", " ", null)));
+ Assertions.assertSame(JexlFeatures.TEST_STR_TRUE, JexlFeatures.pragmaPatterns(Arrays.asList("dlm.*", "*")));
+ final Predicate<String> test = JexlFeatures.pragmaPatterns(Arrays.asList("dlm.*", "jexl.import"));
+ Assertions.assertTrue(test.test("dlm.foo"));
+ Assertions.assertTrue(test.test("jexl.import"));
+ Assertions.assertFalse(test.test("jexl.importer"));
+ Assertions.assertFalse(test.test(null));
+ Assertions.assertEquals("jexl.import, dlm.*", test.toString());
+ }
+
+ @Test
+ void testFeaturesPragmaTest() {
+ final JexlFeatures features = new JexlFeatures().pragmaTest(k -> k.startsWith("dlm."));
+ // copies keep the pragma test
+ Assertions.assertEquals(features, new JexlFeatures(features));
+ final JexlEngine engine = new JexlBuilder().features(features).create();
+ Assertions.assertNotNull(engine.createScript("#pragma dlm.good 1\n1"));
+ Assertions.assertThrows(JexlException.Feature.class, () -> engine.createScript("#pragma other.good 1\n1"));
+ // explicit features use their own pragma test
+ Assertions.assertNotNull(engine.createScript(new JexlFeatures(), null, "#pragma other.good 1\n1"));
+ }
}