From bd16f7355d8fcf5f1aeed5129279778f1bb4a446 Mon Sep 17 00:00:00 2001 From: jasmith-hs Date: Fri, 10 Apr 2026 09:48:16 -0400 Subject: [PATCH 1/6] Make JinjavaConfig a class again --- .../com/hubspot/jinjava/JinjavaConfig.java | 89 ++++++++++--------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java index ccfaf7ca5..42172fd4d 100644 --- a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java +++ b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.el.JinjavaInterpreterResolver; import com.hubspot.jinjava.el.JinjavaObjectUnwrapper; import com.hubspot.jinjava.el.JinjavaProcessors; @@ -53,158 +54,164 @@ @Value.Immutable(singleton = true) @JinjavaImmutableStyle.WithStyle -public interface JinjavaConfig { +public class JinjavaConfig { + + public JinjavaConfig() {} + @Value.Default - default Charset getCharset() { + public Charset getCharset() { return StandardCharsets.UTF_8; } @Value.Default - default Locale getLocale() { + public Locale getLocale() { return Locale.ENGLISH; } @Value.Default - default ZoneId getTimeZone() { + public ZoneId getTimeZone() { return ZoneOffset.UTC; } @Value.Default - default int getMaxRenderDepth() { + public int getMaxRenderDepth() { return 10; } @Value.Default - default long getMaxOutputSize() { + public long getMaxOutputSize() { return 0; } @Value.Default - default boolean isTrimBlocks() { + public boolean isTrimBlocks() { return false; } @Value.Default - default boolean isLstripBlocks() { + public boolean isLstripBlocks() { return false; } @Value.Default - default boolean isEnableRecursiveMacroCalls() { + public boolean isEnableRecursiveMacroCalls() { return false; } @Value.Default - default int getMaxMacroRecursionDepth() { + public int getMaxMacroRecursionDepth() { return 0; } - Map> getDisabled(); + @Value.Default + public Map> getDisabled() { + return ImmutableMap.of(); + } @Value.Default - default boolean isFailOnUnknownTokens() { + public boolean isFailOnUnknownTokens() { return false; } @Value.Default - default boolean isNestedInterpretationEnabled() { + public boolean isNestedInterpretationEnabled() { return false; // Default changed to false in 3.0 } @Value.Default - default RandomNumberGeneratorStrategy getRandomNumberGeneratorStrategy() { + public RandomNumberGeneratorStrategy getRandomNumberGeneratorStrategy() { return RandomNumberGeneratorStrategy.THREAD_LOCAL; } @Value.Default - default boolean isValidationMode() { + public boolean isValidationMode() { return false; } @Value.Default - default long getMaxStringLength() { + public long getMaxStringLength() { return getMaxOutputSize(); } @Value.Default - default int getMaxListSize() { + public int getMaxListSize() { return Integer.MAX_VALUE; } @Value.Default - default int getMaxMapSize() { + public int getMaxMapSize() { return Integer.MAX_VALUE; } @Value.Default - default int getRangeLimit() { + public int getRangeLimit() { return DEFAULT_RANGE_LIMIT; } @Value.Default - default int getMaxNumDeferredTokens() { + public int getMaxNumDeferredTokens() { return 1000; } @Value.Default - default InterpreterFactory getInterpreterFactory() { + public InterpreterFactory getInterpreterFactory() { return new JinjavaInterpreterFactory(); } @Value.Default - default DateTimeProvider getDateTimeProvider() { + public DateTimeProvider getDateTimeProvider() { return new CurrentDateTimeProvider(); } @Value.Default - default TokenScannerSymbols getTokenScannerSymbols() { + public TokenScannerSymbols getTokenScannerSymbols() { return new DefaultTokenScannerSymbols(); } @Value.Default - default AllowlistMethodValidator getMethodValidator() { + public AllowlistMethodValidator getMethodValidator() { return AllowlistMethodValidator.DEFAULT; } @Value.Default - default AllowlistReturnTypeValidator getReturnTypeValidator() { + public AllowlistReturnTypeValidator getReturnTypeValidator() { return AllowlistReturnTypeValidator.DEFAULT; } @Value.Default - default ELResolver getElResolver() { + public ELResolver getElResolver() { return isDefaultReadOnlyResolver() ? JinjavaInterpreterResolver.DEFAULT_RESOLVER_READ_ONLY : JinjavaInterpreterResolver.DEFAULT_RESOLVER_READ_WRITE; } @Value.Default - default boolean isDefaultReadOnlyResolver() { + public boolean isDefaultReadOnlyResolver() { return true; } @Value.Default - default ExecutionMode getExecutionMode() { + public ExecutionMode getExecutionMode() { return DefaultExecutionMode.instance(); } @Value.Default - default LegacyOverrides getLegacyOverrides() { + public LegacyOverrides getLegacyOverrides() { return LegacyOverrides.THREE_POINT_0; } @Value.Default - default boolean getEnablePreciseDivideFilter() { + public boolean getEnablePreciseDivideFilter() { return false; } @Value.Default - default boolean isEnableFilterChainOptimization() { + public boolean isEnableFilterChainOptimization() { return false; } @Value.Default - default ObjectMapper getObjectMapper() { + public ObjectMapper getObjectMapper() { ObjectMapper objectMapper = new ObjectMapper().registerModule(new Jdk8Module()); if (getLegacyOverrides().isUseSnakeCasePropertyNaming()) { objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); @@ -213,42 +220,42 @@ default ObjectMapper getObjectMapper() { } @Value.Default - default ObjectUnwrapper getObjectUnwrapper() { + public ObjectUnwrapper getObjectUnwrapper() { return new JinjavaObjectUnwrapper(); } @Value.Derived - default Features getFeatures() { + public Features getFeatures() { return new Features(getFeatureConfig()); } @Value.Default - default FeatureConfig getFeatureConfig() { + public FeatureConfig getFeatureConfig() { return FeatureConfig.newBuilder().build(); } @Value.Default - default JinjavaProcessors getProcessors() { + public JinjavaProcessors getProcessors() { return JinjavaProcessors.newBuilder().build(); } @Deprecated - default BiConsumer getNodePreProcessor() { + public BiConsumer getNodePreProcessor() { return getProcessors().getNodePreProcessor(); } @Deprecated - default boolean isIterateOverMapKeys() { + public boolean isIterateOverMapKeys() { return getLegacyOverrides().isIterateOverMapKeys(); } - class Builder extends ImmutableJinjavaConfig.Builder {} + public static class Builder extends ImmutableJinjavaConfig.Builder {} - static Builder builder() { + public static Builder builder() { return new Builder(); } - static Builder newBuilder() { + public static Builder newBuilder() { return builder(); } } From cca1a5472faa7018b23aca9ba3677b85982a3698 Mon Sep 17 00:00:00 2001 From: jasmith-hs Date: Fri, 10 Apr 2026 11:20:56 -0400 Subject: [PATCH 2/6] New alias for build --- .../java/com/hubspot/jinjava/JinjavaConfig.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java index 42172fd4d..0ef3c53d6 100644 --- a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java +++ b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java @@ -54,6 +54,12 @@ @Value.Immutable(singleton = true) @JinjavaImmutableStyle.WithStyle +@Value.Style( + init = "with*", + get = { "is*", "get*" }, // Detect 'get' and 'is' prefixes in accessor methods + build = "buildImpl", // This is an alias for keeping binary compatibility on the "build" method. + visibility = Value.Style.ImplementationVisibility.PACKAGE +) public class JinjavaConfig { public JinjavaConfig() {} @@ -249,7 +255,12 @@ public boolean isIterateOverMapKeys() { return getLegacyOverrides().isIterateOverMapKeys(); } - public static class Builder extends ImmutableJinjavaConfig.Builder {} + public static class Builder extends ImmutableJinjavaConfig.Builder { + + public JinjavaConfig build() { + return super.buildImpl(); + } + } public static Builder builder() { return new Builder(); From bcdb8fd6a6749e21c66fcca835da4d5aa9058c96 Mon Sep 17 00:00:00 2001 From: jasmith-hs Date: Wed, 15 Apr 2026 16:28:18 -0400 Subject: [PATCH 3/6] Keep LegacyOverrides as a class rather than immutable --- .../com/hubspot/jinjava/LegacyOverrides.java | 169 +++++++++++++----- .../jinjava/el/ExtendedSyntaxBuilderTest.java | 7 +- .../interpret/JinjavaInterpreterTest.java | 5 +- .../objects/collections/PyMapTest.java | 7 +- .../hubspot/jinjava/tree/TreeParserTest.java | 15 +- .../jinjava/util/ObjectIteratorTest.java | 4 +- 6 files changed, 160 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/LegacyOverrides.java b/src/main/java/com/hubspot/jinjava/LegacyOverrides.java index bd3732455..5192aa866 100644 --- a/src/main/java/com/hubspot/jinjava/LegacyOverrides.java +++ b/src/main/java/com/hubspot/jinjava/LegacyOverrides.java @@ -1,17 +1,14 @@ package com.hubspot.jinjava; -import org.immutables.value.Value; - /** * This class allows Jinjava to be configured to override legacy behaviour. * LegacyOverrides.NONE signifies that none of the legacy functionality will be overridden. * LegacyOverrides.ALL signifies that all new functionality will be used; avoid legacy "bugs". */ -@Value.Immutable(singleton = true) -@JinjavaImmutableStyle.WithStyle -public interface LegacyOverrides extends WithLegacyOverrides { - LegacyOverrides NONE = new Builder().build(); - LegacyOverrides THREE_POINT_0 = new Builder() +public class LegacyOverrides { + + public static final LegacyOverrides NONE = new LegacyOverrides.Builder().build(); + public static final LegacyOverrides THREE_POINT_0 = new Builder() .withEvaluateMapKeys(true) .withIterateOverMapKeys(true) .withUsePyishObjectMapper(true) @@ -22,7 +19,7 @@ public interface LegacyOverrides extends WithLegacyOverrides { .withUseTrimmingForNotesAndExpressions(true) .withKeepNullableLoopValues(true) .build(); - LegacyOverrides ALL = new Builder() + public static final LegacyOverrides ALL = new LegacyOverrides.Builder() .withEvaluateMapKeys(true) .withIterateOverMapKeys(true) .withUsePyishObjectMapper(true) @@ -33,59 +30,151 @@ public interface LegacyOverrides extends WithLegacyOverrides { .withUseTrimmingForNotesAndExpressions(true) .withKeepNullableLoopValues(true) .build(); + private final boolean evaluateMapKeys; + private final boolean iterateOverMapKeys; + private final boolean usePyishObjectMapper; + private final boolean useSnakeCasePropertyNaming; + private final boolean useNaturalOperatorPrecedence; + private final boolean parseWhitespaceControlStrictly; + private final boolean allowAdjacentTextNodes; + private final boolean useTrimmingForNotesAndExpressions; + private final boolean keepNullableLoopValues; - @Value.Default - default boolean isEvaluateMapKeys() { - return false; + private LegacyOverrides(Builder builder) { + evaluateMapKeys = builder.evaluateMapKeys; + iterateOverMapKeys = builder.iterateOverMapKeys; + usePyishObjectMapper = builder.usePyishObjectMapper; + useSnakeCasePropertyNaming = builder.useSnakeCasePropertyNaming; + useNaturalOperatorPrecedence = builder.useNaturalOperatorPrecedence; + parseWhitespaceControlStrictly = builder.parseWhitespaceControlStrictly; + allowAdjacentTextNodes = builder.allowAdjacentTextNodes; + useTrimmingForNotesAndExpressions = builder.useTrimmingForNotesAndExpressions; + keepNullableLoopValues = builder.keepNullableLoopValues; } - @Value.Default - default boolean isIterateOverMapKeys() { - return false; + public static Builder newBuilder() { + return new Builder(); } - @Value.Default - default boolean isUsePyishObjectMapper() { - return false; + public boolean isEvaluateMapKeys() { + return evaluateMapKeys; } - @Value.Default - default boolean isUseSnakeCasePropertyNaming() { - return false; + public boolean isIterateOverMapKeys() { + return iterateOverMapKeys; } - @Value.Default - default boolean isUseNaturalOperatorPrecedence() { - return false; + public boolean isUsePyishObjectMapper() { + return usePyishObjectMapper; } - @Value.Default - default boolean isParseWhitespaceControlStrictly() { - return false; + public boolean isUseSnakeCasePropertyNaming() { + return useSnakeCasePropertyNaming; } - @Value.Default - default boolean isAllowAdjacentTextNodes() { - return false; + public boolean isUseNaturalOperatorPrecedence() { + return useNaturalOperatorPrecedence; } - @Value.Default - default boolean isUseTrimmingForNotesAndExpressions() { - return false; + public boolean isParseWhitespaceControlStrictly() { + return parseWhitespaceControlStrictly; } - @Value.Default - default boolean isKeepNullableLoopValues() { - return false; + public boolean isAllowAdjacentTextNodes() { + return allowAdjacentTextNodes; } - class Builder extends ImmutableLegacyOverrides.Builder {} + public boolean isUseTrimmingForNotesAndExpressions() { + return useTrimmingForNotesAndExpressions; + } - static Builder newBuilder() { - return builder(); + public boolean isKeepNullableLoopValues() { + return keepNullableLoopValues; } - static Builder builder() { - return new Builder(); + public static class Builder { + + private boolean evaluateMapKeys = false; + private boolean iterateOverMapKeys = false; + private boolean usePyishObjectMapper = false; + private boolean useSnakeCasePropertyNaming = false; + private boolean useNaturalOperatorPrecedence = false; + private boolean parseWhitespaceControlStrictly = false; + private boolean allowAdjacentTextNodes = false; + private boolean useTrimmingForNotesAndExpressions = false; + private boolean keepNullableLoopValues = false; + + private Builder() {} + + public LegacyOverrides build() { + return new LegacyOverrides(this); + } + + public static Builder from(LegacyOverrides legacyOverrides) { + return new Builder() + .withEvaluateMapKeys(legacyOverrides.evaluateMapKeys) + .withIterateOverMapKeys(legacyOverrides.iterateOverMapKeys) + .withUsePyishObjectMapper(legacyOverrides.usePyishObjectMapper) + .withUseSnakeCasePropertyNaming(legacyOverrides.useSnakeCasePropertyNaming) + .withUseNaturalOperatorPrecedence(legacyOverrides.useNaturalOperatorPrecedence) + .withParseWhitespaceControlStrictly( + legacyOverrides.parseWhitespaceControlStrictly + ) + .withAllowAdjacentTextNodes(legacyOverrides.allowAdjacentTextNodes) + .withUseTrimmingForNotesAndExpressions( + legacyOverrides.useTrimmingForNotesAndExpressions + ); + } + + public Builder withEvaluateMapKeys(boolean evaluateMapKeys) { + this.evaluateMapKeys = evaluateMapKeys; + return this; + } + + public Builder withIterateOverMapKeys(boolean iterateOverMapKeys) { + this.iterateOverMapKeys = iterateOverMapKeys; + return this; + } + + public Builder withUsePyishObjectMapper(boolean usePyishObjectMapper) { + this.usePyishObjectMapper = usePyishObjectMapper; + return this; + } + + public Builder withUseSnakeCasePropertyNaming(boolean useSnakeCasePropertyNaming) { + this.useSnakeCasePropertyNaming = useSnakeCasePropertyNaming; + return this; + } + + public Builder withUseNaturalOperatorPrecedence( + boolean useNaturalOperatorPrecedence + ) { + this.useNaturalOperatorPrecedence = useNaturalOperatorPrecedence; + return this; + } + + public Builder withParseWhitespaceControlStrictly( + boolean parseWhitespaceControlStrictly + ) { + this.parseWhitespaceControlStrictly = parseWhitespaceControlStrictly; + return this; + } + + public Builder withAllowAdjacentTextNodes(boolean allowAdjacentTextNodes) { + this.allowAdjacentTextNodes = allowAdjacentTextNodes; + return this; + } + + public Builder withUseTrimmingForNotesAndExpressions( + boolean useTrimmingForNotesAndExpressions + ) { + this.useTrimmingForNotesAndExpressions = useTrimmingForNotesAndExpressions; + return this; + } + + public Builder withKeepNullableLoopValues(boolean keepNullableLoopValues) { + this.keepNullableLoopValues = keepNullableLoopValues; + return this; + } } } diff --git a/src/test/java/com/hubspot/jinjava/el/ExtendedSyntaxBuilderTest.java b/src/test/java/com/hubspot/jinjava/el/ExtendedSyntaxBuilderTest.java index 009fab1bd..ac7e6d85f 100644 --- a/src/test/java/com/hubspot/jinjava/el/ExtendedSyntaxBuilderTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ExtendedSyntaxBuilderTest.java @@ -196,7 +196,12 @@ public void mapLiteral() { BaseJinjavaTest .newConfigBuilder() .withMaxOutputSize(MAX_STRING_LENGTH) - .withLegacyOverrides(LegacyOverrides.THREE_POINT_0.withEvaluateMapKeys(false)) + .withLegacyOverrides( + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withEvaluateMapKeys(false) + .build() + ) .build() ) .newInterpreter(); diff --git a/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java b/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java index 1d099dcdf..b6f1524fe 100644 --- a/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java +++ b/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java @@ -387,7 +387,10 @@ public void itInterpretsEmptyExpressions() { .newConfigBuilder() .withTimeZone(ZoneId.of("America/New_York")) .withLegacyOverrides( - LegacyOverrides.THREE_POINT_0.withParseWhitespaceControlStrictly(false) + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withParseWhitespaceControlStrictly(false) + .build() ) .build() ); diff --git a/src/test/java/com/hubspot/jinjava/objects/collections/PyMapTest.java b/src/test/java/com/hubspot/jinjava/objects/collections/PyMapTest.java index 0d0a897af..0bb4bfa33 100644 --- a/src/test/java/com/hubspot/jinjava/objects/collections/PyMapTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/collections/PyMapTest.java @@ -359,7 +359,12 @@ public void itDoesntUpdateKeysWithVariableNameWhenLegacy() { new Jinjava( BaseJinjavaTest .newConfigBuilder() - .withLegacyOverrides(LegacyOverrides.THREE_POINT_0.withEvaluateMapKeys(false)) + .withLegacyOverrides( + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withEvaluateMapKeys(false) + .build() + ) .build() ); assertThat( diff --git a/src/test/java/com/hubspot/jinjava/tree/TreeParserTest.java b/src/test/java/com/hubspot/jinjava/tree/TreeParserTest.java index 80fc04a27..7beba0733 100644 --- a/src/test/java/com/hubspot/jinjava/tree/TreeParserTest.java +++ b/src/test/java/com/hubspot/jinjava/tree/TreeParserTest.java @@ -257,7 +257,10 @@ public void itTrimsNotes() { BaseJinjavaTest .newConfigBuilder() .withLegacyOverrides( - LegacyOverrides.THREE_POINT_0.withUseTrimmingForNotesAndExpressions(false) + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withUseTrimmingForNotesAndExpressions(false) + .build() ) .build() ) @@ -349,7 +352,10 @@ public void itTrimsExpressions() { BaseJinjavaTest .newConfigBuilder() .withLegacyOverrides( - LegacyOverrides.THREE_POINT_0.withUseTrimmingForNotesAndExpressions(false) + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withUseTrimmingForNotesAndExpressions(false) + .build() ) .build() ) @@ -368,7 +374,10 @@ public void itDoesNotMergeAdjacentTextNodesWhenLegacyOverrideIsApplied() { BaseJinjavaTest .newConfigBuilder() .withLegacyOverrides( - LegacyOverrides.THREE_POINT_0.withAllowAdjacentTextNodes(false) + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withAllowAdjacentTextNodes(false) + .build() ) .build() ) diff --git a/src/test/java/com/hubspot/jinjava/util/ObjectIteratorTest.java b/src/test/java/com/hubspot/jinjava/util/ObjectIteratorTest.java index 9c8d9f76c..f77786b50 100644 --- a/src/test/java/com/hubspot/jinjava/util/ObjectIteratorTest.java +++ b/src/test/java/com/hubspot/jinjava/util/ObjectIteratorTest.java @@ -103,7 +103,9 @@ public void testItIteratesOverValues() { public void testItIteratesOverKeys() throws Exception { JinjavaConfig config = BaseJinjavaTest .newConfigBuilder() - .withLegacyOverrides(LegacyOverrides.builder().withIterateOverMapKeys(true).build()) + .withLegacyOverrides( + LegacyOverrides.newBuilder().withIterateOverMapKeys(true).build() + ) .build(); JinjavaInterpreter.pushCurrent( new JinjavaInterpreter(new Jinjava(), new Context(), config) From 5450ee83db5c53e62b466443f03515a2586058ed Mon Sep 17 00:00:00 2001 From: jasmith-hs Date: Fri, 24 Apr 2026 10:40:11 -0400 Subject: [PATCH 4/6] Keep reconstructing the interpreter as `____int3rpr3t3r____` so that the eager execution output template string itself is backwards compatible with Jinjava 2.8.x --- .../com/hubspot/jinjava/el/ext/ExtendedParser.java | 2 +- .../jinjava/el/ext/eager/EvalResultHolder.java | 12 +++++++----- .../jinjava/lib/tag/eager/EagerCycleTag.java | 2 +- src/test/java/com/hubspot/jinjava/EagerTest.java | 12 ++++++++---- .../hubspot/jinjava/el/ext/ExtendedParserTest.java | 5 +++-- .../jinjava/el/ext/eager/EagerAstMethodTest.java | 2 +- .../jinjava/lib/tag/eager/EagerImportTagTest.java | 8 ++++---- .../jinjava/lib/tag/eager/EagerSetTagTest.java | 8 +++++--- .../jinjava/util/EagerExpressionResolverTest.java | 6 +++--- .../defers-macro-in-expression/test.expected.jinja | 4 ++-- .../eager/defers-macro-in-for/test.expected.jinja | 2 +- .../eager/defers-macro-in-if/test.expected.jinja | 2 +- .../test.expected.jinja | 14 +++++++------- .../test.expected.jinja | 4 ++-- .../handles-deferred-cycle-as/test.expected.jinja | 6 +++--- .../test.expected.jinja | 6 +++--- .../test.expected.jinja | 12 ++++++------ .../test.expected.jinja | 4 ++-- .../test.expected.jinja | 2 +- .../test.expected.jinja | 2 +- .../reconstructs-fromed-macro/test.expected.jinja | 2 +- .../uses-unique-macro-names/test.expected.jinja | 4 ++-- 22 files changed, 65 insertions(+), 56 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java index 54726af70..543726a7b 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java @@ -129,7 +129,7 @@ public AstNode createAstNode(AstNode... children) { } protected AstNode interpreter() { - return new AstNull(); + return identifier(INTERPRETER); } @Override diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EvalResultHolder.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EvalResultHolder.java index b2bda607e..f58d05d38 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EvalResultHolder.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EvalResultHolder.java @@ -76,12 +76,14 @@ static String reconstructNode( if (astNode == null) { return ""; } + if ( + astNode instanceof AstIdentifier && + ExtendedParser.INTERPRETER.equals(((AstIdentifier) astNode).getName()) + ) { + return ExtendedParser.INTERPRETER; + } preserveIdentifier = - IdentifierPreservationStrategy.preserving( - preserveIdentifier.isPreserving() || - (astNode instanceof AstIdentifier && - ExtendedParser.INTERPRETER.equals(((AstIdentifier) astNode).getName())) - ); + IdentifierPreservationStrategy.preserving(preserveIdentifier.isPreserving()); if ( preserveIdentifier.isPreserving() && !astNode.hasEvalResult() && diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTag.java index 0592ec3ad..4dc0917bf 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTag.java @@ -245,7 +245,7 @@ private static String getIsIterable(String var, int forIndex, TagToken tagToken) ) + // modulo indexing String.format( - "{{ %s[%d %% filter:length.filter(%s, null)] }}", + "{{ %s[%d %% filter:length.filter(%s, ____int3rpr3t3r____)] }}", var, forIndex, var diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 78d196aa0..e408d2cc2 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -216,7 +216,7 @@ public void itPreserveDeferredVariableResolvingEqualToInOrCondition() { assertThat(output) .isEqualTo( - "{% if false || exptest:equalto.evaluate('a', null, deferred) %}preserved{% endif %}" + "{% if false || exptest:equalto.evaluate('a', ____int3rpr3t3r____, deferred) %}preserved{% endif %}" ); assertThat(interpreter.getErrors()).isEmpty(); localContext.put("deferred", "a"); @@ -307,7 +307,8 @@ public void itPreservesForTag() { @Test public void itPreservesFilters() { String output = interpreter.render("{{ deferred|capitalize }}"); - assertThat(output).isEqualTo("{{ filter:capitalize.filter(deferred, null) }}"); + assertThat(output) + .isEqualTo("{{ filter:capitalize.filter(deferred, ____int3rpr3t3r____) }}"); assertThat(interpreter.getErrors()).isEmpty(); localContext.put("deferred", "foo"); assertThat(interpreter.render(output)).isEqualTo("Foo"); @@ -317,14 +318,17 @@ public void itPreservesFilters() { public void itPreservesFunctions() { String output = interpreter.render("{{ deferred|datetimeformat('%B %e, %Y') }}"); assertThat(output) - .isEqualTo("{{ filter:datetimeformat.filter(deferred, null, '%B %e, %Y') }}"); + .isEqualTo( + "{{ filter:datetimeformat.filter(deferred, ____int3rpr3t3r____, '%B %e, %Y') }}" + ); assertThat(interpreter.getErrors()).isEmpty(); } @Test public void itPreservesRandomness() { String output = interpreter.render("{{ [1, 2, 3]|shuffle }}"); - assertThat(output).isEqualTo("{{ filter:shuffle.filter([1, 2, 3], null) }}"); + assertThat(output) + .isEqualTo("{{ filter:shuffle.filter([1, 2, 3], ____int3rpr3t3r____) }}"); assertThat(interpreter.getErrors()).isEmpty(); } diff --git a/src/test/java/com/hubspot/jinjava/el/ext/ExtendedParserTest.java b/src/test/java/com/hubspot/jinjava/el/ext/ExtendedParserTest.java index 228128248..39cdc6124 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/ExtendedParserTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/ExtendedParserTest.java @@ -11,7 +11,6 @@ import de.odysseus.el.tree.impl.ast.AstMethod; import de.odysseus.el.tree.impl.ast.AstNested; import de.odysseus.el.tree.impl.ast.AstNode; -import de.odysseus.el.tree.impl.ast.AstNull; import de.odysseus.el.tree.impl.ast.AstParameters; import de.odysseus.el.tree.impl.ast.AstString; import org.assertj.core.api.Assertions; @@ -176,10 +175,12 @@ private void assertForExpression( AstParameters astParameters = (AstParameters) astNode.getChild(1); assertThat(astParameters.getChild(0)).isInstanceOf(AstString.class); - assertThat(astParameters.getChild(1)).isInstanceOf(AstNull.class); + assertThat(astParameters.getChild(1)).isInstanceOf(AstIdentifier.class); assertThat(astParameters.getChild(2)).isInstanceOf(AstString.class); assertThat(astParameters.getChild(0).eval(null, null)).isEqualTo(leftExpected); + assertThat(((AstIdentifier) astParameters.getChild(1)).getName()) + .isEqualTo("____int3rpr3t3r____"); assertThat(astParameters.getChild(2).eval(null, null)).isEqualTo(rightExpected); } diff --git a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethodTest.java b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethodTest.java index c60117f6c..d794f6c8e 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethodTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethodTest.java @@ -235,7 +235,7 @@ public void itPreservesUnresolvable() { fail("Should throw DeferredParsingException"); } catch (DeferredParsingException e) { assertThat(e.getDeferredEvalResult()) - .isEqualTo("filter:upper.filter(foo_object.deferred, null)"); + .isEqualTo("filter:upper.filter(foo_object.deferred, ____int3rpr3t3r____)"); } } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTagTest.java index ee05c7748..58de257ed 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTagTest.java @@ -423,7 +423,7 @@ public void itCorrectlySetsAliasedPathForSecondPass() { assertThat(firstPassResult) .isEqualTo( "{% set deferred_import_resource_path = 'import-macro.jinja' %}{% macro m.print_path_macro(var) %}\n" + - "{{ filter:print_path.filter(var, null) }}\n" + + "{{ filter:print_path.filter(var, ____int3rpr3t3r____) }}\n" + "{{ var }}\n" + "{% endmacro %}{% set deferred_import_resource_path = null %}{{ m.print_path_macro(foo) }}" ); @@ -442,7 +442,7 @@ public void itCorrectlySetsPathForSecondPass() { assertThat(firstPassResult) .isEqualTo( "{% set deferred_import_resource_path = 'import-macro.jinja' %}{% macro print_path_macro(var) %}\n" + - "{{ filter:print_path.filter(var, null) }}\n" + + "{{ filter:print_path.filter(var, ____int3rpr3t3r____) }}\n" + "{{ var }}\n" + "{% endmacro %}{% set deferred_import_resource_path = null %}{{ print_path_macro(foo) }}" ); @@ -460,9 +460,9 @@ public void itCorrectlySetsNestedPathsForSecondPass() { ); assertThat(firstPassResult) .isEqualTo( - "{% set deferred_import_resource_path = 'double-import-macro.jinja' %}{% macro print_path_macro2(var) %}{{ filter:print_path.filter(var, null) }}\n" + + "{% set deferred_import_resource_path = 'double-import-macro.jinja' %}{% macro print_path_macro2(var) %}{{ filter:print_path.filter(var, ____int3rpr3t3r____) }}\n" + "{% set deferred_import_resource_path = 'import-macro.jinja' %}{% macro print_path_macro(var) %}\n" + - "{{ filter:print_path.filter(var, null) }}\n" + + "{{ filter:print_path.filter(var, ____int3rpr3t3r____) }}\n" + "{{ var }}\n" + "{% endmacro %}{% set deferred_import_resource_path = 'double-import-macro.jinja' %}{{ print_path_macro(var) }}{% endmacro %}{% set deferred_import_resource_path = null %}{{ print_path_macro2(foo) }}" ); diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagTest.java index f80628e69..73f484d4d 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagTest.java @@ -142,7 +142,9 @@ public void itDefersBlockWithFilter() { final String result = interpreter.render(template); assertThat(result) - .isEqualTo("{% set foo = filter:add.filter(2, null, deferred) %}{{ foo }}"); + .isEqualTo( + "{% set foo = filter:add.filter(2, ____int3rpr3t3r____, deferred) %}{{ foo }}" + ); assertThat( context .getDeferredTokens() @@ -177,7 +179,7 @@ public void itDefersDeferredBlockWithDeferredFilter() { assertThat(result) .isEqualTo( - "{% set foo %}{{ 1 + deferred }}{% endset %}{% set foo = filter:add.filter(foo, null, filter:int.filter(deferred, null)) %}{{ foo }}" + "{% set foo %}{{ 1 + deferred }}{% endset %}{% set foo = filter:add.filter(foo, ____int3rpr3t3r____, filter:int.filter(deferred, ____int3rpr3t3r____)) %}{{ foo }}" ); context.remove("foo"); context.put("deferred", 2); @@ -201,7 +203,7 @@ public void itDefersInDeferredExecutionModeWithFilter() { assertThat(result) .isEqualTo( - "{% set foo %}1{% endset %}{% set foo = filter:add.filter(1, null, deferred) %}{{ foo }}" + "{% set foo %}1{% endset %}{% set foo = filter:add.filter(1, ____int3rpr3t3r____, deferred) %}{{ foo }}" ); assertThat( context diff --git a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java index d3458e3bb..1065349cb 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java @@ -641,7 +641,7 @@ public void itHandlesDeferredExpTests() { interpreter.getContext().setThrowInterpreterErrors(true); String partiallyResolved = eagerExpressionResult.toString(); assertThat(partiallyResolved) - .isEqualTo("exptest:equalto.evaluateNegated(4, null, deferred)"); + .isEqualTo("exptest:equalto.evaluateNegated(4, ____int3rpr3t3r____, deferred)"); assertThat(eagerExpressionResult.getDeferredWords()) .containsExactlyInAnyOrder("deferred", "equalto.evaluateNegated"); context.put("deferred", 4); @@ -756,7 +756,7 @@ public void itHandlesDeferredMethod() { assertThat(eagerResolveExpression("deferred.append(foo)").toString()) .isEqualTo("deferred.append('foo')"); assertThat(eagerResolveExpression("deferred[1 + 1] | length").toString()) - .isEqualTo("filter:length.filter(deferred[2], null)"); + .isEqualTo("filter:length.filter(deferred[2], ____int3rpr3t3r____)"); } @Test @@ -823,7 +823,7 @@ public void itDoesNotSplitJsonInArrayResolvedExpression() { @Test public void itHandlesRandom() { assertThat(eagerResolveExpression("range(1)|random").toString()) - .isEqualTo("filter:random.filter(range(1), null)"); + .isEqualTo("filter:random.filter(range(1), ____int3rpr3t3r____)"); } @Test diff --git a/src/test/resources/eager/defers-macro-in-expression/test.expected.jinja b/src/test/resources/eager/defers-macro-in-expression/test.expected.jinja index 18ad7e65b..7443a94eb 100644 --- a/src/test/resources/eager/defers-macro-in-expression/test.expected.jinja +++ b/src/test/resources/eager/defers-macro-in-expression/test.expected.jinja @@ -1,10 +1,10 @@ 2 {% macro plus(foo, add) %}\ -{{ foo + (filter:int.filter(add, null)) }}\ +{{ foo + (filter:int.filter(add, ____int3rpr3t3r____)) }}\ {% endmacro %}\ {{ plus(deferred, 1.1) }}\ {% set deferred = deferred + 2 %} {% macro plus(foo, add) %}\ -{{ foo + (filter:int.filter(add, null)) }}\ +{{ foo + (filter:int.filter(add, ____int3rpr3t3r____)) }}\ {% endmacro %}\ {{ plus(deferred, 3.1) }} \ No newline at end of file diff --git a/src/test/resources/eager/defers-macro-in-for/test.expected.jinja b/src/test/resources/eager/defers-macro-in-for/test.expected.jinja index e9540a513..3311b8714 100644 --- a/src/test/resources/eager/defers-macro-in-for/test.expected.jinja +++ b/src/test/resources/eager/defers-macro-in-for/test.expected.jinja @@ -3,6 +3,6 @@ {% do my_list.append(num) %}\ {{ my_list }}\ {% endmacro %}\ -{% for item in filter:split.filter(macro_append(deferred), null, ',', 2) %} +{% for item in filter:split.filter(macro_append(deferred), ____int3rpr3t3r____, ',', 2) %} {{ item }} {% endfor %} diff --git a/src/test/resources/eager/defers-macro-in-if/test.expected.jinja b/src/test/resources/eager/defers-macro-in-if/test.expected.jinja index 9837ae87b..67f28d9e4 100644 --- a/src/test/resources/eager/defers-macro-in-if/test.expected.jinja +++ b/src/test/resources/eager/defers-macro-in-if/test.expected.jinja @@ -3,6 +3,6 @@ {% do my_list.append(num) %}\ {{ my_list }}\ {% endmacro %}\ -{% if [] == filter:split.filter(macro_append(deferred), null, ',', 2) %} +{% if [] == filter:split.filter(macro_append(deferred), ____int3rpr3t3r____, ',', 2) %} {{ my_list }} {% endif %} diff --git a/src/test/resources/eager/does-not-override-import-modification-in-for/test.expected.jinja b/src/test/resources/eager/does-not-override-import-modification-in-for/test.expected.jinja index e37dfc69f..0e1e4a8a2 100644 --- a/src/test/resources/eager/does-not-override-import-modification-in-for/test.expected.jinja +++ b/src/test/resources/eager/does-not-override-import-modification-in-for/test.expected.jinja @@ -11,7 +11,7 @@ {% endif %} -{% set foo = filter:join.filter([foo, 'b'], null, '') %}\ +{% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}\ {% do __temp_meta_import_alias_3016318__.update({'foo': foo}) %} {% do __temp_meta_import_alias_3016318__.update({'foo': foo,'import_resource_path': 'eager/supplements/deferred-modification.jinja'}) %}\ {% endfor %}\ @@ -25,12 +25,12 @@ {% for __ignored__ in [0] %}\ {% if deferred %} -{% set foo = filter:join.filter([foo, 'a'], null, '') %}\ +{% set foo = filter:join.filter([foo, 'a'], ____int3rpr3t3r____, '') %}\ {% do __temp_meta_import_alias_3016319__.update({'foo': foo}) %} {% endif %} -{% set foo = filter:join.filter([foo, 'b'], null, '') %}\ +{% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}\ {% do __temp_meta_import_alias_3016319__.update({'foo': foo}) %} {% do __temp_meta_import_alias_3016319__.update({'import_resource_path': 'eager/supplements/deferred-modification.jinja'}) %}\ {% endfor %}\ @@ -45,12 +45,12 @@ {% for __ignored__ in [0] %}\ {% if deferred %} -{% set foo = filter:join.filter([foo, 'a'], null, '') %}\ +{% set foo = filter:join.filter([foo, 'a'], ____int3rpr3t3r____, '') %}\ {% do __temp_meta_import_alias_3016318__.update({'foo': foo}) %} {% endif %} -{% set foo = filter:join.filter([foo, 'b'], null, '') %}\ +{% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}\ {% do __temp_meta_import_alias_3016318__.update({'foo': foo}) %} {% do __temp_meta_import_alias_3016318__.update({'import_resource_path': 'eager/supplements/deferred-modification.jinja'}) %}\ {% endfor %}\ @@ -64,12 +64,12 @@ {% for __ignored__ in [0] %}\ {% if deferred %} -{% set foo = filter:join.filter([foo, 'a'], null, '') %}\ +{% set foo = filter:join.filter([foo, 'a'], ____int3rpr3t3r____, '') %}\ {% do __temp_meta_import_alias_3016319__.update({'foo': foo}) %} {% endif %} -{% set foo = filter:join.filter([foo, 'b'], null, '') %}\ +{% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}\ {% do __temp_meta_import_alias_3016319__.update({'foo': foo}) %} {% do __temp_meta_import_alias_3016319__.update({'import_resource_path': 'eager/supplements/deferred-modification.jinja'}) %}\ {% endfor %}\ diff --git a/src/test/resources/eager/fully-defers-filtered-macro/test.expected.jinja b/src/test/resources/eager/fully-defers-filtered-macro/test.expected.jinja index 76727bbcc..4938403bd 100644 --- a/src/test/resources/eager/fully-defers-filtered-macro/test.expected.jinja +++ b/src/test/resources/eager/fully-defers-filtered-macro/test.expected.jinja @@ -1,5 +1,5 @@ {% macro flashy(foo) %}\ -{{ filter:upper.filter(foo, null) }} +{{ filter:upper.filter(foo, ____int3rpr3t3r____) }} A flashy {{ deferred }}\ .{% endmacro %}\ {% set __macro_flashy_1625622909_temp_variable_0__ %}\ @@ -12,4 +12,4 @@ BAR {% set __macro_silly_2092874071_temp_variable_0__ %}\ A silly {{ deferred }}\ .{% endset %}\ -{{ filter:upper.filter(__macro_silly_2092874071_temp_variable_0__, null) }} \ No newline at end of file +{{ filter:upper.filter(__macro_silly_2092874071_temp_variable_0__, ____int3rpr3t3r____) }} \ No newline at end of file diff --git a/src/test/resources/eager/handles-deferred-cycle-as/test.expected.jinja b/src/test/resources/eager/handles-deferred-cycle-as/test.expected.jinja index 48b005b8c..6c84f4a4f 100644 --- a/src/test/resources/eager/handles-deferred-cycle-as/test.expected.jinja +++ b/src/test/resources/eager/handles-deferred-cycle-as/test.expected.jinja @@ -1,19 +1,19 @@ {% for __ignored__ in [0] %} {% set c = [1, deferred] %} {% if exptest:iterable.evaluate(c, null) %}\ -{{ c[0 % filter:length.filter(c, null)] }}\ +{{ c[0 % filter:length.filter(c, ____int3rpr3t3r____)] }}\ {% else %}\ {{ c }}\ {% endif %} {% set c = [2, deferred] %} {% if exptest:iterable.evaluate(c, null) %}\ -{{ c[1 % filter:length.filter(c, null)] }}\ +{{ c[1 % filter:length.filter(c, ____int3rpr3t3r____)] }}\ {% else %}\ {{ c }}\ {% endif %} {% set c = [3, deferred] %} {% if exptest:iterable.evaluate(c, null) %}\ -{{ c[2 % filter:length.filter(c, null)] }}\ +{{ c[2 % filter:length.filter(c, ____int3rpr3t3r____)] }}\ {% else %}\ {{ c }}\ {% endif %}\ diff --git a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro/test.expected.jinja b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro/test.expected.jinja index d7b09f4d6..54d9fc69a 100644 --- a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro/test.expected.jinja +++ b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro/test.expected.jinja @@ -5,12 +5,12 @@ {% set __macro_doIt_1327224118_temp_variable_0__ %} {{ deferred ~ '{\"a\":\"a\"}' }} {% endset %}\ -{{ filter:upper.filter(__macro_doIt_1327224118_temp_variable_0__, null) }} +{{ filter:upper.filter(__macro_doIt_1327224118_temp_variable_0__, ____int3rpr3t3r____) }} {% set __macro_doIt_1327224118_temp_variable_1__ %} {{ deferred ~ '{\"b\":\"b\"}' }} {% endset %}\ -{{ filter:upper.filter(__macro_doIt_1327224118_temp_variable_1__, null) }} +{{ filter:upper.filter(__macro_doIt_1327224118_temp_variable_1__, ____int3rpr3t3r____) }} {% endfor %} {% endset %}\ -{{ filter:upper.filter(__macro_getData_357124436_temp_variable_0__, null) }} +{{ filter:upper.filter(__macro_getData_357124436_temp_variable_0__, ____int3rpr3t3r____) }} diff --git a/src/test/resources/eager/handles-deferred-value-in-render-filter/test.expected.jinja b/src/test/resources/eager/handles-deferred-value-in-render-filter/test.expected.jinja index 5070d0d93..21a51dcc1 100644 --- a/src/test/resources/eager/handles-deferred-value-in-render-filter/test.expected.jinja +++ b/src/test/resources/eager/handles-deferred-value-in-render-filter/test.expected.jinja @@ -1,9 +1,9 @@ -{% set __render_572171194_temp_variable__ %}\ -Hi {{ filter:escape.filter(deferred, null) }}\ +{% set __render_524436216_temp_variable__ %}\ +Hi {{ filter:escape.filter(deferred, ____int3rpr3t3r____) }}\ {% endset %}\ -{{ filter:escape_jinjava.filter(__render_572171194_temp_variable__, null) }} +{{ filter:escape_jinjava.filter(__render_524436216_temp_variable__, ____int3rpr3t3r____) }} -{% set __render_572171194_temp_variable__ %}\ -Hi {{ filter:escape.filter(deferred, null) }}\ +{% set __render_524436216_temp_variable__ %}\ +Hi {{ filter:escape.filter(deferred, ____int3rpr3t3r____) }}\ {% endset %}\ -{{ __render_572171194_temp_variable__ }} \ No newline at end of file +{{ __render_524436216_temp_variable__ }} \ No newline at end of file diff --git a/src/test/resources/eager/handles-double-import-modification/test.expected.jinja b/src/test/resources/eager/handles-double-import-modification/test.expected.jinja index 7790ece96..66acdde36 100644 --- a/src/test/resources/eager/handles-double-import-modification/test.expected.jinja +++ b/src/test/resources/eager/handles-double-import-modification/test.expected.jinja @@ -9,7 +9,7 @@ {% endif %} -{% set foo = filter:join.filter([foo, 'b'], null, '') %}\ +{% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}\ {% do __temp_meta_import_alias_3016318__.update({'foo': foo}) %} {% do __temp_meta_import_alias_3016318__.update({'foo': foo,'import_resource_path': 'eager/supplements/deferred-modification.jinja'}) %}\ {% endfor %}\ @@ -28,7 +28,7 @@ {% endif %} -{% set foo = filter:join.filter([foo, 'b'], null, '') %}\ +{% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}\ {% do __temp_meta_import_alias_3016319__.update({'foo': foo}) %} {% do __temp_meta_import_alias_3016319__.update({'foo': foo,'import_resource_path': 'eager/supplements/deferred-modification.jinja'}) %}\ {% endfor %}\ diff --git a/src/test/resources/eager/handles-same-name-import-var/test.expected.jinja b/src/test/resources/eager/handles-same-name-import-var/test.expected.jinja index 7059c551c..66cfa99f0 100644 --- a/src/test/resources/eager/handles-same-name-import-var/test.expected.jinja +++ b/src/test/resources/eager/handles-same-name-import-var/test.expected.jinja @@ -35,5 +35,5 @@ {% set my_var = __temp_meta_import_alias_1059697132__ %}\ {% set current_path,__temp_meta_current_path_944750549__ = __temp_meta_current_path_944750549__,null %}\ {% enddo %} -{{ filter:dictsort.filter(my_var, null, false, 'key') }} +{{ filter:dictsort.filter(my_var, ____int3rpr3t3r____, false, 'key') }} {% endif %} diff --git a/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop/test.expected.jinja b/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop/test.expected.jinja index 4340b7b25..d0152a7f9 100644 --- a/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop/test.expected.jinja +++ b/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop/test.expected.jinja @@ -2,5 +2,5 @@ {% set __macro_foo_97643642_temp_variable_0__ %} {{ deferred }} {% endset %}\ -{{ filter:int.filter(__macro_foo_97643642_temp_variable_0__, null) + 3 }} +{{ filter:int.filter(__macro_foo_97643642_temp_variable_0__, ____int3rpr3t3r____) + 3 }} {% endfor %} diff --git a/src/test/resources/eager/reconstructs-fromed-macro/test.expected.jinja b/src/test/resources/eager/reconstructs-fromed-macro/test.expected.jinja index 8e02146cf..c7213b008 100644 --- a/src/test/resources/eager/reconstructs-fromed-macro/test.expected.jinja +++ b/src/test/resources/eager/reconstructs-fromed-macro/test.expected.jinja @@ -1,6 +1,6 @@ {% set deferred_import_resource_path = 'eager/reconstructs-fromed-macro/has-macro.jinja' %}\ {% macro to_upper(param) %} - {{ filter:upper.filter(param, null) }} + {{ filter:upper.filter(param, ____int3rpr3t3r____) }} {% endmacro %}\ {% set deferred_import_resource_path = null %}\ {{ to_upper(deferred) }} \ No newline at end of file diff --git a/src/test/resources/eager/uses-unique-macro-names/test.expected.jinja b/src/test/resources/eager/uses-unique-macro-names/test.expected.jinja index e3585f49e..54c40b809 100644 --- a/src/test/resources/eager/uses-unique-macro-names/test.expected.jinja +++ b/src/test/resources/eager/uses-unique-macro-names/test.expected.jinja @@ -3,13 +3,13 @@ {% set __macro_foo_97643642_temp_variable_0__ %} Goodbye {{ myname }} {% endset %}\ -{% set a = filter:upper.filter(__macro_foo_97643642_temp_variable_0__, null) %} +{% set a = filter:upper.filter(__macro_foo_97643642_temp_variable_0__, ____int3rpr3t3r____) %} {% do %}\ {% set __temp_meta_current_path_203114534__,current_path = current_path,'eager/uses-unique-macro-names/macro-with-filter.jinja' %} {% set __macro_foo_1717337666_temp_variable_0__ %}\ Hello {{ myname }}\ {% endset %}\ -{% set b = filter:upper.filter(__macro_foo_1717337666_temp_variable_0__, null) %} +{% set b = filter:upper.filter(__macro_foo_1717337666_temp_variable_0__, ____int3rpr3t3r____) %} {% set current_path,__temp_meta_current_path_203114534__ = __temp_meta_current_path_203114534__,null %}\ {% enddo %} {{ a }} From 4891843cf2a7b28bef6d7e3b4a23dc732ba211ab Mon Sep 17 00:00:00 2001 From: jasmith-hs Date: Tue, 28 Apr 2026 16:00:01 -0400 Subject: [PATCH 5/6] Add iteratorOnlyReverseFilter LegacyOverride and fix Builder.from() bug The |reverse filter in Jinja returns an iterator, but Jinjava 2 returned a list. The ReverseArrayIterator introduced in 3.0 breaks non-standard usage like {{ (arr|reverse)[0] }}. This override (enabled in THREE_POINT_0) keeps the iterator behavior; when disabled, the result is collected into a list for backwards compatibility. Also fixes Builder.from() which was missing keepNullableLoopValues, causing it to silently reset to false when cloning a LegacyOverrides instance. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/hubspot/jinjava/LegacyOverrides.java | 18 ++++++++++++++- .../jinjava/lib/filter/ReverseFilter.java | 23 +++++++++++++++++-- .../jinjava/lib/filter/ReverseFilterTest.java | 20 ++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/LegacyOverrides.java b/src/main/java/com/hubspot/jinjava/LegacyOverrides.java index 5192aa866..be16ae950 100644 --- a/src/main/java/com/hubspot/jinjava/LegacyOverrides.java +++ b/src/main/java/com/hubspot/jinjava/LegacyOverrides.java @@ -18,6 +18,7 @@ public class LegacyOverrides { .withAllowAdjacentTextNodes(true) .withUseTrimmingForNotesAndExpressions(true) .withKeepNullableLoopValues(true) + .withIteratorOnlyReverseFilter(true) .build(); public static final LegacyOverrides ALL = new LegacyOverrides.Builder() .withEvaluateMapKeys(true) @@ -29,6 +30,7 @@ public class LegacyOverrides { .withAllowAdjacentTextNodes(true) .withUseTrimmingForNotesAndExpressions(true) .withKeepNullableLoopValues(true) + .withIteratorOnlyReverseFilter(true) .build(); private final boolean evaluateMapKeys; private final boolean iterateOverMapKeys; @@ -39,6 +41,7 @@ public class LegacyOverrides { private final boolean allowAdjacentTextNodes; private final boolean useTrimmingForNotesAndExpressions; private final boolean keepNullableLoopValues; + private final boolean iteratorOnlyReverseFilter; private LegacyOverrides(Builder builder) { evaluateMapKeys = builder.evaluateMapKeys; @@ -50,6 +53,7 @@ private LegacyOverrides(Builder builder) { allowAdjacentTextNodes = builder.allowAdjacentTextNodes; useTrimmingForNotesAndExpressions = builder.useTrimmingForNotesAndExpressions; keepNullableLoopValues = builder.keepNullableLoopValues; + iteratorOnlyReverseFilter = builder.iteratorOnlyReverseFilter; } public static Builder newBuilder() { @@ -92,6 +96,10 @@ public boolean isKeepNullableLoopValues() { return keepNullableLoopValues; } + public boolean isIteratorOnlyReverseFilter() { + return iteratorOnlyReverseFilter; + } + public static class Builder { private boolean evaluateMapKeys = false; @@ -103,6 +111,7 @@ public static class Builder { private boolean allowAdjacentTextNodes = false; private boolean useTrimmingForNotesAndExpressions = false; private boolean keepNullableLoopValues = false; + private boolean iteratorOnlyReverseFilter = false; private Builder() {} @@ -123,7 +132,9 @@ public static Builder from(LegacyOverrides legacyOverrides) { .withAllowAdjacentTextNodes(legacyOverrides.allowAdjacentTextNodes) .withUseTrimmingForNotesAndExpressions( legacyOverrides.useTrimmingForNotesAndExpressions - ); + ) + .withKeepNullableLoopValues(legacyOverrides.keepNullableLoopValues) + .withIteratorOnlyReverseFilter(legacyOverrides.iteratorOnlyReverseFilter); } public Builder withEvaluateMapKeys(boolean evaluateMapKeys) { @@ -176,5 +187,10 @@ public Builder withKeepNullableLoopValues(boolean keepNullableLoopValues) { this.keepNullableLoopValues = keepNullableLoopValues; return this; } + + public Builder withIteratorOnlyReverseFilter(boolean iteratorOnlyReverseFilter) { + this.iteratorOnlyReverseFilter = iteratorOnlyReverseFilter; + return this; + } } } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/ReverseFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/ReverseFilter.java index effe5a31c..53ad2d0a3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/ReverseFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/ReverseFilter.java @@ -21,8 +21,10 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.objects.collections.ArrayBacked; import java.lang.reflect.Array; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; +import java.util.List; import java.util.NoSuchElementException; @JinjavaDoc( @@ -51,11 +53,14 @@ public Object filter(Object object, JinjavaInterpreter interpreter, String... ar } // collection if (object instanceof Collection) { - return ReverseArrayIterator.create(((Collection) object).toArray()); + return maybeCollectToList( + ReverseArrayIterator.create(((Collection) object).toArray()), + interpreter + ); } // array if (object.getClass().isArray()) { - return ReverseArrayIterator.create(object); + return maybeCollectToList(ReverseArrayIterator.create(object), interpreter); } // string if (object instanceof String) { @@ -72,6 +77,20 @@ public Object filter(Object object, JinjavaInterpreter interpreter, String... ar return object; } + private Object maybeCollectToList( + ReverseArrayIterator iterator, + JinjavaInterpreter interpreter + ) { + if (interpreter.getConfig().getLegacyOverrides().isIteratorOnlyReverseFilter()) { + return iterator; + } + List result = new ArrayList<>(); + while (iterator.hasNext()) { + result.add(iterator.next()); + } + return result; + } + @Override public String getName() { return "reverse"; diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/ReverseFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/ReverseFilterTest.java index 7ded09ad8..5d60a5d90 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/ReverseFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/ReverseFilterTest.java @@ -3,6 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.LegacyOverrides; import java.util.HashMap; import java.util.Map; import org.junit.Test; @@ -28,4 +30,22 @@ public void itReversesObjectArray() { ) .isEqualTo("cba"); } + + @Test + public void itAllowsIndexingWhenLegacyOverrideIsDisabled() { + Jinjava legacyJinjava = new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withIteratorOnlyReverseFilter(false) + .build() + ) + .build() + ); + Map context = new HashMap<>(); + context.put("arr", new String[] { "a", "b", "c" }); + assertThat(legacyJinjava.render("{{ (arr|reverse)[0] }}", context)).isEqualTo("c"); + } } From 0b591477a48112718bbb0d1cbf552d5cbeca22f4 Mon Sep 17 00:00:00 2001 From: jasmith-hs Date: Wed, 29 Apr 2026 09:59:55 -0400 Subject: [PATCH 6/6] Delegate all public ZonedDateTime instance methods on PyishDate JinjavaInterpreterResolver wraps ZonedDateTime results back into PyishDate, so the delegate methods return raw ZonedDateTime to keep the PyishDate implementation clean. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../jinjava/objects/date/PyishDate.java | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) diff --git a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java index 498bf4cb7..f7b194dfa 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java @@ -7,10 +7,24 @@ import com.hubspot.jinjava.objects.serialization.PyishSerializable; import java.io.IOException; import java.io.Serializable; +import java.time.DayOfWeek; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoField; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalField; +import java.time.temporal.TemporalQuery; +import java.time.temporal.TemporalUnit; +import java.time.temporal.ValueRange; import java.util.Date; import java.util.Objects; import java.util.Optional; @@ -115,6 +129,237 @@ public int getMicrosecond() { return date.get(ChronoField.MILLI_OF_SECOND); } + // ZonedDateTime delegate methods + + public ZoneId getZone() { + return date.getZone(); + } + + public ZoneOffset getOffset() { + return date.getOffset(); + } + + public ZonedDateTime withZoneSameLocal(ZoneId zone) { + return date.withZoneSameLocal(zone); + } + + public ZonedDateTime withZoneSameInstant(ZoneId zone) { + return date.withZoneSameInstant(zone); + } + + public ZonedDateTime withFixedOffsetZone() { + return date.withFixedOffsetZone(); + } + + public ZonedDateTime withEarlierOffsetAtOverlap() { + return date.withEarlierOffsetAtOverlap(); + } + + public ZonedDateTime withLaterOffsetAtOverlap() { + return date.withLaterOffsetAtOverlap(); + } + + public LocalDateTime toLocalDateTime() { + return date.toLocalDateTime(); + } + + public LocalDate toLocalDate() { + return date.toLocalDate(); + } + + public LocalTime toLocalTime() { + return date.toLocalTime(); + } + + public OffsetDateTime toOffsetDateTime() { + return date.toOffsetDateTime(); + } + + @Override + public Instant toInstant() { + return date.toInstant(); + } + + public boolean isSupported(TemporalField field) { + return date.isSupported(field); + } + + public boolean isSupported(TemporalUnit unit) { + return date.isSupported(unit); + } + + public ValueRange range(TemporalField field) { + return date.range(field); + } + + public int get(TemporalField field) { + return date.get(field); + } + + public long getLong(TemporalField field) { + return date.getLong(field); + } + + public int getMonthValue() { + return date.getMonthValue(); + } + + public int getDayOfMonth() { + return date.getDayOfMonth(); + } + + public int getDayOfYear() { + return date.getDayOfYear(); + } + + public DayOfWeek getDayOfWeek() { + return date.getDayOfWeek(); + } + + public int getNano() { + return date.getNano(); + } + + public ZonedDateTime with(TemporalAdjuster adjuster) { + return date.with(adjuster); + } + + public ZonedDateTime with(TemporalField field, long newValue) { + return date.with(field, newValue); + } + + public ZonedDateTime withYear(int year) { + return date.withYear(year); + } + + public ZonedDateTime withMonth(int month) { + return date.withMonth(month); + } + + public ZonedDateTime withDayOfMonth(int dayOfMonth) { + return date.withDayOfMonth(dayOfMonth); + } + + public ZonedDateTime withDayOfYear(int dayOfYear) { + return date.withDayOfYear(dayOfYear); + } + + public ZonedDateTime withHour(int hour) { + return date.withHour(hour); + } + + public ZonedDateTime withMinute(int minute) { + return date.withMinute(minute); + } + + public ZonedDateTime withSecond(int second) { + return date.withSecond(second); + } + + public ZonedDateTime withNano(int nanoOfSecond) { + return date.withNano(nanoOfSecond); + } + + public ZonedDateTime truncatedTo(TemporalUnit unit) { + return date.truncatedTo(unit); + } + + public ZonedDateTime plus(TemporalAmount amountToAdd) { + return date.plus(amountToAdd); + } + + public ZonedDateTime plus(long amountToAdd, TemporalUnit unit) { + return date.plus(amountToAdd, unit); + } + + public ZonedDateTime plusYears(long years) { + return date.plusYears(years); + } + + public ZonedDateTime plusMonths(long months) { + return date.plusMonths(months); + } + + public ZonedDateTime plusWeeks(long weeks) { + return date.plusWeeks(weeks); + } + + public ZonedDateTime plusDays(long days) { + return date.plusDays(days); + } + + public ZonedDateTime plusHours(long hours) { + return date.plusHours(hours); + } + + public ZonedDateTime plusMinutes(long minutes) { + return date.plusMinutes(minutes); + } + + public ZonedDateTime plusSeconds(long seconds) { + return date.plusSeconds(seconds); + } + + public ZonedDateTime plusNanos(long nanos) { + return date.plusNanos(nanos); + } + + public ZonedDateTime minus(TemporalAmount amountToSubtract) { + return date.minus(amountToSubtract); + } + + public ZonedDateTime minus(long amountToSubtract, TemporalUnit unit) { + return date.minus(amountToSubtract, unit); + } + + public ZonedDateTime minusYears(long years) { + return date.minusYears(years); + } + + public ZonedDateTime minusMonths(long months) { + return date.minusMonths(months); + } + + public ZonedDateTime minusWeeks(long weeks) { + return date.minusWeeks(weeks); + } + + public ZonedDateTime minusDays(long days) { + return date.minusDays(days); + } + + public ZonedDateTime minusHours(long hours) { + return date.minusHours(hours); + } + + public ZonedDateTime minusMinutes(long minutes) { + return date.minusMinutes(minutes); + } + + public ZonedDateTime minusSeconds(long seconds) { + return date.minusSeconds(seconds); + } + + public ZonedDateTime minusNanos(long nanos) { + return date.minusNanos(nanos); + } + + public R query(TemporalQuery query) { + return date.query(query); + } + + public long until(Temporal endExclusive, TemporalUnit unit) { + return date.until(endExclusive, unit); + } + + public String format(DateTimeFormatter formatter) { + return date.format(formatter); + } + + public long toEpochSecond() { + return date.toEpochSecond(); + } + public String getDateFormat() { return dateFormat; }