From 43a4330026f33ed73de873afd3342861b96f14f8 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 30 Aug 2022 14:53:02 +0200 Subject: [PATCH 001/637] add field dateFormat to set strftime date format per object --- .../com/hubspot/jinjava/objects/date/PyishDate.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 25bd02d31..d27c358be 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java @@ -30,6 +30,8 @@ public final class PyishDate private final ZonedDateTime date; + private String dateFormat = PYISH_DATE_FORMAT; + public PyishDate(ZonedDateTime dt) { super(dt.toInstant().toEpochMilli()); this.date = dt; @@ -100,6 +102,14 @@ public int getMicrosecond() { return date.get(ChronoField.MILLI_OF_SECOND); } + public String getDateFormat() { + return dateFormat; + } + + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } + public Date toDate() { return Date.from(date.toInstant()); } @@ -126,7 +136,7 @@ public String toString() { ); } - return strftime(PYISH_DATE_FORMAT); + return strftime(dateFormat); } @Override From 6cfcea0a0b2df7a00a51212a2c62cde8602e53d6 Mon Sep 17 00:00:00 2001 From: Alpri Else Date: Tue, 30 Aug 2022 09:51:29 -0400 Subject: [PATCH 002/637] Return long value when under min int value from IntFilter --- src/main/java/com/hubspot/jinjava/lib/filter/IntFilter.java | 2 +- .../java/com/hubspot/jinjava/lib/filter/IntFilterTest.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/IntFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/IntFilter.java index ecb454896..9e1689f09 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/IntFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/IntFilter.java @@ -74,7 +74,7 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) } private Object convertResult(Long result) { - if (result > Integer.MAX_VALUE) { + if (result < Integer.MIN_VALUE || result > Integer.MAX_VALUE) { return result; } return result.intValue(); diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/IntFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/IntFilterTest.java index 9fb63dc69..1fd199ffb 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/IntFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/IntFilterTest.java @@ -124,6 +124,11 @@ public void itUsesLongsForLargeValueDefaults() { .isEqualTo(1000000000001L); } + @Test + public void itUsesLongsForVerySmallValues() { + assertThat(filter.filter("-42595200000", interpreter)).isEqualTo(-42595200000L); + } + @Test public void itConvertsProperlyInExpressionTest() { assertThat(interpreter.render("{{ '3'|int in [null, 4, 5, 6, null, 3] }}")) From 8ddb2b2112126b24fba5b5d940acb9aee7263ecf Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 30 Aug 2022 16:46:17 +0200 Subject: [PATCH 003/637] add test --- .../com/hubspot/jinjava/objects/date/PyishDateTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java b/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java index 42f3e17ca..0c25fc12f 100644 --- a/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java @@ -93,4 +93,11 @@ public void testPyishDateToStringWithCustomDateFormatter() { JinjavaInterpreter.popCurrent(); } } + + @Test + public void testPyishDateCustomDateFormat() { + PyishDate d = new PyishDate(ZonedDateTime.parse("2013-10-31T14:15:16.170+02:00")); + d.setDateFormat("dd - MM - YYYY <> HH:mm:ss"); + assertThat(d.toString()).isEqualTo("31 - 10 - 2013 <> 14:15:16"); + } } From 0959806e846f9baf13d50259b9aa04ac619e602e Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 8 Sep 2022 06:45:58 -0400 Subject: [PATCH 004/637] Upgrade jackson to 2.12.6 --- pom.xml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pom.xml b/pom.xml index 67e11dfed..9c2cd9207 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,27 @@ java-ipv6 0.17 + + com.fasterxml.jackson.core + jackson-annotations + + 2.12.6 + + + com.fasterxml.jackson.core + jackson-databind + 2.12.6 + + + com.fasterxml.jackson.core + jackson-core + 2.12.6 + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.12.6 + @@ -228,6 +249,16 @@ + + org.basepom.maven + duplicate-finder-maven-plugin + + + module-info + META-INF.versions.9.module-info + + + From ac2a06368ede25e56bbbbcf6752224c3c6c50ed2 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 8 Sep 2022 06:46:14 -0400 Subject: [PATCH 005/637] Use single quote char for pyish serialization --- pom.xml | 1 - .../hubspot/jinjava/objects/date/PyishDate.java | 5 +++-- .../serialization/PyishCharacterEscapes.java | 8 +++++++- .../objects/serialization/PyishObjectMapper.java | 15 +++++++-------- .../objects/serialization/PyishSerializable.java | 7 +++++-- .../jinjava/util/EagerReconstructionUtils.java | 6 ++++-- .../jinjava/util/EagerExpressionResolverTest.java | 13 ++++++++++++- 7 files changed, 38 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 9c2cd9207..78969d2ff 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,6 @@ com.fasterxml.jackson.core jackson-annotations - 2.12.6 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 25bd02d31..4b036cea6 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java @@ -2,6 +2,7 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.objects.PyWrapper; +import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.objects.serialization.PyishSerializable; import java.io.Serializable; import java.time.Instant; @@ -149,9 +150,9 @@ public boolean equals(Object obj) { @Override public String toPyishString() { return String.format( - "\"%s\"|strtotime(\"%s\")", + "'%s'|strtotime(%s)", strftime(FULL_DATE_FORMAT), - FULL_DATE_FORMAT + PyishObjectMapper.getAsPyishString(FULL_DATE_FORMAT) ); } } diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishCharacterEscapes.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishCharacterEscapes.java index 7255e97c5..60f0dd2a2 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishCharacterEscapes.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishCharacterEscapes.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.SerializableString; import com.fasterxml.jackson.core.io.CharacterEscapes; +import com.fasterxml.jackson.core.io.SerializedString; import java.util.Arrays; public class PyishCharacterEscapes extends CharacterEscapes { @@ -11,9 +12,11 @@ public class PyishCharacterEscapes extends CharacterEscapes { private PyishCharacterEscapes() { int[] escapes = CharacterEscapes.standardAsciiEscapesForJSON(); escapes['\n'] = CharacterEscapes.ESCAPE_NONE; + escapes['"'] = CharacterEscapes.ESCAPE_NONE; escapes['\t'] = CharacterEscapes.ESCAPE_NONE; escapes['\r'] = CharacterEscapes.ESCAPE_NONE; escapes['\f'] = CharacterEscapes.ESCAPE_NONE; + escapes['\''] = CharacterEscapes.ESCAPE_CUSTOM; asciiEscapes = escapes; } @@ -23,7 +26,10 @@ public int[] getEscapeCodesForAscii() { } @Override - public SerializableString getEscapeSequence(int i) { + public SerializableString getEscapeSequence(int ch) { + if (ch == '\'') { + return new SerializedString("\\'"); + } return null; } } diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 42328329e..83b4f558b 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.objects.serialization; +import com.fasterxml.jackson.core.JsonFactoryBuilder; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonSerializer; @@ -18,7 +19,9 @@ public class PyishObjectMapper { public static final ObjectWriter PYISH_OBJECT_WRITER; static { - ObjectMapper mapper = new ObjectMapper() + ObjectMapper mapper = new ObjectMapper( + new JsonFactoryBuilder().quoteChar('\'').build() + ) .registerModule( new SimpleModule() .setSerializerModifier(PyishBeanSerializerModifier.INSTANCE) @@ -54,14 +57,10 @@ public static String getAsPyishStringOrThrow(Object val) if (maxStringLength.map(max -> string.length() > max).orElse(false)) { throw new OutputTooBigException(maxStringLength.get(), string.length()); } - String result = string - .replace("'", "\\'") - // Replace double-quotes with single quote as they are preferred in Jinja - .replaceAll("(? { diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java index dd8508382..4bab88047 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.objects.serialization; +import com.fasterxml.jackson.core.JsonFactoryBuilder; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; @@ -7,7 +8,9 @@ import java.util.Objects; public interface PyishSerializable extends PyWrapper { - ObjectWriter SELF_WRITER = new ObjectMapper() + ObjectWriter SELF_WRITER = new ObjectMapper( + new JsonFactoryBuilder().quoteChar('\'').build() + ) .writer(PyishPrettyPrinter.INSTANCE) .with(PyishCharacterEscapes.INSTANCE); /** @@ -31,7 +34,7 @@ static String writeValueAsString(Object value) { try { return SELF_WRITER.writeValueAsString(value); } catch (JsonProcessingException e) { - return '"' + Objects.toString(value) + '"'; + return '\'' + Objects.toString(value) + '\''; } } } diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 86df973ce..d2044ff0c 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -122,7 +122,8 @@ public static EagerExecutionResult executeInChildContext( .stream() .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) .filter( - entry -> EagerExpressionResolver.isResolvableObject(entry.getValue(), 2, 20) // TODO make this configurable + entry -> + EagerExpressionResolver.isResolvableObject(entry.getValue(), 4, 400) // TODO make this configurable ); } else { entryStream = @@ -132,7 +133,8 @@ public static EagerExecutionResult executeInChildContext( .stream() .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) .filter( - entry -> EagerExpressionResolver.isResolvableObject(entry.getValue(), 2, 20) // TODO make this configurable + entry -> + EagerExpressionResolver.isResolvableObject(entry.getValue(), 4, 400) // TODO make this configurable ); } entryStream.forEach( diff --git a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java index 104ae7d64..6cfe1771a 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java @@ -310,7 +310,18 @@ public void itSerializesDateProperly() { EagerExpressionResult eagerExpressionResult = eagerResolveExpression("date"); assertThat(WhitespaceUtils.unquoteAndUnescape(eagerExpressionResult.toString())) - .isEqualTo(date.toPyishString().replace("'", "\\'").replace('"', '\'')); + .isEqualTo(date.toPyishString()); + interpreter.render( + "{% set foo = " + + PyishObjectMapper.getAsPyishString(ImmutableMap.of("a", date)) + + " %}" + ); + assertThat( + ( + (PyishDate) ((Map) interpreter.getContext().get("foo")).get("a") + ).toDateTime() + ) + .isEqualTo(date.toDateTime()); } @Test From fc08e7c22f979e075a253ee0f9fb176c57fdfbe7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Sep 2022 03:30:42 +0000 Subject: [PATCH 006/637] Bump snakeyaml from 1.26 to 1.31 in /benchmark Bumps [snakeyaml](https://bitbucket.org/snakeyaml/snakeyaml) from 1.26 to 1.31. - [Commits](https://bitbucket.org/snakeyaml/snakeyaml/branches/compare/snakeyaml-1.31..snakeyaml-1.26) --- updated-dependencies: - dependency-name: org.yaml:snakeyaml dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- benchmark/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/pom.xml b/benchmark/pom.xml index 6c50e9178..740b1f2df 100644 --- a/benchmark/pom.xml +++ b/benchmark/pom.xml @@ -56,7 +56,7 @@ org.yaml snakeyaml - 1.26 + 1.31 de.sven-jacobs From 9e6fd2056dd22f6101501961286a4bb37a1847ef Mon Sep 17 00:00:00 2001 From: Alpri Else Date: Thu, 29 Sep 2022 16:08:40 -0400 Subject: [PATCH 007/637] Implement TemplateError warning when intersection Set elements have mismatched types --- .../jinjava/lib/filter/IntersectFilter.java | 44 +++++++++++++++++-- .../lib/filter/IntersectFilterTest.java | 23 ++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java index b7d1b9601..e598ed613 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java @@ -5,8 +5,14 @@ import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; +import com.hubspot.jinjava.lib.fn.TypeFunction; +import com.hubspot.jinjava.objects.collections.SizeLimitingPyList; +import java.lang.reflect.ParameterizedType; import java.util.ArrayList; +import java.util.List; import java.util.Map; +import java.util.Set; @JinjavaDoc( value = "Returns a list containing elements present in both lists", @@ -35,9 +41,41 @@ public Object filter( Object[] args, Map kwargs ) { - return new ArrayList<>( - Sets.intersection(objectToSet(var), objectToSet(parseArgs(interpreter, args))) - ); + Object argObj = parseArgs(interpreter, args); + + Set varSet = objectToSet(var); + Set argSet = objectToSet(argObj); + + boolean areMismatchedElementTypes = !getTypeOfSetElements(varSet) + .equals(getTypeOfSetElements(argSet)); + + if (areMismatchedElementTypes) { + interpreter.addError( + new TemplateError( + TemplateError.ErrorType.WARNING, + TemplateError.ErrorReason.OTHER, + TemplateError.ErrorItem.FILTER, + String.format( + "Mismatched types. `value` elements are of type `%s` and `list` elements are of type `%s`. This may lead to unexpected behavior.", + getTypeOfSetElements(varSet), + getTypeOfSetElements(argSet) + ), + "list", + interpreter.getLineNumber(), + -1, + null + ) + ); + } + + return new ArrayList<>(Sets.intersection(varSet, argSet)); + } + + private String getTypeOfSetElements(Set set) { + if (set.isEmpty()) { + return "null"; + } + return TypeFunction.type(set.iterator().next()); } @Override diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java index 2161fe58f..d2dcbe9d7 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java @@ -3,7 +3,10 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; import java.util.HashMap; +import java.util.List; import org.junit.Before; import org.junit.Test; @@ -31,4 +34,24 @@ public void itReturnsEmptyOnNullParameters() { assertThat(jinjava.render("{{ [1, 2, 3]|intersect(null) }}", new HashMap<>())) .isEqualTo("[]"); } + + @Test + public void itThrowsWarningOnMismatchTypes() { + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + + String renderedOutput = interpreter.render( + "{{ [1, 2, 3]|intersect(['1', '2', '3']) }}" + ); + assertThat(renderedOutput).isEqualTo("[]"); + + List errors = interpreter.getErrors(); + assertThat(errors).isNotEmpty(); + + TemplateError error = errors.get(0); + assertThat(error.getSeverity()).isEqualTo(TemplateError.ErrorType.WARNING); + assertThat(error.getMessage()) + .isEqualTo( + "Mismatched types. `value` elements are of type `long` and `list` elements are of type `str`. This may lead to unexpected behavior." + ); + } } From 6549175800878c18687cb3d59dc5d7ff653f3f06 Mon Sep 17 00:00:00 2001 From: Alpri Else Date: Thu, 29 Sep 2022 16:09:53 -0400 Subject: [PATCH 008/637] Rm unused imports --- .../java/com/hubspot/jinjava/lib/filter/IntersectFilter.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java index e598ed613..9f9c0fffb 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java @@ -7,10 +7,7 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.lib.fn.TypeFunction; -import com.hubspot.jinjava.objects.collections.SizeLimitingPyList; -import java.lang.reflect.ParameterizedType; import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.Set; From ed628e46533dc166b1321038974118af43a442d6 Mon Sep 17 00:00:00 2001 From: Alpri Else Date: Thu, 29 Sep 2022 16:19:21 -0400 Subject: [PATCH 009/637] Add unit test to IntersectFilterTest when set types are matching --- .../jinjava/lib/filter/IntersectFilterTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java index d2dcbe9d7..e989f4f41 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java @@ -35,6 +35,17 @@ public void itReturnsEmptyOnNullParameters() { .isEqualTo("[]"); } + @Test + public void itDoesNotThrowWarningOnMatchedTypes() { + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + + String renderedOutput = interpreter.render("{{ [1, 2, 3]|intersect([1, 2, 3]) }}"); + assertThat(renderedOutput).isEqualTo("[1, 2, 3]"); + + List errors = interpreter.getErrors(); + assertThat(errors).isEmpty(); + } + @Test public void itThrowsWarningOnMismatchTypes() { JinjavaInterpreter interpreter = jinjava.newInterpreter(); From 43660c5d3ffd7370f669b9f8f7447b94320ba331 Mon Sep 17 00:00:00 2001 From: Alpri Else Date: Mon, 3 Oct 2022 11:12:20 -0400 Subject: [PATCH 010/637] Don't throw warning on empty sets for |intersect filter --- .../jinjava/lib/filter/IntersectFilter.java | 5 ++-- .../lib/filter/IntersectFilterTest.java | 24 ++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java index 9f9c0fffb..5c01a09b0 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java @@ -43,17 +43,18 @@ public Object filter( Set varSet = objectToSet(var); Set argSet = objectToSet(argObj); + boolean isAtLeastOneSetEmpty = varSet.isEmpty() || argSet.isEmpty(); boolean areMismatchedElementTypes = !getTypeOfSetElements(varSet) .equals(getTypeOfSetElements(argSet)); - if (areMismatchedElementTypes) { + if (areMismatchedElementTypes && !isAtLeastOneSetEmpty) { interpreter.addError( new TemplateError( TemplateError.ErrorType.WARNING, TemplateError.ErrorReason.OTHER, TemplateError.ErrorItem.FILTER, String.format( - "Mismatched types. `value` elements are of type `%s` and `list` elements are of type `%s`. This may lead to unexpected behavior.", + "Mismatched Types: input set has elements of type '%s' but arg set has elements of type '%s'. Use |map filter to convert sets to the same type for filter to work correctly.", getTypeOfSetElements(varSet), getTypeOfSetElements(argSet) ), diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java index e989f4f41..1b6743325 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java @@ -46,6 +46,28 @@ public void itDoesNotThrowWarningOnMatchedTypes() { assertThat(errors).isEmpty(); } + @Test + public void itDoesNotThrowWarningOnEmptyVarSet() { + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + + String renderedOutput = interpreter.render("{{ []|intersect([1, 2, 3]) }}"); + assertThat(renderedOutput).isEqualTo("[]"); + + List errors = interpreter.getErrors(); + assertThat(errors).isEmpty(); + } + + @Test + public void itDoesNotThrowWarningOnEmptyArgSet() { + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + + String renderedOutput = interpreter.render("{{ [1, 2, 3]|intersect([]) }}"); + assertThat(renderedOutput).isEqualTo("[]"); + + List errors = interpreter.getErrors(); + assertThat(errors).isEmpty(); + } + @Test public void itThrowsWarningOnMismatchTypes() { JinjavaInterpreter interpreter = jinjava.newInterpreter(); @@ -62,7 +84,7 @@ public void itThrowsWarningOnMismatchTypes() { assertThat(error.getSeverity()).isEqualTo(TemplateError.ErrorType.WARNING); assertThat(error.getMessage()) .isEqualTo( - "Mismatched types. `value` elements are of type `long` and `list` elements are of type `str`. This may lead to unexpected behavior." + "Mismatched Types: input set has elements of type 'long' but arg set has elements of type 'str'. Use |map filter to convert sets to the same type for filter to work correctly." ); } } From a0bde4263b7dbecb8c9c6367501dc856f88cb043 Mon Sep 17 00:00:00 2001 From: Alpri Else Date: Mon, 3 Oct 2022 11:16:40 -0400 Subject: [PATCH 011/637] Move type helper method to AbstractSetFilter --- .../com/hubspot/jinjava/lib/filter/AbstractSetFilter.java | 8 ++++++++ .../com/hubspot/jinjava/lib/filter/IntersectFilter.java | 8 -------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java index 42ae7e6ca..f13a43bf4 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java @@ -2,6 +2,7 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateSyntaxException; +import com.hubspot.jinjava.lib.fn.TypeFunction; import com.hubspot.jinjava.util.ForLoop; import com.hubspot.jinjava.util.ObjectIterator; import java.util.LinkedHashSet; @@ -29,4 +30,11 @@ protected Set objectToSet(Object var) { } return result; } + + protected String getTypeOfSetElements(Set set) { + if (set.isEmpty()) { + return "null"; + } + return TypeFunction.type(set.iterator().next()); + } } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java index 5c01a09b0..0a2930288 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java @@ -6,7 +6,6 @@ import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateError; -import com.hubspot.jinjava.lib.fn.TypeFunction; import java.util.ArrayList; import java.util.Map; import java.util.Set; @@ -69,13 +68,6 @@ public Object filter( return new ArrayList<>(Sets.intersection(varSet, argSet)); } - private String getTypeOfSetElements(Set set) { - if (set.isEmpty()) { - return "null"; - } - return TypeFunction.type(set.iterator().next()); - } - @Override public String getName() { return "intersect"; From d06c4bd815516b5db191ff0b3b9b275490e0cdee Mon Sep 17 00:00:00 2001 From: Alpri Else Date: Mon, 3 Oct 2022 12:05:34 -0400 Subject: [PATCH 012/637] Throw warning for mismatched Set types for all concrete implementations of AbstractSetFilter --- .../jinjava/lib/filter/AbstractSetFilter.java | 37 ++++++++- .../jinjava/lib/filter/DifferenceFilter.java | 10 ++- .../jinjava/lib/filter/IntersectFilter.java | 24 +----- .../lib/filter/SymmetricDifferenceFilter.java | 13 +-- .../jinjava/lib/filter/UnionFilter.java | 10 ++- .../lib/filter/AbstractSetFilterTest.java | 82 +++++++++++++++++++ .../lib/filter/IntersectFilterTest.java | 56 ------------- 7 files changed, 140 insertions(+), 92 deletions(-) create mode 100644 src/test/java/com/hubspot/jinjava/lib/filter/AbstractSetFilterTest.java diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java index f13a43bf4..24291d462 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.lib.filter; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.fn.TypeFunction; import com.hubspot.jinjava.util.ForLoop; @@ -31,7 +32,41 @@ protected Set objectToSet(Object var) { return result; } - protected String getTypeOfSetElements(Set set) { + protected void attachMismatchedTypesWarning( + JinjavaInterpreter interpreter, + Set varSet, + Set argSet + ) { + boolean hasAtLeastOneSetEmpty = varSet.isEmpty() || argSet.isEmpty(); + if (hasAtLeastOneSetEmpty) { + return; + } + + boolean areMatchedElementTypes = getTypeOfSetElements(varSet) + .equals(getTypeOfSetElements(argSet)); + if (areMatchedElementTypes) { + return; + } + + interpreter.addError( + new TemplateError( + TemplateError.ErrorType.WARNING, + TemplateError.ErrorReason.OTHER, + TemplateError.ErrorItem.FILTER, + String.format( + "Mismatched Types: input set has elements of type '%s' but arg set has elements of type '%s'. Use |map filter to convert sets to the same type for filter to work correctly.", + getTypeOfSetElements(varSet), + getTypeOfSetElements(argSet) + ), + "list", + interpreter.getLineNumber(), + -1, + null + ) + ); + } + + private String getTypeOfSetElements(Set set) { if (set.isEmpty()) { return "null"; } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/DifferenceFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/DifferenceFilter.java index bbedc9c30..01ac0f434 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/DifferenceFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/DifferenceFilter.java @@ -7,6 +7,7 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.util.ArrayList; import java.util.Map; +import java.util.Set; @JinjavaDoc( value = "Returns a list containing elements present in the first list but not the second list", @@ -35,9 +36,12 @@ public Object filter( Object[] args, Map kwargs ) { - return new ArrayList<>( - Sets.difference(objectToSet(var), objectToSet(parseArgs(interpreter, args))) - ); + Set varSet = objectToSet(var); + Set argSet = objectToSet(parseArgs(interpreter, args)); + + attachMismatchedTypesWarning(interpreter, varSet, argSet); + + return new ArrayList<>(Sets.difference(varSet, argSet)); } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java index 0a2930288..6aaa654d5 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java @@ -5,7 +5,6 @@ import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.TemplateError; import java.util.ArrayList; import java.util.Map; import java.util.Set; @@ -42,28 +41,7 @@ public Object filter( Set varSet = objectToSet(var); Set argSet = objectToSet(argObj); - boolean isAtLeastOneSetEmpty = varSet.isEmpty() || argSet.isEmpty(); - boolean areMismatchedElementTypes = !getTypeOfSetElements(varSet) - .equals(getTypeOfSetElements(argSet)); - - if (areMismatchedElementTypes && !isAtLeastOneSetEmpty) { - interpreter.addError( - new TemplateError( - TemplateError.ErrorType.WARNING, - TemplateError.ErrorReason.OTHER, - TemplateError.ErrorItem.FILTER, - String.format( - "Mismatched Types: input set has elements of type '%s' but arg set has elements of type '%s'. Use |map filter to convert sets to the same type for filter to work correctly.", - getTypeOfSetElements(varSet), - getTypeOfSetElements(argSet) - ), - "list", - interpreter.getLineNumber(), - -1, - null - ) - ); - } + attachMismatchedTypesWarning(interpreter, varSet, argSet); return new ArrayList<>(Sets.intersection(varSet, argSet)); } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/SymmetricDifferenceFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/SymmetricDifferenceFilter.java index cd685159c..2da04e20a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/SymmetricDifferenceFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/SymmetricDifferenceFilter.java @@ -7,6 +7,7 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.util.ArrayList; import java.util.Map; +import java.util.Set; @JinjavaDoc( value = "Returns a list containing elements present in only one list.", @@ -35,12 +36,12 @@ public Object filter( Object[] args, Map kwargs ) { - return new ArrayList<>( - Sets.symmetricDifference( - objectToSet(var), - objectToSet(parseArgs(interpreter, args)) - ) - ); + Set varSet = objectToSet(var); + Set argSet = objectToSet(parseArgs(interpreter, args)); + + attachMismatchedTypesWarning(interpreter, varSet, argSet); + + return new ArrayList<>(Sets.symmetricDifference(varSet, argSet)); } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/UnionFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/UnionFilter.java index 73adf565d..3053c88e3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/UnionFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/UnionFilter.java @@ -7,6 +7,7 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.util.ArrayList; import java.util.Map; +import java.util.Set; @JinjavaDoc( value = "Returns a list containing elements present in either list", @@ -35,9 +36,12 @@ public Object filter( Object[] args, Map kwargs ) { - return new ArrayList<>( - Sets.union(objectToSet(var), objectToSet(parseArgs(interpreter, args))) - ); + Set varSet = objectToSet(var); + Set argSet = objectToSet(parseArgs(interpreter, args)); + + attachMismatchedTypesWarning(interpreter, varSet, argSet); + + return new ArrayList<>(Sets.union(varSet, argSet)); } @Override diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/AbstractSetFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/AbstractSetFilterTest.java new file mode 100644 index 000000000..a1543765f --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/AbstractSetFilterTest.java @@ -0,0 +1,82 @@ +package com.hubspot.jinjava.lib.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; +import java.util.List; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; + +public class AbstractSetFilterTest extends BaseJinjavaTest { + private static final IntersectFilter concreteSetFilter = new IntersectFilter(); + + @Before + public void setup() { + jinjava.getGlobalContext().registerClasses(EscapeJsFilter.class); + } + + @Test + public void itDoesNotThrowWarningOnMatchedTypes() { + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + + // {{ [1, 2, 3]|intersect([1, 2, 3]) }} + Set varSet = concreteSetFilter.objectToSet(new Long[] { 1L, 2L, 3L }); + Set argSet = concreteSetFilter.objectToSet(new Long[] { 1L, 2L, 3L }); + concreteSetFilter.attachMismatchedTypesWarning(interpreter, varSet, argSet); + + List errors = interpreter.getErrors(); + assertThat(errors).isEmpty(); + } + + @Test + public void itDoesNotThrowWarningOnEmptyVarSet() { + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + + String renderedOutput = interpreter.render("{{ []|intersect([1, 2, 3]) }}"); + assertThat(renderedOutput).isEqualTo("[]"); + + // {{ []|intersect([1, 2, 3]) }} + Set varSet = concreteSetFilter.objectToSet(new Object[] {}); + Set argSet = concreteSetFilter.objectToSet(new Long[] { 1L, 2L, 3L }); + concreteSetFilter.attachMismatchedTypesWarning(interpreter, varSet, argSet); + + List errors = interpreter.getErrors(); + assertThat(errors).isEmpty(); + } + + @Test + public void itDoesNotThrowWarningOnEmptyArgSet() { + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + + // {{ [1, 2, 3]|intersect([]) }} + Set varSet = concreteSetFilter.objectToSet(new Long[] { 1L, 2L, 3L }); + Set argSet = concreteSetFilter.objectToSet(new Object[] {}); + concreteSetFilter.attachMismatchedTypesWarning(interpreter, varSet, argSet); + + List errors = interpreter.getErrors(); + assertThat(errors).isEmpty(); + } + + @Test + public void itThrowsWarningOnMismatchTypes() { + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + + // {{ [1, 2, 3]|intersect(['1', '2', '3']) }} + Set varSet = concreteSetFilter.objectToSet(new Long[] { 1L, 2L, 3L }); + Set argSet = concreteSetFilter.objectToSet(new String[] { "1", "2", "3" }); + concreteSetFilter.attachMismatchedTypesWarning(interpreter, varSet, argSet); + + List errors = interpreter.getErrors(); + assertThat(errors).isNotEmpty(); + + TemplateError error = errors.get(0); + assertThat(error.getSeverity()).isEqualTo(TemplateError.ErrorType.WARNING); + assertThat(error.getMessage()) + .isEqualTo( + "Mismatched Types: input set has elements of type 'long' but arg set has elements of type 'str'. Use |map filter to convert sets to the same type for filter to work correctly." + ); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java index 1b6743325..2161fe58f 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java @@ -3,10 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hubspot.jinjava.BaseJinjavaTest; -import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.TemplateError; import java.util.HashMap; -import java.util.List; import org.junit.Before; import org.junit.Test; @@ -34,57 +31,4 @@ public void itReturnsEmptyOnNullParameters() { assertThat(jinjava.render("{{ [1, 2, 3]|intersect(null) }}", new HashMap<>())) .isEqualTo("[]"); } - - @Test - public void itDoesNotThrowWarningOnMatchedTypes() { - JinjavaInterpreter interpreter = jinjava.newInterpreter(); - - String renderedOutput = interpreter.render("{{ [1, 2, 3]|intersect([1, 2, 3]) }}"); - assertThat(renderedOutput).isEqualTo("[1, 2, 3]"); - - List errors = interpreter.getErrors(); - assertThat(errors).isEmpty(); - } - - @Test - public void itDoesNotThrowWarningOnEmptyVarSet() { - JinjavaInterpreter interpreter = jinjava.newInterpreter(); - - String renderedOutput = interpreter.render("{{ []|intersect([1, 2, 3]) }}"); - assertThat(renderedOutput).isEqualTo("[]"); - - List errors = interpreter.getErrors(); - assertThat(errors).isEmpty(); - } - - @Test - public void itDoesNotThrowWarningOnEmptyArgSet() { - JinjavaInterpreter interpreter = jinjava.newInterpreter(); - - String renderedOutput = interpreter.render("{{ [1, 2, 3]|intersect([]) }}"); - assertThat(renderedOutput).isEqualTo("[]"); - - List errors = interpreter.getErrors(); - assertThat(errors).isEmpty(); - } - - @Test - public void itThrowsWarningOnMismatchTypes() { - JinjavaInterpreter interpreter = jinjava.newInterpreter(); - - String renderedOutput = interpreter.render( - "{{ [1, 2, 3]|intersect(['1', '2', '3']) }}" - ); - assertThat(renderedOutput).isEqualTo("[]"); - - List errors = interpreter.getErrors(); - assertThat(errors).isNotEmpty(); - - TemplateError error = errors.get(0); - assertThat(error.getSeverity()).isEqualTo(TemplateError.ErrorType.WARNING); - assertThat(error.getMessage()) - .isEqualTo( - "Mismatched Types: input set has elements of type 'long' but arg set has elements of type 'str'. Use |map filter to convert sets to the same type for filter to work correctly." - ); - } } From 9df462a45a3bfe760d92acf194e7ccf0e853805a Mon Sep 17 00:00:00 2001 From: Alpri Else Date: Mon, 3 Oct 2022 12:28:26 -0400 Subject: [PATCH 013/637] Move more shared logic to AbstractSetFilter --- .../jinjava/lib/filter/AbstractSetFilter.java | 18 ++++++++++++++++++ .../jinjava/lib/filter/DifferenceFilter.java | 14 +------------- .../jinjava/lib/filter/IntersectFilter.java | 16 +--------------- .../lib/filter/SymmetricDifferenceFilter.java | 14 +------------- .../jinjava/lib/filter/UnionFilter.java | 14 +------------- 5 files changed, 22 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java index 24291d462..f37bdb2ce 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java @@ -7,6 +7,7 @@ import com.hubspot.jinjava.util.ForLoop; import com.hubspot.jinjava.util.ObjectIterator; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; public abstract class AbstractSetFilter implements AdvancedFilter { @@ -32,6 +33,23 @@ protected Set objectToSet(Object var) { return result; } + @Override + public Object filter( + Object var, + JinjavaInterpreter interpreter, + Object[] args, + Map kwargs + ) { + Set varSet = objectToSet(var); + Set argSet = objectToSet(parseArgs(interpreter, args)); + + attachMismatchedTypesWarning(interpreter, varSet, argSet); + + return filter(varSet, argSet); + } + + public abstract Object filter(Set varSet, Set argSet); + protected void attachMismatchedTypesWarning( JinjavaInterpreter interpreter, Set varSet, diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/DifferenceFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/DifferenceFilter.java index 01ac0f434..46b4c7cab 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/DifferenceFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/DifferenceFilter.java @@ -4,9 +4,7 @@ import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; -import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.util.ArrayList; -import java.util.Map; import java.util.Set; @JinjavaDoc( @@ -30,17 +28,7 @@ public class DifferenceFilter extends AbstractSetFilter { @Override - public Object filter( - Object var, - JinjavaInterpreter interpreter, - Object[] args, - Map kwargs - ) { - Set varSet = objectToSet(var); - Set argSet = objectToSet(parseArgs(interpreter, args)); - - attachMismatchedTypesWarning(interpreter, varSet, argSet); - + public Object filter(Set varSet, Set argSet) { return new ArrayList<>(Sets.difference(varSet, argSet)); } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java index 6aaa654d5..99a54d800 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java @@ -4,9 +4,7 @@ import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; -import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.util.ArrayList; -import java.util.Map; import java.util.Set; @JinjavaDoc( @@ -30,19 +28,7 @@ public class IntersectFilter extends AbstractSetFilter { @Override - public Object filter( - Object var, - JinjavaInterpreter interpreter, - Object[] args, - Map kwargs - ) { - Object argObj = parseArgs(interpreter, args); - - Set varSet = objectToSet(var); - Set argSet = objectToSet(argObj); - - attachMismatchedTypesWarning(interpreter, varSet, argSet); - + public Object filter(Set varSet, Set argSet) { return new ArrayList<>(Sets.intersection(varSet, argSet)); } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/SymmetricDifferenceFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/SymmetricDifferenceFilter.java index 2da04e20a..5f9743024 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/SymmetricDifferenceFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/SymmetricDifferenceFilter.java @@ -4,9 +4,7 @@ import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; -import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.util.ArrayList; -import java.util.Map; import java.util.Set; @JinjavaDoc( @@ -30,17 +28,7 @@ public class SymmetricDifferenceFilter extends AbstractSetFilter { @Override - public Object filter( - Object var, - JinjavaInterpreter interpreter, - Object[] args, - Map kwargs - ) { - Set varSet = objectToSet(var); - Set argSet = objectToSet(parseArgs(interpreter, args)); - - attachMismatchedTypesWarning(interpreter, varSet, argSet); - + public Object filter(Set varSet, Set argSet) { return new ArrayList<>(Sets.symmetricDifference(varSet, argSet)); } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/UnionFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/UnionFilter.java index 3053c88e3..40daf4853 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/UnionFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/UnionFilter.java @@ -4,9 +4,7 @@ import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; -import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.util.ArrayList; -import java.util.Map; import java.util.Set; @JinjavaDoc( @@ -30,17 +28,7 @@ public class UnionFilter extends AbstractSetFilter { @Override - public Object filter( - Object var, - JinjavaInterpreter interpreter, - Object[] args, - Map kwargs - ) { - Set varSet = objectToSet(var); - Set argSet = objectToSet(parseArgs(interpreter, args)); - - attachMismatchedTypesWarning(interpreter, varSet, argSet); - + public Object filter(Set varSet, Set argSet) { return new ArrayList<>(Sets.union(varSet, argSet)); } From 426a7817130254594ef64b3823bde7d8e4d30222 Mon Sep 17 00:00:00 2001 From: Alpri Else Date: Tue, 4 Oct 2022 09:50:14 -0400 Subject: [PATCH 014/637] Rm empty set check in AbstractSetFilter.getTypeOfSetElements --- .../java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java index f37bdb2ce..6d6082f6b 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java @@ -85,9 +85,6 @@ protected void attachMismatchedTypesWarning( } private String getTypeOfSetElements(Set set) { - if (set.isEmpty()) { - return "null"; - } return TypeFunction.type(set.iterator().next()); } } From a7cb415b66b7abff7fc53d89c9047dc6719228c5 Mon Sep 17 00:00:00 2001 From: Alpri Else Date: Tue, 4 Oct 2022 10:50:28 -0400 Subject: [PATCH 015/637] Pass interpreter.getPosition --- .../java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java index 6d6082f6b..48306bacc 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java @@ -78,7 +78,7 @@ protected void attachMismatchedTypesWarning( ), "list", interpreter.getLineNumber(), - -1, + interpreter.getPosition(), null ) ); From 95c4138f7bbeed5841dcf1fa8b7d5388444e9747 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 5 Oct 2022 10:27:36 -0400 Subject: [PATCH 016/637] Store macro function evaluation in temp variable when result has deferred tokens and partial macro evaluation is off --- .../el/ext/eager/EagerAstMacroFunction.java | 7 ++ .../ext/eager/MacroFunctionTempVariable.java | 39 ++++++++++ .../hubspot/jinjava/lib/fn/MacroFunction.java | 24 +++--- .../PyishBlockSetSerializable.java | 5 ++ .../util/EagerReconstructionUtils.java | 78 ++++++++++++++++++- 5 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/hubspot/jinjava/el/ext/eager/MacroFunctionTempVariable.java create mode 100644 src/main/java/com/hubspot/jinjava/objects/serialization/PyishBlockSetSerializable.java diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMacroFunction.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMacroFunction.java index 5dd0cca4d..8b5fc23dd 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMacroFunction.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMacroFunction.java @@ -6,6 +6,7 @@ import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.fn.MacroFunction; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstParameters; import java.lang.reflect.Array; @@ -147,6 +148,12 @@ public String getPartiallyResolved( DeferredParsingException deferredParsingException, boolean preserveIdentifier ) { + if ( + deferredParsingException != null && + deferredParsingException.getSourceNode() instanceof MacroFunction + ) { + return deferredParsingException.getDeferredEvalResult(); + } StringBuilder sb = new StringBuilder(); sb.append(getName()); try { diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/MacroFunctionTempVariable.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/MacroFunctionTempVariable.java new file mode 100644 index 000000000..d65a9eae2 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/MacroFunctionTempVariable.java @@ -0,0 +1,39 @@ +package com.hubspot.jinjava.el.ext.eager; + +import com.hubspot.jinjava.objects.serialization.PyishBlockSetSerializable; +import java.util.Objects; + +public class MacroFunctionTempVariable implements PyishBlockSetSerializable { + private static final String CONTEXT_KEY_PREFIX = "__macro_%s_temp_variable_%d__"; + private final String deferredResult; + + public MacroFunctionTempVariable(String deferredResult) { + this.deferredResult = deferredResult; + } + + public static String getVarName(String macroFunctionName, int callCount) { + return String.format(CONTEXT_KEY_PREFIX, macroFunctionName, callCount); + } + + @Override + public String getBlockSetBody() { + return deferredResult; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MacroFunctionTempVariable that = (MacroFunctionTempVariable) o; + return deferredResult.equals(that.deferredResult); + } + + @Override + public int hashCode() { + return Objects.hash(deferredResult); + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java index 1e291f983..cb07e89c0 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java @@ -1,11 +1,11 @@ package com.hubspot.jinjava.lib.fn; -import com.google.common.collect.ImmutableList; import com.hubspot.jinjava.el.ext.AbstractCallableMethod; +import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.eager.MacroFunctionTempVariable; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; import com.hubspot.jinjava.interpret.DeferredValue; -import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; import com.hubspot.jinjava.tree.Node; @@ -16,6 +16,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; /** * Function definition parsed from a jinjava template, stored in global macros registry in interpreter context. @@ -36,6 +37,8 @@ public class MacroFunction extends AbstractCallableMethod { private boolean deferred; + private AtomicInteger callCount = new AtomicInteger(); + public MacroFunction( List content, String name, @@ -70,6 +73,7 @@ public Object doEvaluate( Map kwargMap, List varArgs ) { + int currentCallCount = callCount.getAndIncrement(); JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); Optional importFile = getImportFile(interpreter); try (InterpreterScopeClosable c = interpreter.enterScope()) { @@ -83,17 +87,15 @@ public Object doEvaluate( !interpreter.getContext().getDeferredTokens().isEmpty() ) ) { - interpreter - .getContext() - .removeDeferredTokens( - ImmutableList.copyOf(interpreter.getContext().getDeferredTokens()) - ); - // If the macro function could not be fully evaluated, throw a DeferredValueException. - throw new DeferredValueException( + String tempVarName = MacroFunctionTempVariable.getVarName( getName(), - interpreter.getLineNumber(), - interpreter.getPosition() + currentCallCount ); + interpreter + .getContext() + .getParent() + .put(tempVarName, new MacroFunctionTempVariable(result)); + throw new DeferredParsingException(this, tempVarName); } return result; diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBlockSetSerializable.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBlockSetSerializable.java new file mode 100644 index 000000000..7cc319a12 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBlockSetSerializable.java @@ -0,0 +1,5 @@ +package com.hubspot.jinjava.objects.serialization; + +public interface PyishBlockSetSerializable { + String getBlockSetBody(); +} diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 86df973ce..c1d4a30b7 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -22,6 +22,7 @@ import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; import com.hubspot.jinjava.objects.collections.PyList; import com.hubspot.jinjava.objects.collections.PyMap; +import com.hubspot.jinjava.objects.serialization.PyishBlockSetSerializable; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; @@ -296,7 +297,8 @@ public static String reconstructFromContextBeforeDeferring( ) { return ( reconstructMacroFunctionsBeforeDeferring(deferredWords, interpreter) + - reconstructVariablesBeforeDeferring(deferredWords, interpreter) + reconstructBlockSetVariablesBeforeDeferring(deferredWords, interpreter) + + reconstructInlineSetVariablesBeforeDeferring(deferredWords, interpreter) ); } @@ -360,7 +362,79 @@ private static String reconstructMacroFunctionsBeforeDeferring( return result; } - private static String reconstructVariablesBeforeDeferring( + private static String reconstructBlockSetVariablesBeforeDeferring( + Set deferredWords, + JinjavaInterpreter interpreter + ) { + if (interpreter.getContext().isDeferredExecutionMode()) { + Context parent = interpreter.getContext().getParent(); + while (parent.isDeferredExecutionMode()) { + parent = parent.getParent(); + } + final Context finalParent = parent; + deferredWords = + deferredWords + .stream() + .filter(word -> interpreter.getContext().get(word) != finalParent.get(word)) + .collect(Collectors.toSet()); + } + if (deferredWords.isEmpty()) { + return ""; + } + Set metaContextVariables = interpreter.getContext().getMetaContextVariables(); + Map blockSetMap = new HashMap<>(); + + deferredWords + .stream() + .map(w -> w.split("\\.", 2)[0]) // get base prop + .filter(w -> !metaContextVariables.contains(w)) + .filter(w -> interpreter.getContext().get(w) instanceof PyishBlockSetSerializable) + .forEach( + w -> + blockSetMap.put(w, (PyishBlockSetSerializable) interpreter.getContext().get(w)) + ); + deferredWords + .stream() + .map(w -> w.split("\\.", 2)[0]) // get base prop + .filter( + w -> { + Object value = interpreter.getContext().get(w); + return ( + value instanceof DeferredLazyReference && + ( + (DeferredLazyReference) value + ).getOriginalValue() instanceof PyishBlockSetSerializable + ); + } + ) + .forEach( + w -> { + blockSetMap.put( + w, + (PyishBlockSetSerializable) ( + (DeferredLazyReference) interpreter.getContext().get(w) + ).getOriginalValue() + ); + } + ); + String blockSetTags = blockSetMap + .entrySet() + .stream() + .map( + entry -> + buildBlockSetTag( + entry.getKey(), + entry.getValue().getBlockSetBody(), + interpreter, + false + ) + ) + .collect(Collectors.joining()); + deferredWords.removeAll(blockSetMap.keySet()); + return blockSetTags; + } + + private static String reconstructInlineSetVariablesBeforeDeferring( Set deferredWords, JinjavaInterpreter interpreter ) { From f838bade3eb30021c61e26756078982f6e09484e Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 5 Oct 2022 10:29:17 -0400 Subject: [PATCH 017/637] Fix expected results to expect the macro function temporary variable --- .../eager/eagerly-defers-macro.expected.jinja | 8 ++++---- .../fully-defers-filtered-macro.expected.jinja | 2 +- ...ferred-for-loop-var-from-macro.expected.jinja | 16 ++++++++-------- ...eferred-fromed-macro-in-output.expected.jinja | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/test/resources/eager/eagerly-defers-macro.expected.jinja b/src/test/resources/eager/eagerly-defers-macro.expected.jinja index cd45e9ec6..bbf836e2d 100644 --- a/src/test/resources/eager/eagerly-defers-macro.expected.jinja +++ b/src/test/resources/eager/eagerly-defers-macro.expected.jinja @@ -1,6 +1,6 @@ -{% macro big_guy() %} +{% set __macro_big_guy_temp_variable_0__ %} {% if deferred %}I am foo{% else %}I am bar{% endif %} -{% endmacro %}{% print big_guy() %} -{% macro big_guy() %} +{% endset %}{% print __macro_big_guy_temp_variable_0__ %} +{% set __macro_big_guy_temp_variable_1__ %} {% if deferred %}No more foo{% else %}I am bar{% endif %} -{% endmacro %}{% print big_guy() %} \ No newline at end of file +{% endset %}{% print __macro_big_guy_temp_variable_1__ %} diff --git a/src/test/resources/eager/fully-defers-filtered-macro.expected.jinja b/src/test/resources/eager/fully-defers-filtered-macro.expected.jinja index 492a80e38..352034b90 100644 --- a/src/test/resources/eager/fully-defers-filtered-macro.expected.jinja +++ b/src/test/resources/eager/fully-defers-filtered-macro.expected.jinja @@ -2,4 +2,4 @@ A flashy {{ deferred }}.{% endmacro %}{{ flashy(flashy('bar')) }} --- -{% macro silly() %}A silly {{ deferred }}.{% endmacro %}{{ filter:upper.filter(silly(), ____int3rpr3t3r____) }} +{% set __macro_silly_temp_variable_0__ %}A silly {{ deferred }}.{% endset %}{{ filter:upper.filter(__macro_silly_temp_variable_0__, ____int3rpr3t3r____) }} diff --git a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja index 22ad3b882..c650a5d45 100644 --- a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja +++ b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja @@ -1,13 +1,13 @@ -{% macro getData() %} +{% set __macro_getData_temp_variable_0__ %} {% for __ignored__ in [0] %} -{% macro doIt(val) %} -{{ deferred ~ filter:tojson.filter(val, ____int3rpr3t3r____) }} -{% endmacro %}{% set val = {'a': 'a'} %}{{ filter:upper.filter(doIt(val), ____int3rpr3t3r____) }} +{% set __macro_doIt_temp_variable_0__ %} +{{ deferred ~ '{\"a\":\"a\"}' }} +{% endset %}{{ filter:upper.filter(__macro_doIt_temp_variable_0__, ____int3rpr3t3r____) }} -{% macro doIt(val) %} -{{ deferred ~ filter:tojson.filter(val, ____int3rpr3t3r____) }} -{% endmacro %}{% set val = {'b': 'b'} %}{{ filter:upper.filter(doIt(val), ____int3rpr3t3r____) }} +{% set __macro_doIt_temp_variable_1__ %} +{{ deferred ~ '{\"b\":\"b\"}' }} +{% endset %}{{ filter:upper.filter(__macro_doIt_temp_variable_1__, ____int3rpr3t3r____) }} {% endfor %} -{% endmacro %}{{ filter:upper.filter(getData(), ____int3rpr3t3r____) }} +{% endset %}{{ filter:upper.filter(__macro_getData_temp_variable_0__, ____int3rpr3t3r____) }} diff --git a/src/test/resources/eager/puts-deferred-fromed-macro-in-output.expected.jinja b/src/test/resources/eager/puts-deferred-fromed-macro-in-output.expected.jinja index 720db4c8c..9241dd338 100644 --- a/src/test/resources/eager/puts-deferred-fromed-macro-in-output.expected.jinja +++ b/src/test/resources/eager/puts-deferred-fromed-macro-in-output.expected.jinja @@ -1 +1 @@ -{% set myname = deferred + 3 %}{% set deferred_import_resource_path = 'simple-with-call.jinja' %}{% macro getPath() %}Hello {{ myname }}{% endmacro %}{% set deferred_import_resource_path = null %}{% print getPath() %} +{% set myname = deferred + 3 %}{% set __macro_getPath_temp_variable_1__ %}Hello {{ myname }}{% endset %}{% print __macro_getPath_temp_variable_1__ %} From 8d0335bec393e1fc2b6b340b8bdc7ec0d5e13934 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 5 Oct 2022 10:54:55 -0400 Subject: [PATCH 018/637] Fix variable referencing so that keys are removed from the original set --- .../hubspot/jinjava/util/EagerReconstructionUtils.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index c1d4a30b7..4f544662c 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -366,25 +366,26 @@ private static String reconstructBlockSetVariablesBeforeDeferring( Set deferredWords, JinjavaInterpreter interpreter ) { + Set filteredDeferredWords = deferredWords; if (interpreter.getContext().isDeferredExecutionMode()) { Context parent = interpreter.getContext().getParent(); while (parent.isDeferredExecutionMode()) { parent = parent.getParent(); } final Context finalParent = parent; - deferredWords = + filteredDeferredWords = deferredWords .stream() .filter(word -> interpreter.getContext().get(word) != finalParent.get(word)) .collect(Collectors.toSet()); } - if (deferredWords.isEmpty()) { + if (filteredDeferredWords.isEmpty()) { return ""; } Set metaContextVariables = interpreter.getContext().getMetaContextVariables(); Map blockSetMap = new HashMap<>(); - deferredWords + filteredDeferredWords .stream() .map(w -> w.split("\\.", 2)[0]) // get base prop .filter(w -> !metaContextVariables.contains(w)) @@ -393,7 +394,7 @@ private static String reconstructBlockSetVariablesBeforeDeferring( w -> blockSetMap.put(w, (PyishBlockSetSerializable) interpreter.getContext().get(w)) ); - deferredWords + filteredDeferredWords .stream() .map(w -> w.split("\\.", 2)[0]) // get base prop .filter( From 2f811c4205bf21673063a720ebcf9229cd57d3a9 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 5 Oct 2022 11:46:42 -0400 Subject: [PATCH 019/637] Don't add expressions to resolved expressions when evaluating chunks in the eager expression resolver --- .../com/hubspot/jinjava/el/ExpressionResolver.java | 8 +++++++- .../hubspot/jinjava/interpret/JinjavaInterpreter.java | 11 +++++++++++ .../hubspot/jinjava/util/EagerExpressionResolver.java | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java b/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java index cbc0fa723..8d724241c 100644 --- a/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java @@ -65,11 +65,17 @@ public ExpressionResolver(JinjavaInterpreter interpreter, Jinjava jinjava) { * @return Value of expression. */ public Object resolveExpression(String expression) { + return resolveExpression(expression, true); + } + + public Object resolveExpression(String expression, boolean addToResolvedExpressions) { if (StringUtils.isBlank(expression)) { return null; } expression = expression.trim(); - interpreter.getContext().addResolvedExpression(expression); + if (addToResolvedExpressions) { + interpreter.getContext().addResolvedExpression(expression); + } if (WhitespaceUtils.isWrappedWith(expression, "[", "]")) { Arrays diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index 564f9bdb8..0a5361f6b 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -600,6 +600,17 @@ public JinjavaConfig getConfig() { return config; } + /** + * Resolve expression against current context, but does not add the expression to the set of resolved expressions. + * + * @param expression + * Jinja expression. + * @return Value of expression. + */ + public Object resolveELExpressionSilently(String expression) { + return expressionResolver.resolveExpression(expression, false); + } + /** * Resolve expression against current context. * diff --git a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java index 2d601be00..19b3288ab 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java @@ -200,7 +200,7 @@ public static boolean shouldBeEvaluated(String w, JinjavaInterpreter interpreter // val is still null } // don't defer numbers, values such as true/false, etc. - return interpreter.resolveELExpression(w, interpreter.getLineNumber()) == null; + return interpreter.resolveELExpressionSilently(w) == null; } catch (ELException | DeferredValueException | TemplateSyntaxException e) { return true; } From 85dfe8b05d38ac6a3120831ccc6631fcf33975b5 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 5 Oct 2022 11:55:44 -0400 Subject: [PATCH 020/637] Check that the expression end token is `}}` before splitting the curly braces --- .../serialization/PyishObjectMapper.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 83b4f558b..6778e9b4f 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -50,14 +50,27 @@ public static String getAsPyishString(Object val) { public static String getAsPyishStringOrThrow(Object val) throws JsonProcessingException { String string = PYISH_OBJECT_WRITER.writeValueAsString(val); - Optional maxStringLength = JinjavaInterpreter - .getCurrentMaybe() + Optional interpreterMaybe = JinjavaInterpreter.getCurrentMaybe(); + Optional maxStringLength = interpreterMaybe .map(interpreter -> interpreter.getConfig().getMaxStringLength()) .filter(max -> max > 0); if (maxStringLength.map(max -> string.length() > max).orElse(false)) { throw new OutputTooBigException(maxStringLength.get(), string.length()); } - if (string.contains("}}") && !string.contains("{{")) { + if ( + interpreterMaybe + .map( + interpreter -> + interpreter + .getConfig() + .getTokenScannerSymbols() + .getExpressionEnd() + .equals("}}") + ) + .orElse(true) && + string.contains("}}") && + !string.contains("{{") + ) { return String.join("} ", string.split("}(?=})")); } return string; From a028eb85321eb7124a90aae46bdab3f2daaa0174 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 6 Oct 2022 11:09:51 -0400 Subject: [PATCH 021/637] Fix tests that fail as a result of multiple merges --- .../com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java | 2 +- .../handles-deferred-for-loop-var-from-macro.expected.jinja | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java index 9c44dcf35..2609fca1b 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java @@ -204,7 +204,7 @@ public void itDoesNotSwallowDeferredValueException() { String input = "{% set my_list = [] %}" + - "{% for i in range(30) %}" + + "{% for i in range(401) %}" + "{{ my_list.append(i) }}" + "{% endfor %}" + "{% for i in [0, 1] %}" + diff --git a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja index c650a5d45..711b57dbe 100644 --- a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja +++ b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja @@ -3,11 +3,11 @@ {% for __ignored__ in [0] %} {% set __macro_doIt_temp_variable_0__ %} -{{ deferred ~ '{\"a\":\"a\"}' }} +{{ deferred ~ '{"a":"a"}' }} {% endset %}{{ filter:upper.filter(__macro_doIt_temp_variable_0__, ____int3rpr3t3r____) }} {% set __macro_doIt_temp_variable_1__ %} -{{ deferred ~ '{\"b\":\"b\"}' }} +{{ deferred ~ '{"b":"b"}' }} {% endset %}{{ filter:upper.filter(__macro_doIt_temp_variable_1__, ____int3rpr3t3r____) }} {% endfor %} {% endset %}{{ filter:upper.filter(__macro_getData_temp_variable_0__, ____int3rpr3t3r____) }} From 2204a48f5b23e7abee5bf2042fc28efcd7dd2935 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 11 Oct 2022 10:51:52 -0400 Subject: [PATCH 022/637] Escape double quotes normally rather than not escaping them This is to maintain functionality before changes to jackson/pyish object mapper --- .../jinjava/objects/serialization/PyishCharacterEscapes.java | 1 - .../handles-deferred-for-loop-var-from-macro.expected.jinja | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishCharacterEscapes.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishCharacterEscapes.java index 60f0dd2a2..494078cf6 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishCharacterEscapes.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishCharacterEscapes.java @@ -12,7 +12,6 @@ public class PyishCharacterEscapes extends CharacterEscapes { private PyishCharacterEscapes() { int[] escapes = CharacterEscapes.standardAsciiEscapesForJSON(); escapes['\n'] = CharacterEscapes.ESCAPE_NONE; - escapes['"'] = CharacterEscapes.ESCAPE_NONE; escapes['\t'] = CharacterEscapes.ESCAPE_NONE; escapes['\r'] = CharacterEscapes.ESCAPE_NONE; escapes['\f'] = CharacterEscapes.ESCAPE_NONE; diff --git a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja index 711b57dbe..c650a5d45 100644 --- a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja +++ b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja @@ -3,11 +3,11 @@ {% for __ignored__ in [0] %} {% set __macro_doIt_temp_variable_0__ %} -{{ deferred ~ '{"a":"a"}' }} +{{ deferred ~ '{\"a\":\"a\"}' }} {% endset %}{{ filter:upper.filter(__macro_doIt_temp_variable_0__, ____int3rpr3t3r____) }} {% set __macro_doIt_temp_variable_1__ %} -{{ deferred ~ '{"b":"b"}' }} +{{ deferred ~ '{\"b\":\"b\"}' }} {% endset %}{{ filter:upper.filter(__macro_doIt_temp_variable_1__, ____int3rpr3t3r____) }} {% endfor %} {% endset %}{{ filter:upper.filter(__macro_getData_temp_variable_0__, ____int3rpr3t3r____) }} From d363372e39441eacf0c5b741346a4ad48ffd0b5e Mon Sep 17 00:00:00 2001 From: rhuddleston Date: Fri, 14 Oct 2022 22:36:18 -0600 Subject: [PATCH 023/637] Whitelist -> Safelist in jsoup Whitelist is officially depricated in jsoup 1.15.1+ now --- .../java/com/hubspot/jinjava/lib/filter/StripTagsFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/StripTagsFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/StripTagsFilter.java index 9beebe359..ebe6e05af 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/StripTagsFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/StripTagsFilter.java @@ -7,7 +7,7 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.util.regex.Pattern; import org.jsoup.Jsoup; -import org.jsoup.safety.Whitelist; +import org.jsoup.safety.Safelist; /** * striptags(value) Strip SGML/XML tags and replace adjacent whitespace by one space. @@ -42,7 +42,7 @@ public Object filter(Object object, JinjavaInterpreter interpreter, String... ar } String cleanedVal = Jsoup.parse(val).text(); - cleanedVal = Jsoup.clean(cleanedVal, Whitelist.none()); + cleanedVal = Jsoup.clean(cleanedVal, Safelist.none()); // backwards compatibility with Jsoup.parse cleanedVal = cleanedVal.replaceAll(" ", " "); From 5edbcc30319221bc8fdcfc08e19b61cf9d5fdc45 Mon Sep 17 00:00:00 2001 From: Jack Smith <72623970+jasmith-hs@users.noreply.github.com> Date: Mon, 17 Oct 2022 14:29:48 -0400 Subject: [PATCH 024/637] Revert "Whitelist -> Safelist in jsoup" --- .../java/com/hubspot/jinjava/lib/filter/StripTagsFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/StripTagsFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/StripTagsFilter.java index ebe6e05af..9beebe359 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/StripTagsFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/StripTagsFilter.java @@ -7,7 +7,7 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.util.regex.Pattern; import org.jsoup.Jsoup; -import org.jsoup.safety.Safelist; +import org.jsoup.safety.Whitelist; /** * striptags(value) Strip SGML/XML tags and replace adjacent whitespace by one space. @@ -42,7 +42,7 @@ public Object filter(Object object, JinjavaInterpreter interpreter, String... ar } String cleanedVal = Jsoup.parse(val).text(); - cleanedVal = Jsoup.clean(cleanedVal, Safelist.none()); + cleanedVal = Jsoup.clean(cleanedVal, Whitelist.none()); // backwards compatibility with Jsoup.parse cleanedVal = cleanedVal.replaceAll(" ", " "); From 42308bb28998358764130b4907bf75505cd8ad70 Mon Sep 17 00:00:00 2001 From: Nathaniel Pautzke Date: Tue, 18 Oct 2022 15:01:30 -0500 Subject: [PATCH 025/637] add proper reconstruction for eager cycle tag when being deferred --- .../jinjava/lib/tag/eager/EagerCycleTag.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) 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 1c72d553f..171960329 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 @@ -13,6 +13,7 @@ import com.hubspot.jinjava.util.WhitespaceUtils; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; public class EagerCycleTag extends EagerStateChangingTag { @@ -112,7 +113,8 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter interpreter, resolvedValues, resolvedExpression, - eagerExecutionResult.getResult().isFullyResolved() + eagerExecutionResult.getResult().isFullyResolved(), + eagerExecutionResult.getResult().getDeferredWords() ) ); } else if (helper.size() == 3) { @@ -174,10 +176,26 @@ private String interpretPrintingCycle( JinjavaInterpreter interpreter, List values, String resolvedExpression, - boolean fullyResolved + boolean fullyResolved, + Set deferredWords ) { if (interpreter.getContext().isDeferredExecutionMode()) { - return reconstructCycleTag(resolvedExpression, tagToken); + String reconstructedTag = reconstructCycleTag(resolvedExpression, tagToken); + + interpreter + .getContext() + .handleDeferredToken( + new DeferredToken( + new TagToken( + reconstructedTag, + tagToken.getLineNumber(), + tagToken.getStartPosition(), + tagToken.getSymbols() + ), + deferredWords + ) + ); + return reconstructedTag; } Integer forindex = (Integer) interpreter.retraceVariable( CycleTag.LOOP_INDEX, From f32e0a6359298b7a9ed19ca3cf623a970af5dceb Mon Sep 17 00:00:00 2001 From: Nathaniel Pautzke Date: Thu, 20 Oct 2022 11:47:07 -0500 Subject: [PATCH 026/637] add unit test for eager cycle tag --- .../jinjava/lib/tag/eager/EagerCycleTagTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java index e24d71f6c..8a8a743a2 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java @@ -1,13 +1,17 @@ package com.hubspot.jinjava.lib.tag.eager; +import static org.assertj.core.api.Java6Assertions.assertThat; + import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; +import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.CycleTagTest; import com.hubspot.jinjava.lib.tag.Tag; import com.hubspot.jinjava.mode.EagerExecutionMode; import org.junit.After; import org.junit.Before; +import org.junit.Test; public class EagerCycleTagTest extends CycleTagTest { private static final long MAX_OUTPUT_SIZE = 500L; @@ -31,6 +35,7 @@ public void eagerSetup() { tag = new EagerCycleTag(); context.registerTag(tag); + context.put("deferred", DeferredValue.instance()); context.registerTag(new EagerForTag()); JinjavaInterpreter.pushCurrent(interpreter); } @@ -39,4 +44,10 @@ public void eagerSetup() { public void teardown() { JinjavaInterpreter.popCurrent(); } + + @Test + public void itHandlesDeferredCycle() { + String template = "{% for item in deferred %}{% cycle {{foo}},{{bar}} %}{% endfor %}"; + assertThat(interpreter.render(template)).isEqualTo(template); + } } From 377e98c59a6610d5fa9f6d9f10aacd72cc019815 Mon Sep 17 00:00:00 2001 From: Nathaniel Pautzke Date: Fri, 21 Oct 2022 11:51:21 -0500 Subject: [PATCH 027/637] modify test to look for deferred token in the context --- .../jinjava/lib/tag/eager/EagerCycleTagTest.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java index 8a8a743a2..d55dae006 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Java6Assertions.assertThat; +import com.hubspot.jinjava.ExpectedNodeInterpreter; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.DeferredValue; @@ -9,6 +10,10 @@ import com.hubspot.jinjava.lib.tag.CycleTagTest; import com.hubspot.jinjava.lib.tag.Tag; import com.hubspot.jinjava.mode.EagerExecutionMode; +import com.hubspot.jinjava.tree.parse.TagToken; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -47,7 +52,16 @@ public void teardown() { @Test public void itHandlesDeferredCycle() { - String template = "{% for item in deferred %}{% cycle {{foo}},{{bar}} %}{% endfor %}"; + String template = + "{% for item in deferred %}{% cycle 'item-1','item-2' %}{% endfor %}"; assertThat(interpreter.render(template)).isEqualTo(template); + Optional maybeDeferredToken = context + .getDeferredTokens() + .stream() + .filter(e -> ((TagToken) e.getToken()).getTagName().equals(tag.getName())) + .findAny(); + assertThat(maybeDeferredToken.isPresent()); + assertThat(maybeDeferredToken.get().getToken().getImage()) + .isEqualTo("{% cycle 'item-1','item-2' %}"); } } From 033f820a7a59d26db7bc0203491296cab56ee2f1 Mon Sep 17 00:00:00 2001 From: Tyler Kindy Date: Mon, 24 Oct 2022 14:46:42 -0400 Subject: [PATCH 028/637] Start hiding DateTimeFormatter patterns behind StrftimeFormatter API (#929) --- .../el/JinjavaInterpreterResolver.java | 22 +--- .../objects/date/StrftimeFormatter.java | 6 +- .../interpret/JinjavaInterpreterTest.java | 110 +++++++++++++++++- .../objects/date/StrftimeFormatterTest.java | 12 +- 4 files changed, 118 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java b/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java index cfc519f67..285043d28 100644 --- a/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java @@ -24,6 +24,7 @@ import com.hubspot.jinjava.objects.collections.SizeLimitingPyList; import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; import com.hubspot.jinjava.objects.date.FormattedDate; +import com.hubspot.jinjava.objects.date.InvalidDateFormatException; import com.hubspot.jinjava.objects.date.PyishDate; import com.hubspot.jinjava.objects.date.StrftimeFormatter; import com.hubspot.jinjava.objects.serialization.PyishSerializable; @@ -31,8 +32,6 @@ import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -380,22 +379,13 @@ private static String formattedDateToString( JinjavaInterpreter interpreter, FormattedDate d ) { - DateTimeFormatter formatter = getFormatter(interpreter, d) - .withLocale(getLocale(interpreter, d)); - return formatter.format(localizeDateTime(interpreter, d.getDate())); - } + ZonedDateTime zonedDateTime = localizeDateTime(interpreter, d.getDate()); + Locale locale = getLocale(interpreter, d); - private static DateTimeFormatter getFormatter( - JinjavaInterpreter interpreter, - FormattedDate d - ) { if (!StringUtils.isBlank(d.getFormat())) { try { - return StrftimeFormatter.formatter( - d.getFormat(), - interpreter.getConfig().getLocale() - ); - } catch (IllegalArgumentException e) { + return StrftimeFormatter.format(zonedDateTime, d.getFormat(), locale); + } catch (InvalidDateFormatException e) { interpreter.addError( new TemplateError( ErrorType.WARNING, @@ -420,7 +410,7 @@ private static DateTimeFormatter getFormatter( } } - return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM); + return StrftimeFormatter.format(zonedDateTime, "medium", locale); } private static Locale getLocale(JinjavaInterpreter interpreter, FormattedDate d) { diff --git a/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java b/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java index c5f070c25..82df71855 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java @@ -113,11 +113,7 @@ public static String toJavaDateTimeFormat(String strftime) { return result.toString(); } - public static DateTimeFormatter formatter(String strftime) { - return formatter(strftime, Locale.ENGLISH); - } - - public static DateTimeFormatter formatter(String strftime, Locale locale) { + private static DateTimeFormatter formatter(String strftime, Locale locale) { DateTimeFormatter fmt; if (strftime == null) { diff --git a/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java b/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java index 7b606d6f1..d7d4bedce 100644 --- a/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java +++ b/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java @@ -13,12 +13,16 @@ import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; import com.hubspot.jinjava.interpret.TemplateError.ErrorType; import com.hubspot.jinjava.mode.PreserveRawExecutionMode; +import com.hubspot.jinjava.objects.date.FormattedDate; +import com.hubspot.jinjava.objects.date.StrftimeFormatter; import com.hubspot.jinjava.tree.TextNode; import com.hubspot.jinjava.tree.output.BlockInfo; import com.hubspot.jinjava.tree.parse.TextToken; import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.HashMap; +import java.util.Locale; import java.util.Optional; import org.junit.Before; import org.junit.Test; @@ -30,7 +34,10 @@ public class JinjavaInterpreterTest { @Before public void setup() { - jinjava = new Jinjava(); + jinjava = + new Jinjava( + JinjavaConfig.newBuilder().withTimeZone(ZoneId.of("America/New_York")).build() + ); interpreter = jinjava.newInterpreter(); symbols = interpreter.getConfig().getTokenScannerSymbols(); } @@ -353,4 +360,105 @@ public void itInterpretsWhitespaceControl() { public void itInterpretsEmptyExpressions() { assertThat(interpreter.render("{{}}")).isEqualTo(""); } + + @Test + public void itInterpretsFormattedDates() { + String result = jinjava.render( + "{{ d }}", + ImmutableMap.of( + "d", + new FormattedDate( + "medium", + "en-US", + ZonedDateTime.of(2022, 10, 20, 17, 9, 43, 0, ZoneId.of("America/New_York")) + ) + ) + ); + + assertThat(result).isEqualTo("Oct 20, 2022, 5:09:43 PM"); + } + + @Test + public void itHandlesInvalidFormatInFormattedDate() { + RenderResult result = jinjava.renderForResult( + "{{ d }}", + ImmutableMap.of( + "d", + new FormattedDate( + "not a real format", + "en_US", + ZonedDateTime.of(2022, 10, 20, 17, 9, 43, 0, ZoneId.of("America/New_York")) + ) + ) + ); + + assertThat(result.getErrors()) + .extracting(TemplateError::getMessage) + .containsOnly("Invalid date format: [not a real format]"); + } + + @Test + public void itDefaultsToMediumOnEmptyFormatInFormattedDate() { + ZonedDateTime date = ZonedDateTime.of( + 2022, + 10, + 20, + 17, + 9, + 43, + 0, + ZoneId.of("America/New_York") + ); + String result = jinjava.render( + "{{ d }}", + ImmutableMap.of("d", new FormattedDate("", "en_US", date)) + ); + + assertThat(result) + .isEqualTo( + StrftimeFormatter.format(date, "medium", Locale.forLanguageTag("en-US")) + ); + } + + @Test + public void itHandlesInvalidLocaleInFormattedDate() { + RenderResult result = jinjava.renderForResult( + "{{ d }}", + ImmutableMap.of( + "d", + new FormattedDate( + "medium", + "not a real locale", + ZonedDateTime.of(2022, 10, 20, 17, 9, 43, 0, ZoneId.of("America/New_York")) + ) + ) + ); + + assertThat(result.getErrors()) + .extracting(TemplateError::getMessage) + .containsOnly("Invalid locale format: not a real locale"); + } + + @Test + public void itDefaultsToUnitedStatesOnEmptyLocaleInFormattedDate() { + ZonedDateTime date = ZonedDateTime.of( + 2022, + 10, + 20, + 17, + 9, + 43, + 0, + ZoneId.of("America/New_York") + ); + String result = jinjava.render( + "{{ d }}", + ImmutableMap.of("d", new FormattedDate("medium", "", date)) + ); + + assertThat(result) + .isEqualTo( + StrftimeFormatter.format(date, "medium", Locale.forLanguageTag("en-US")) + ); + } } diff --git a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java index 34facd7be..263afe73c 100644 --- a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java @@ -92,12 +92,7 @@ public void testPaddedMinFmt() { @Test public void testFinnishMonths() { - assertThat( - StrftimeFormatter - .formatter("long") - .withLocale(Locale.forLanguageTag("fi")) - .format(d) - ) + assertThat(StrftimeFormatter.format(d, "long", Locale.forLanguageTag("fi"))) .startsWith("6. marraskuuta 2013 klo 14.22.00"); } @@ -118,10 +113,7 @@ public void itConvertsNominativeFormats() { ZonedDateTime zonedDateTime = ZonedDateTime.parse("2019-06-06T14:22:00.000+00:00"); assertThat( - StrftimeFormatter - .formatter("%OB") - .withLocale(Locale.forLanguageTag("ru")) - .format(zonedDateTime) + StrftimeFormatter.format(zonedDateTime, "%OB", Locale.forLanguageTag("ru")) ) .isIn("Июнь", "июнь"); } From 7ba2d6b758be7d25d05fd0f4f67e11feef1af7c9 Mon Sep 17 00:00:00 2001 From: Tyler Kindy Date: Mon, 24 Oct 2022 14:48:59 -0400 Subject: [PATCH 029/637] Improve date format tests (#930) --- .../lib/filter/DateTimeFormatFilterTest.java | 26 ++++++++++ .../lib/fn/StringToTimeFunctionTest.java | 51 ++++++++++--------- .../objects/date/StrftimeFormatterTest.java | 11 ++++ 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java index 59a99e5c6..9370e7ca1 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java @@ -2,8 +2,12 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.BaseInterpretingTest; import com.hubspot.jinjava.interpret.InvalidArgumentException; +import com.hubspot.jinjava.interpret.RenderResult; +import com.hubspot.jinjava.interpret.TemplateError; +import com.hubspot.jinjava.interpret.TemplateError.ErrorType; import com.hubspot.jinjava.lib.fn.Functions; import com.hubspot.jinjava.objects.date.InvalidDateFormatException; import com.hubspot.jinjava.objects.date.StrftimeFormatter; @@ -124,4 +128,26 @@ public void itDefaultsToUtcForNullTimezone() { ) .isEqualTo("onsdag, 6 november, 02:22 em"); } + + @Test + public void itHandlesInvalidDateFormats() { + RenderResult result = jinjava.renderForResult( + "{{ d | datetimeformat('%é') }}", + ImmutableMap.of("d", d) + ); + + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + + TemplateError error = result.getErrors().get(0); + assertThat(error.getSeverity()).isEqualTo(ErrorType.FATAL); + assertThat(error.getMessage()).contains("Invalid date format: [%é]"); + + /* + datetimeformat outputs the string "null" for unrecognized format codes, + which DateTimeFormatter then tries to interpret as a pattern. 'n' and 'u' + are valid pattern letters, but 'l' is not, hence the following error message. + */ + assertThat(error.getMessage()).contains("Unknown pattern letter: l"); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/fn/StringToTimeFunctionTest.java b/src/test/java/com/hubspot/jinjava/lib/fn/StringToTimeFunctionTest.java index d6500fc81..36c6e66bf 100644 --- a/src/test/java/com/hubspot/jinjava/lib/fn/StringToTimeFunctionTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/fn/StringToTimeFunctionTest.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.lib.fn; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.objects.date.PyishDate; @@ -12,44 +13,46 @@ public class StringToTimeFunctionTest { @Test public void itConvertsStringToTime() { - String datetime = "2018-07-14T14:31:30+0530"; - String format = "yyyy-MM-dd'T'HH:mm:ssZ"; PyishDate expected = new PyishDate( ZonedDateTime.of(2018, 7, 14, 14, 31, 30, 0, ZoneOffset.ofHoursMinutes(5, 30)) ); - assertThat(Functions.stringToTime(datetime, format)).isEqualTo(expected); + assertThat( + Functions.stringToTime("2018-07-14T14:31:30+0530", "yyyy-MM-dd'T'HH:mm:ssZ") + ) + .isEqualTo(expected); } - @Test(expected = InterpretException.class) + @Test public void itFailsOnInvalidFormat() { - String datetime = "2018-07-14T14:31:30+0530"; - String format = "not a time format"; - PyishDate expected = new PyishDate( - ZonedDateTime.of(2018, 7, 14, 14, 31, 30, 0, ZoneOffset.ofHoursMinutes(5, 30)) - ); - assertThat(Functions.stringToTime(datetime, format)).isEqualTo(expected); + assertThatExceptionOfType(InterpretException.class) + .isThrownBy( + () -> Functions.stringToTime("2018-07-14T14:31:30+0530", "not a time format") + ) + .withMessageContaining("requires valid datetime format"); } - @Test(expected = InterpretException.class) + @Test public void itFailsOnTimeFormatMismatch() { - String datetime = "Saturday, Jul 14, 2018 14:31:06 PM"; - String format = "yyyy-MM-dd'T'HH:mm:ssZ"; - PyishDate expected = new PyishDate( - ZonedDateTime.of(2018, 7, 14, 14, 31, 30, 0, ZoneOffset.ofHoursMinutes(5, 30)) - ); - assertThat(Functions.stringToTime(datetime, format)).isEqualTo(expected); + assertThatExceptionOfType(InterpretException.class) + .isThrownBy( + () -> + Functions.stringToTime( + "Saturday, Jul 14, 2018 14:31:06 PM", + "yyyy-MM-dd'T'HH:mm:ssZ" + ) + ) + .withMessageContaining("could not match datetime input"); } + @Test public void itReturnsNullOnNullInput() { - String datetime = null; - String format = "yyyy-MM-dd'T'HH:mm:ssZ"; - assertThat(Functions.stringToTime(datetime, format)).isEqualTo(null); + assertThat(Functions.stringToTime(null, "yyyy-MM-dd'T'HH:mm:ssZ")).isEqualTo(null); } - @Test(expected = InterpretException.class) + @Test public void itFailsOnNullDatetimeFormat() { - String datetime = "2018-07-14T14:31:30+0530"; - String format = null; - assertThat(Functions.stringToTime(datetime, format)).isEqualTo(null); + assertThatExceptionOfType(InterpretException.class) + .isThrownBy(() -> Functions.stringToTime("2018-07-14T14:31:30+0530", null)) + .withMessageContaining("requires non-null datetime format"); } } diff --git a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java index 263afe73c..35bea0916 100644 --- a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java @@ -129,4 +129,15 @@ public void testJavaFormatWithGT255Char() { assertThat(StrftimeFormatter.toJavaDateTimeFormat("%d.%ğ.%Y")) .isEqualTo("dd.null.yyyy"); } + + @Test + public void itOutputsLiteralPercents() { + assertThat(StrftimeFormatter.format(d, "hi %% there")).isEqualTo("hi % there"); + assertThat(StrftimeFormatter.format(d, "%%")).isEqualTo("%"); + } + + @Test + public void itIgnoresFinalStandalonePercent() { + assertThat(StrftimeFormatter.format(d, "%")).isEqualTo("%"); + } } From abd34b3f85245aa8cee159e39a7042446f8a2f8a Mon Sep 17 00:00:00 2001 From: Libo Song Date: Mon, 24 Oct 2022 17:18:34 -0400 Subject: [PATCH 030/637] Use instead of --- .../jinjava/lib/filter/EscapeFilter.java | 7 + .../jinjava/lib/filter/EscapeFilterTest.java | 28 + src/test/resources/filter/blog.html | 2597 +++++++++++++++++ 3 files changed, 2632 insertions(+) create mode 100644 src/test/resources/filter/blog.html diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/EscapeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/EscapeFilter.java index 854efa527..76bb539ed 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/EscapeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/EscapeFilter.java @@ -48,6 +48,13 @@ public class EscapeFilter implements Filter { private static final String[] REPLACE_WITH = new String[] { BAMP, BGT, BLT, BSQ, BDQ }; public static String escapeHtmlEntities(String input) { + for (int idx = 0; idx < TO_REPLACE.length; ++idx) { + input = input.replace(TO_REPLACE[idx], REPLACE_WITH[idx]); + } + return input; + } + + public static String oldEscapeHtmlEntities(String input) { return StringUtils.replaceEach(input, TO_REPLACE, REPLACE_WITH); } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java index 09028c62d..cf049fc67 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java @@ -2,8 +2,13 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.google.common.base.Stopwatch; +import com.google.common.io.Resources; import com.hubspot.jinjava.BaseInterpretingTest; import com.hubspot.jinjava.objects.SafeString; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import org.junit.Before; import org.junit.Test; @@ -35,4 +40,27 @@ public void testSafeStringCanBeEscaped() { assertThat(f.filter(new SafeString("Previously marked as safe"), interpreter)) .isInstanceOf(SafeString.class); } + + @Test + public void testNewStringReplaceIsFaster() { + String html = fixture("filter/blog.html").substring(0, 100_000); + Stopwatch oldStopWatch = Stopwatch.createStarted(); + String oldResult = EscapeFilter.oldEscapeHtmlEntities(html); + Duration oldTime = oldStopWatch.elapsed(); + + Stopwatch newStopWatch = Stopwatch.createStarted(); + String newResult = EscapeFilter.escapeHtmlEntities(html); + Duration newTime = newStopWatch.elapsed(); + + assertThat(newResult).isEqualTo(oldResult); + assertThat(newTime.toMillis()).isLessThan(oldTime.toMillis() / 50); + } + + private static String fixture(String name) { + try { + return Resources.toString(Resources.getResource(name), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/test/resources/filter/blog.html b/src/test/resources/filter/blog.html new file mode 100644 index 000000000..86cf073d7 --- /dev/null +++ b/src/test/resources/filter/blog.html @@ -0,0 +1,2597 @@ + + + Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+ + + +
+ +
+ +
+ + + +
+ + + + + +

Stratsys Blog

+ + + + + + + + + + +
+
Why start from a blank sheet? Our experience within both public and private sector has provided a range of out of the lorem ipsum...
+
+ + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ +
+
+
+
+
+ + +
+ + + + + +
+ + + + +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+
+
+ +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + \ No newline at end of file From f8c7262ec221154534982ac863a146dc5152d676 Mon Sep 17 00:00:00 2001 From: Libo Song Date: Mon, 24 Oct 2022 18:06:22 -0400 Subject: [PATCH 031/637] Update test -- lower the speed-up factor. --- .../java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java index cf049fc67..226f3cfae 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java @@ -53,7 +53,9 @@ public void testNewStringReplaceIsFaster() { Duration newTime = newStopWatch.elapsed(); assertThat(newResult).isEqualTo(oldResult); - assertThat(newTime.toMillis()).isLessThan(oldTime.toMillis() / 50); + System.out.printf("New: %d Old:%d\n", newTime.toMillis(), oldTime.toMillis()); + int speedUpFactor = 2; // On M1, it is between 50 and 100 times faster. + assertThat(newTime.toMillis()).isLessThan(oldTime.toMillis() / speedUpFactor); } private static String fixture(String name) { From e3591556357c93d94abc9259c12a5bd6c45a9c90 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 25 Oct 2022 10:12:21 -0400 Subject: [PATCH 032/637] Use DeferredMacroValueImpl here, to match how it's used elsewhere --- .../hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java | 5 +++-- .../hubspot/jinjava/lib/tag/eager/EagerIfTagTest.java | 9 ++++++--- .../jinjava/lib/tag/eager/EagerUnlessTagTest.java | 3 ++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java index 3fa3628cc..cea34a9fc 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java @@ -1,7 +1,7 @@ package com.hubspot.jinjava.lib.tag.eager; import com.hubspot.jinjava.el.ext.DeferredParsingException; -import com.hubspot.jinjava.interpret.DeferredValue; +import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; @@ -222,7 +222,8 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter .getDeferredWords() .stream() .filter( - word -> !(interpreter.getContext().get(word) instanceof DeferredValue) + word -> + !(interpreter.getContext().get(word) instanceof DeferredMacroValueImpl) ) .collect(Collectors.toSet()) ) diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTagTest.java index a9a63c872..4ced5d48e 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTagTest.java @@ -66,7 +66,8 @@ public void itHandlesDeferredInEager() { .findAny(); assertThat(maybeEagerTagToken).isPresent(); assertThat(maybeEagerTagToken.get().getSetDeferredWords()).isEmpty(); - assertThat(maybeEagerTagToken.get().getUsedDeferredWords()).isEmpty(); + assertThat(maybeEagerTagToken.get().getUsedDeferredWords()) + .containsExactly("deferred"); } @Test @@ -81,7 +82,8 @@ public void itHandlesOnlyDeferredElif() { .findAny(); assertThat(maybeEagerTagToken).isPresent(); assertThat(maybeEagerTagToken.get().getSetDeferredWords()).isEmpty(); - assertThat(maybeEagerTagToken.get().getUsedDeferredWords()).isEmpty(); + assertThat(maybeEagerTagToken.get().getUsedDeferredWords()) + .containsExactly("deferred"); } @Test @@ -96,6 +98,7 @@ public void itRemovesImpossibleIfBlocks() { .findAny(); assertThat(maybeEagerTagToken).isPresent(); assertThat(maybeEagerTagToken.get().getSetDeferredWords()).isEmpty(); - assertThat(maybeEagerTagToken.get().getUsedDeferredWords()).isEmpty(); + assertThat(maybeEagerTagToken.get().getUsedDeferredWords()) + .containsExactly("deferred"); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerUnlessTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerUnlessTagTest.java index d800ea898..9e87ee14d 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerUnlessTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerUnlessTagTest.java @@ -66,6 +66,7 @@ public void itHandlesDeferredInEager() { .findAny(); assertThat(maybeEagerTagToken).isPresent(); assertThat(maybeEagerTagToken.get().getSetDeferredWords()).isEmpty(); - assertThat(maybeEagerTagToken.get().getUsedDeferredWords()).isEmpty(); + assertThat(maybeEagerTagToken.get().getUsedDeferredWords()) + .containsExactly("deferred"); } } From e1bac10fc258980252341fe59eaeb2fb686d87ca Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Wed, 26 Oct 2022 16:17:43 +0800 Subject: [PATCH 033/637] add keys function to dictionary --- .../java/com/hubspot/jinjava/objects/collections/PyMap.java | 4 ++++ src/test/java/com/hubspot/jinjava/el/ext/AstDictTest.java | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/main/java/com/hubspot/jinjava/objects/collections/PyMap.java b/src/main/java/com/hubspot/jinjava/objects/collections/PyMap.java index 280b14e66..05c715831 100644 --- a/src/main/java/com/hubspot/jinjava/objects/collections/PyMap.java +++ b/src/main/java/com/hubspot/jinjava/objects/collections/PyMap.java @@ -38,6 +38,10 @@ public Set> items() { return entrySet(); } + public Set keys() { + return keySet(); + } + public void update(Map m) { if (m == this) { throw new IllegalArgumentException("Can't update map object with itself"); diff --git a/src/test/java/com/hubspot/jinjava/el/ext/AstDictTest.java b/src/test/java/com/hubspot/jinjava/el/ext/AstDictTest.java index 7e60db6b5..a225ea956 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/AstDictTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/AstDictTest.java @@ -44,6 +44,12 @@ public void itDoesItemsMethodCall() { .isInstanceOf(Set.class); } + @Test + public void itDoesKeysMethodCall() { + interpreter.getContext().put("foo", ImmutableMap.of(TestEnum.BAR, "test")); + assertThat(interpreter.resolveELExpression("foo.keys()", -1)).isInstanceOf(Set.class); + } + @Test public void itHandlesEmptyMaps() { interpreter.getContext().put("foo", ImmutableMap.of()); From 414a82761ed53e254bce218de027642752b60579 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 26 Oct 2022 10:52:35 -0400 Subject: [PATCH 034/637] Use getPrefixToPreserveState in EagerForTag to share common logic --- .../lib/tag/eager/EagerExecutionResult.java | 19 +++++++++++-------- .../jinjava/lib/tag/eager/EagerForTag.java | 16 +--------------- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java index 1da827d0d..8668f03f6 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java @@ -39,6 +39,15 @@ public Map getSpeculativeBindings() { } public String getPrefixToPreserveState() { + return getPrefixToPreserveState( + !JinjavaInterpreter + .getCurrentMaybe() + .map(interpreter -> interpreter.getContext().isDeferredExecutionMode()) + .orElse(false) + ); + } + + public String getPrefixToPreserveState(boolean registerDeferredToken) { if (prefixToPreserveState != null) { return prefixToPreserveState; } @@ -55,10 +64,7 @@ public String getPrefixToPreserveState() { ) ), JinjavaInterpreter.getCurrent(), - !JinjavaInterpreter - .getCurrentMaybe() - .map(interpreter -> interpreter.getContext().isDeferredExecutionMode()) - .orElse(false) + registerDeferredToken ) + speculativeBindings .entrySet() @@ -77,10 +83,7 @@ public String getPrefixToPreserveState() { buildSetTag( Collections.singletonMap(pair.getKey(), pair.getValue()), JinjavaInterpreter.getCurrent(), - !JinjavaInterpreter - .getCurrentMaybe() - .map(interpreter -> interpreter.getContext().isDeferredExecutionMode()) - .orElse(false) + registerDeferredToken ) ) .collect(Collectors.joining()); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index fd89a9743..4a0775875 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -138,21 +138,7 @@ public String eagerInterpret( EagerExecutionResult eagerExecutionResult = runLoopOnce(tagNode, interpreter); if (!eagerExecutionResult.getSpeculativeBindings().isEmpty()) { // Defer any variables that we tried to modify during the loop - prefix = - EagerReconstructionUtils.buildSetTag( - eagerExecutionResult - .getSpeculativeBindings() - .entrySet() - .stream() - .collect( - Collectors.toMap( - Entry::getKey, - entry -> PyishObjectMapper.getAsPyishString(entry.getValue()) - ) - ), - interpreter, - true - ); + prefix = eagerExecutionResult.getPrefixToPreserveState(true); } // Run for loop again now that the necessary values have been deferred eagerExecutionResult = runLoopOnce(tagNode, interpreter); From c2f11914cd240564bb7ddb3556c02b32576ca0d1 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 26 Oct 2022 11:09:28 -0400 Subject: [PATCH 035/637] Properly reconstruct block set serializable variables when getting prefix to preserve state --- .../lib/tag/eager/EagerExecutionResult.java | 22 +++++++++++++++++-- .../jinjava/lib/tag/eager/EagerForTag.java | 2 -- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java index 8668f03f6..698543906 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java @@ -1,9 +1,11 @@ package com.hubspot.jinjava.lib.tag.eager; +import static com.hubspot.jinjava.util.EagerReconstructionUtils.buildBlockSetTag; import static com.hubspot.jinjava.util.EagerReconstructionUtils.buildSetTag; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.LazyReference; +import com.hubspot.jinjava.objects.serialization.PyishBlockSetSerializable; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import java.util.Collections; @@ -51,11 +53,27 @@ public String getPrefixToPreserveState(boolean registerDeferredToken) { if (prefixToPreserveState != null) { return prefixToPreserveState; } + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); prefixToPreserveState = + speculativeBindings + .entrySet() + .stream() + .filter(entry -> entry.getValue() instanceof PyishBlockSetSerializable) + .map( + entry -> + buildBlockSetTag( + entry.getKey(), + ((PyishBlockSetSerializable) entry.getValue()).getBlockSetBody(), + interpreter, + registerDeferredToken + ) + ) + .collect(Collectors.joining()) + buildSetTag( speculativeBindings .entrySet() .stream() + .filter(entry -> !(entry.getValue() instanceof PyishBlockSetSerializable)) .filter(entry -> !(entry.getValue() instanceof LazyReference)) .collect( Collectors.toMap( @@ -63,7 +81,7 @@ public String getPrefixToPreserveState(boolean registerDeferredToken) { entry -> PyishObjectMapper.getAsPyishString(entry.getValue()) ) ), - JinjavaInterpreter.getCurrent(), + interpreter, registerDeferredToken ) + speculativeBindings @@ -82,7 +100,7 @@ public String getPrefixToPreserveState(boolean registerDeferredToken) { pair -> buildSetTag( Collections.singletonMap(pair.getKey(), pair.getValue()), - JinjavaInterpreter.getCurrent(), + interpreter, registerDeferredToken ) ) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 4a0775875..3b2e127f8 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -11,7 +11,6 @@ import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.tag.ForTag; -import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.util.EagerExpressionResolver; @@ -23,7 +22,6 @@ import com.hubspot.jinjava.util.LengthLimitingStringJoiner; import java.util.HashSet; import java.util.List; -import java.util.Map.Entry; import java.util.Set; import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.Pair; From ed842d9c8f18e4f42375e26ee0c6e7a8bc2e366e Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 26 Oct 2022 11:09:54 -0400 Subject: [PATCH 036/637] Add test for reconstructing block set variables in for loop --- src/test/java/com/hubspot/jinjava/EagerTest.java | 7 +++++++ ...nstructs-block-set-variables-in-for-loop.expected.jinja | 5 +++++ .../reconstructs-block-set-variables-in-for-loop.jinja | 6 ++++++ 3 files changed, 18 insertions(+) create mode 100644 src/test/resources/eager/reconstructs-block-set-variables-in-for-loop.expected.jinja create mode 100644 src/test/resources/eager/reconstructs-block-set-variables-in-for-loop.jinja diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index bbd80ec9d..a9d71a953 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1167,4 +1167,11 @@ public void itHandlesDeferredForLoopVarFromMacroSecondPass() { "handles-deferred-for-loop-var-from-macro.expected" ); } + + @Test + public void itReconstructsBlockSetVariablesInForLoop() { + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-block-set-variables-in-for-loop" + ); + } } diff --git a/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop.expected.jinja b/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop.expected.jinja new file mode 100644 index 000000000..331e4f754 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop.expected.jinja @@ -0,0 +1,5 @@ +{% for i in range(deferred) %} +{% set __macro_foo_temp_variable_0__ %} +{{ deferred }} +{% endset %}{{ filter:int.filter(__macro_foo_temp_variable_0__, ____int3rpr3t3r____) + 3 }} +{% endfor %} diff --git a/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop.jinja b/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop.jinja new file mode 100644 index 000000000..443054813 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop.jinja @@ -0,0 +1,6 @@ +{% for i in range(deferred) %} +{%- macro foo() %} +{{ deferred }} +{% endmacro %} +{{ foo()|int + 3 }} +{% endfor %} From accafe5559dcb482a62d970a225b85e6fea3b6ee Mon Sep 17 00:00:00 2001 From: Tyler Kindy Date: Fri, 28 Oct 2022 09:10:11 -0400 Subject: [PATCH 037/637] Refactor StrftimeFormatter conversion logic (#931) --- .../lib/filter/DateTimeFormatFilter.java | 6 +- .../com/hubspot/jinjava/lib/fn/Functions.java | 10 +- .../date/InvalidDateFormatException.java | 13 +- .../objects/date/StrftimeFormatter.java | 171 ++++++++++-------- .../interpret/JinjavaInterpreterTest.java | 2 +- .../lib/filter/DateTimeFormatFilterTest.java | 34 +++- .../objects/date/StrftimeFormatterTest.java | 22 ++- 7 files changed, 152 insertions(+), 106 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilter.java index 7aa50f664..39f230a9b 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilter.java @@ -49,10 +49,6 @@ public String getName() { @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - if (args.length > 0) { - return Functions.dateTimeFormat(var, args); - } else { - return Functions.dateTimeFormat(var); - } + return Functions.dateTimeFormat(var, args); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java b/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java index ef7eb835e..94c2b2c07 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java @@ -23,7 +23,6 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; @@ -311,9 +310,11 @@ public static PyishDate stringToTime(String datetimeString, String datetimeForma } try { - String convertedFormat = StrftimeFormatter.toJavaDateTimeFormat(datetimeFormat); return new PyishDate( - ZonedDateTime.parse(datetimeString, DateTimeFormatter.ofPattern(convertedFormat)) + ZonedDateTime.parse( + datetimeString, + StrftimeFormatter.toDateTimeFormatter(datetimeFormat) + ) ); } catch (DateTimeParseException e) { throw new InterpretException( @@ -358,10 +359,9 @@ public static PyishDate stringToDate(String dateString, String dateFormat) { } try { - String convertedFormat = StrftimeFormatter.toJavaDateTimeFormat(dateFormat); return new PyishDate( LocalDate - .parse(dateString, DateTimeFormatter.ofPattern(convertedFormat)) + .parse(dateString, StrftimeFormatter.toDateTimeFormatter(dateFormat)) .atTime(0, 0) .toInstant(ZoneOffset.UTC) ); diff --git a/src/main/java/com/hubspot/jinjava/objects/date/InvalidDateFormatException.java b/src/main/java/com/hubspot/jinjava/objects/date/InvalidDateFormatException.java index 92711d452..a8e03b146 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/InvalidDateFormatException.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/InvalidDateFormatException.java @@ -5,11 +5,20 @@ public class InvalidDateFormatException extends IllegalArgumentException { private final String format; - public InvalidDateFormatException(String format, Throwable t) { - super("Invalid date format: [" + format + "]", t); + public InvalidDateFormatException(String format, Throwable cause) { + super(buildMessage(format), cause); this.format = format; } + public InvalidDateFormatException(String format, String reason) { + super(buildMessage(format) + ": " + reason); + this.format = format; + } + + private static String buildMessage(String format) { + return "Invalid date format '" + format + "'"; + } + public String getFormat() { return format; } diff --git a/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java b/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java index 82df71855..7dcd67949 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java @@ -1,9 +1,15 @@ package com.hubspot.jinjava.objects.date; +import static com.hubspot.jinjava.objects.date.StrftimeFormatter.ConversionComponent.pattern; + +import com.google.common.collect.ImmutableMap; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.time.format.FormatStyle; import java.util.Locale; +import java.util.Map; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; /** @@ -16,101 +22,98 @@ public class StrftimeFormatter { /* * Mapped from http://strftime.org/, http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html */ - private static final String[] CONVERSIONS = new String[255]; - private static final String[] NOMINATIVE_CONVERSIONS = new String[255]; + private static final Map COMPONENTS; + private static final Map NOMINATIVE_COMPONENTS; static { - CONVERSIONS['a'] = "EEE"; - CONVERSIONS['A'] = "EEEE"; - CONVERSIONS['b'] = "MMM"; - CONVERSIONS['B'] = "MMMM"; - CONVERSIONS['c'] = "EEE MMM dd HH:mm:ss yyyy"; - CONVERSIONS['d'] = "dd"; - CONVERSIONS['e'] = "d"; // The day of the month like with %d, but padded with blank (range 1 through 31). - CONVERSIONS['f'] = "SSSSSS"; - CONVERSIONS['H'] = "HH"; - CONVERSIONS['h'] = "hh"; - CONVERSIONS['I'] = "hh"; - CONVERSIONS['j'] = "DDD"; - CONVERSIONS['k'] = "H"; // The hour as a decimal number, using a 24-hour clock like %H, but padded with blank (range 0 through 23). - CONVERSIONS['l'] = "h"; // The hour as a decimal number, using a 12-hour clock like %I, but padded with blank (range 1 through 12). - CONVERSIONS['m'] = "MM"; - CONVERSIONS['M'] = "mm"; - CONVERSIONS['p'] = "a"; - CONVERSIONS['S'] = "ss"; - CONVERSIONS['U'] = "ww"; - CONVERSIONS['w'] = "e"; - CONVERSIONS['W'] = "ww"; - CONVERSIONS['x'] = "MM/dd/yy"; - CONVERSIONS['X'] = "HH:mm:ss"; - CONVERSIONS['y'] = "yy"; - CONVERSIONS['Y'] = "yyyy"; - CONVERSIONS['z'] = "Z"; - CONVERSIONS['Z'] = "z"; - CONVERSIONS['%'] = "%"; - - NOMINATIVE_CONVERSIONS['B'] = "LLLL"; + COMPONENTS = + ImmutableMap + .builder() + .put('a', pattern("EEE")) + .put('A', pattern("EEEE")) + .put('b', pattern("MMM")) + .put('B', pattern("MMMM")) + .put('c', pattern("EEE MMM dd HH:mm:ss yyyy")) + .put('d', pattern("dd")) + .put('e', pattern("d")) // The day of the month like with %d, but padded with blank (range 1 through 31). + .put('f', pattern("SSSSSS")) + .put('H', pattern("HH")) + .put('h', pattern("hh")) + .put('I', pattern("hh")) + .put('j', pattern("DDD")) + .put('k', pattern("H")) // The hour as a decimal number, using a 24-hour clock like %H, but padded with blank (range 0 through 23). + .put('l', pattern("h")) // The hour as a decimal number, using a 12-hour clock like %I, but padded with blank (range 1 through 12). + .put('m', pattern("MM")) + .put('M', pattern("mm")) + .put('p', pattern("a")) + .put('S', pattern("ss")) + .put('U', pattern("ww")) + .put('w', pattern("e")) + .put('W', pattern("ww")) + .put('x', pattern("MM/dd/yy")) + .put('X', pattern("HH:mm:ss")) + .put('y', pattern("yy")) + .put('Y', pattern("yyyy")) + .put('z', pattern("Z")) + .put('Z', pattern("z")) + .put('%', (builder, stripLeadingZero) -> builder.appendLiteral("%")) + .build(); + + NOMINATIVE_COMPONENTS = + ImmutableMap + .builder() + .put('B', pattern("LLLL")) + .build(); } /** - * Parses a string in python strftime format, returning the equivalent string in java date time format. + * Build a {@link DateTimeFormatter} that matches the given Python strftime pattern. * - * @param strftime - * @return date formatted as string + * @see Python strftime cheatsheet */ - public static String toJavaDateTimeFormat(String strftime) { + public static DateTimeFormatter toDateTimeFormatter(String strftime) { if (!StringUtils.contains(strftime, '%')) { - return strftime; + return DateTimeFormatter.ofPattern(strftime); } - StringBuilder result = new StringBuilder(); + DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder(); for (int i = 0; i < strftime.length(); i++) { char c = strftime.charAt(i); - if (c == '%' && strftime.length() > i + 1) { - c = strftime.charAt(++i); - boolean stripLeadingZero = false; - String[] conversions = CONVERSIONS; + if (c != '%' || strftime.length() <= i + 1) { + builder.appendLiteral(c); + continue; + } - if (c == '-') { - stripLeadingZero = true; - c = strftime.charAt(++i); - } + c = strftime.charAt(++i); + boolean stripLeadingZero = false; + Map components = COMPONENTS; - if (c == 'O') { - c = strftime.charAt(++i); - conversions = NOMINATIVE_CONVERSIONS; - } + if (c == '-') { + stripLeadingZero = true; + c = strftime.charAt(++i); + } - if (c > 255) { - // If the date format has invalid character that is > ascii (255) then - // maintain the behaviour similar to invalid ascii char <= 255 i.e. append null - result.append(conversions[0]); - } else { - if (stripLeadingZero) { - result.append(conversions[c].substring(1)); - } else { - result.append(conversions[c]); - } - } // < 255 - } else if (Character.isLetter(c)) { - result.append("'"); - while (Character.isLetter(c)) { - result.append(c); - if (++i < strftime.length()) { - c = strftime.charAt(i); - } else { - c = 0; - } - } - result.append("'"); - --i; // re-consume last char - } else { - result.append(c); + if (c == 'O') { + c = strftime.charAt(++i); + components = NOMINATIVE_COMPONENTS; } + + final char finalChar = c; + + Optional + .ofNullable(components.get(finalChar)) + .orElseThrow( + () -> + new InvalidDateFormatException( + strftime, + String.format("unknown format code '%s'", finalChar) + ) + ) + .append(builder, stripLeadingZero); } - return result.toString(); + return builder.toFormatter(); } private static DateTimeFormatter formatter(String strftime, Locale locale) { @@ -135,7 +138,7 @@ private static DateTimeFormatter formatter(String strftime, Locale locale) { break; default: try { - fmt = DateTimeFormatter.ofPattern(toJavaDateTimeFormat(strftime)); + fmt = toDateTimeFormatter(strftime); break; } catch (IllegalArgumentException e) { throw new InvalidDateFormatException(strftime, e); @@ -160,4 +163,18 @@ public static String format(ZonedDateTime d, String strftime) { public static String format(ZonedDateTime d, String strftime, Locale locale) { return formatter(strftime, locale).format(d); } + + interface ConversionComponent { + DateTimeFormatterBuilder append( + DateTimeFormatterBuilder builder, + boolean stripLeadingZero + ); + + static ConversionComponent pattern(String targetPattern) { + return (builder, stripLeadingZero) -> + builder.appendPattern( + stripLeadingZero ? targetPattern.substring(1) : targetPattern + ); + } + } } diff --git a/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java b/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java index d7d4bedce..9233e6685 100644 --- a/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java +++ b/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java @@ -394,7 +394,7 @@ public void itHandlesInvalidFormatInFormattedDate() { assertThat(result.getErrors()) .extracting(TemplateError::getMessage) - .containsOnly("Invalid date format: [not a real format]"); + .containsOnly("Invalid date format 'not a real format'"); } @Test diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java index 9370e7ca1..a91306809 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java @@ -105,6 +105,30 @@ public void itConvertsDatetimesByLocales() { .isEqualTo("onsdag, 6 november"); } + @Test + public void itConvertsToLocaleSpecificDateTimeFormat() { + assertThat( + filter.filter( + 1539277785000L, + interpreter, + "%x %X - %c", + "America/New_York", + "en-US" + ) + ) + .isEqualTo("10/11/18 13:09:45 - Thu Oct 11 13:09:45 2018"); + assertThat( + filter.filter( + 1539277785000L, + interpreter, + "%x %X - %c", + "America/New_York", + "de-DE" + ) + ) + .isEqualTo("10/11/18 13:09:45 - Do. Okt. 11 13:09:45 2018"); + } + @Test public void itDefaultsToEnglishForBadLocaleValues() { interpreter.getContext().put("d", d); @@ -141,13 +165,7 @@ public void itHandlesInvalidDateFormats() { TemplateError error = result.getErrors().get(0); assertThat(error.getSeverity()).isEqualTo(ErrorType.FATAL); - assertThat(error.getMessage()).contains("Invalid date format: [%é]"); - - /* - datetimeformat outputs the string "null" for unrecognized format codes, - which DateTimeFormatter then tries to interpret as a pattern. 'n' and 'u' - are valid pattern letters, but 'l' is not, hence the following error message. - */ - assertThat(error.getMessage()).contains("Unknown pattern letter: l"); + assertThat(error.getMessage()) + .contains("Invalid date format '%é': unknown format code 'é'"); } } diff --git a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java index 35bea0916..cd441e6a3 100644 --- a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.objects.date; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -119,15 +120,14 @@ public void itConvertsNominativeFormats() { } @Test - public void testJavaFormatWithInvalidChar() { - assertThat(StrftimeFormatter.toJavaDateTimeFormat("%d.%é.%Y")) - .isEqualTo("dd.null.yyyy"); - } + public void itThrowsOnInvalidFormats() { + assertThatExceptionOfType(InvalidDateFormatException.class) + .isThrownBy(() -> StrftimeFormatter.format(d, "%d.%é.%Y")) + .withMessage("Invalid date format '%d.%é.%Y'"); - @Test - public void testJavaFormatWithGT255Char() { - assertThat(StrftimeFormatter.toJavaDateTimeFormat("%d.%ğ.%Y")) - .isEqualTo("dd.null.yyyy"); + assertThatExceptionOfType(InvalidDateFormatException.class) + .isThrownBy(() -> StrftimeFormatter.format(d, "%d.%ğ.%Y")) + .withMessage("Invalid date format '%d.%ğ.%Y'"); } @Test @@ -140,4 +140,10 @@ public void itOutputsLiteralPercents() { public void itIgnoresFinalStandalonePercent() { assertThat(StrftimeFormatter.format(d, "%")).isEqualTo("%"); } + + @Test + public void itAllowsLiteralCharacters() { + assertThat(StrftimeFormatter.format(d, "1: day %d month %B")) + .isEqualTo("1: day 06 month November"); + } } From 6780d72c397fb9f5c8a1bc35bc41b468a60ba48e Mon Sep 17 00:00:00 2001 From: Tyler Kindy Date: Fri, 28 Oct 2022 09:15:47 -0400 Subject: [PATCH 038/637] Implement actual locale-aware datetime formatting (#932) --- .../jinjava/objects/date/StrftimeFormatter.java | 11 ++++++++--- .../jinjava/lib/filter/DateTimeFormatFilterTest.java | 4 ++-- .../jinjava/objects/date/StrftimeFormatterTest.java | 8 ++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java b/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java index 7dcd67949..8480e2f1f 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.objects.date; +import static com.hubspot.jinjava.objects.date.StrftimeFormatter.ConversionComponent.localized; import static com.hubspot.jinjava.objects.date.StrftimeFormatter.ConversionComponent.pattern; import com.google.common.collect.ImmutableMap; @@ -33,7 +34,7 @@ public class StrftimeFormatter { .put('A', pattern("EEEE")) .put('b', pattern("MMM")) .put('B', pattern("MMMM")) - .put('c', pattern("EEE MMM dd HH:mm:ss yyyy")) + .put('c', localized(FormatStyle.MEDIUM, FormatStyle.MEDIUM)) .put('d', pattern("dd")) .put('e', pattern("d")) // The day of the month like with %d, but padded with blank (range 1 through 31). .put('f', pattern("SSSSSS")) @@ -50,8 +51,8 @@ public class StrftimeFormatter { .put('U', pattern("ww")) .put('w', pattern("e")) .put('W', pattern("ww")) - .put('x', pattern("MM/dd/yy")) - .put('X', pattern("HH:mm:ss")) + .put('x', localized(FormatStyle.SHORT, null)) + .put('X', localized(null, FormatStyle.MEDIUM)) .put('y', pattern("yy")) .put('Y', pattern("yyyy")) .put('z', pattern("Z")) @@ -176,5 +177,9 @@ static ConversionComponent pattern(String targetPattern) { stripLeadingZero ? targetPattern.substring(1) : targetPattern ); } + + static ConversionComponent localized(FormatStyle dateStyle, FormatStyle timeStyle) { + return (builder, stripLeadingZero) -> builder.appendLocalized(dateStyle, timeStyle); + } } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java index a91306809..78b15e2c3 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java @@ -116,7 +116,7 @@ public void itConvertsToLocaleSpecificDateTimeFormat() { "en-US" ) ) - .isEqualTo("10/11/18 13:09:45 - Thu Oct 11 13:09:45 2018"); + .isEqualTo("10/11/18 1:09:45 PM - Oct 11, 2018, 1:09:45 PM"); assertThat( filter.filter( 1539277785000L, @@ -126,7 +126,7 @@ public void itConvertsToLocaleSpecificDateTimeFormat() { "de-DE" ) ) - .isEqualTo("10/11/18 13:09:45 - Do. Okt. 11 13:09:45 2018"); + .isEqualTo("11.10.18 13:09:45 - 11.10.2018, 13:09:45"); } @Test diff --git a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java index cd441e6a3..acfdff5db 100644 --- a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java @@ -54,12 +54,12 @@ public void testWithNoPcts() { @Test public void testDateTime() { - assertThat(StrftimeFormatter.format(d, "%c")).isEqualTo("Wed Nov 06 14:22:00 2013"); + assertThat(StrftimeFormatter.format(d, "%c")).isEqualTo("Nov 6, 2013, 2:22:00 PM"); } @Test public void testDate() { - assertThat(StrftimeFormatter.format(d, "%x")).isEqualTo("11/06/13"); + assertThat(StrftimeFormatter.format(d, "%x")).isEqualTo("11/6/13"); } @Test @@ -69,12 +69,12 @@ public void testDayOfWeekNumber() { @Test public void testTime() { - assertThat(StrftimeFormatter.format(d, "%X")).isEqualTo("14:22:00"); + assertThat(StrftimeFormatter.format(d, "%X")).isEqualTo("2:22:00 PM"); } @Test public void testMicrosecs() { - assertThat(StrftimeFormatter.format(d, "%X %f")).isEqualTo("14:22:00 123000"); + assertThat(StrftimeFormatter.format(d, "%X %f")).isEqualTo("2:22:00 PM 123000"); } @Test From 09ee2ede8cca41e63bcfc67b1870c72e5c9468e3 Mon Sep 17 00:00:00 2001 From: Tyler Kindy Date: Fri, 28 Oct 2022 14:28:54 -0400 Subject: [PATCH 039/637] Revert "Implement actual locale-aware datetime formatting (#932)" (#939) This reverts commit 6780d72c397fb9f5c8a1bc35bc41b468a60ba48e. --- .../jinjava/objects/date/StrftimeFormatter.java | 11 +++-------- .../jinjava/lib/filter/DateTimeFormatFilterTest.java | 4 ++-- .../jinjava/objects/date/StrftimeFormatterTest.java | 8 ++++---- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java b/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java index 8480e2f1f..7dcd67949 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java @@ -1,6 +1,5 @@ package com.hubspot.jinjava.objects.date; -import static com.hubspot.jinjava.objects.date.StrftimeFormatter.ConversionComponent.localized; import static com.hubspot.jinjava.objects.date.StrftimeFormatter.ConversionComponent.pattern; import com.google.common.collect.ImmutableMap; @@ -34,7 +33,7 @@ public class StrftimeFormatter { .put('A', pattern("EEEE")) .put('b', pattern("MMM")) .put('B', pattern("MMMM")) - .put('c', localized(FormatStyle.MEDIUM, FormatStyle.MEDIUM)) + .put('c', pattern("EEE MMM dd HH:mm:ss yyyy")) .put('d', pattern("dd")) .put('e', pattern("d")) // The day of the month like with %d, but padded with blank (range 1 through 31). .put('f', pattern("SSSSSS")) @@ -51,8 +50,8 @@ public class StrftimeFormatter { .put('U', pattern("ww")) .put('w', pattern("e")) .put('W', pattern("ww")) - .put('x', localized(FormatStyle.SHORT, null)) - .put('X', localized(null, FormatStyle.MEDIUM)) + .put('x', pattern("MM/dd/yy")) + .put('X', pattern("HH:mm:ss")) .put('y', pattern("yy")) .put('Y', pattern("yyyy")) .put('z', pattern("Z")) @@ -177,9 +176,5 @@ static ConversionComponent pattern(String targetPattern) { stripLeadingZero ? targetPattern.substring(1) : targetPattern ); } - - static ConversionComponent localized(FormatStyle dateStyle, FormatStyle timeStyle) { - return (builder, stripLeadingZero) -> builder.appendLocalized(dateStyle, timeStyle); - } } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java index 78b15e2c3..a91306809 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java @@ -116,7 +116,7 @@ public void itConvertsToLocaleSpecificDateTimeFormat() { "en-US" ) ) - .isEqualTo("10/11/18 1:09:45 PM - Oct 11, 2018, 1:09:45 PM"); + .isEqualTo("10/11/18 13:09:45 - Thu Oct 11 13:09:45 2018"); assertThat( filter.filter( 1539277785000L, @@ -126,7 +126,7 @@ public void itConvertsToLocaleSpecificDateTimeFormat() { "de-DE" ) ) - .isEqualTo("11.10.18 13:09:45 - 11.10.2018, 13:09:45"); + .isEqualTo("10/11/18 13:09:45 - Do. Okt. 11 13:09:45 2018"); } @Test diff --git a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java index acfdff5db..cd441e6a3 100644 --- a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java @@ -54,12 +54,12 @@ public void testWithNoPcts() { @Test public void testDateTime() { - assertThat(StrftimeFormatter.format(d, "%c")).isEqualTo("Nov 6, 2013, 2:22:00 PM"); + assertThat(StrftimeFormatter.format(d, "%c")).isEqualTo("Wed Nov 06 14:22:00 2013"); } @Test public void testDate() { - assertThat(StrftimeFormatter.format(d, "%x")).isEqualTo("11/6/13"); + assertThat(StrftimeFormatter.format(d, "%x")).isEqualTo("11/06/13"); } @Test @@ -69,12 +69,12 @@ public void testDayOfWeekNumber() { @Test public void testTime() { - assertThat(StrftimeFormatter.format(d, "%X")).isEqualTo("2:22:00 PM"); + assertThat(StrftimeFormatter.format(d, "%X")).isEqualTo("14:22:00"); } @Test public void testMicrosecs() { - assertThat(StrftimeFormatter.format(d, "%X %f")).isEqualTo("2:22:00 PM 123000"); + assertThat(StrftimeFormatter.format(d, "%X %f")).isEqualTo("14:22:00 123000"); } @Test From 9f4a5fcde16c2d94ef0e4010ead168e41e2d78af Mon Sep 17 00:00:00 2001 From: Tyler Kindy Date: Fri, 28 Oct 2022 14:36:23 -0400 Subject: [PATCH 040/637] Revert "Revert "Implement actual locale-aware datetime formatting (#932)" (#939)" (#941) This reverts commit 09ee2ede8cca41e63bcfc67b1870c72e5c9468e3. --- .../jinjava/objects/date/StrftimeFormatter.java | 11 ++++++++--- .../jinjava/lib/filter/DateTimeFormatFilterTest.java | 4 ++-- .../jinjava/objects/date/StrftimeFormatterTest.java | 8 ++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java b/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java index 7dcd67949..8480e2f1f 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.objects.date; +import static com.hubspot.jinjava.objects.date.StrftimeFormatter.ConversionComponent.localized; import static com.hubspot.jinjava.objects.date.StrftimeFormatter.ConversionComponent.pattern; import com.google.common.collect.ImmutableMap; @@ -33,7 +34,7 @@ public class StrftimeFormatter { .put('A', pattern("EEEE")) .put('b', pattern("MMM")) .put('B', pattern("MMMM")) - .put('c', pattern("EEE MMM dd HH:mm:ss yyyy")) + .put('c', localized(FormatStyle.MEDIUM, FormatStyle.MEDIUM)) .put('d', pattern("dd")) .put('e', pattern("d")) // The day of the month like with %d, but padded with blank (range 1 through 31). .put('f', pattern("SSSSSS")) @@ -50,8 +51,8 @@ public class StrftimeFormatter { .put('U', pattern("ww")) .put('w', pattern("e")) .put('W', pattern("ww")) - .put('x', pattern("MM/dd/yy")) - .put('X', pattern("HH:mm:ss")) + .put('x', localized(FormatStyle.SHORT, null)) + .put('X', localized(null, FormatStyle.MEDIUM)) .put('y', pattern("yy")) .put('Y', pattern("yyyy")) .put('z', pattern("Z")) @@ -176,5 +177,9 @@ static ConversionComponent pattern(String targetPattern) { stripLeadingZero ? targetPattern.substring(1) : targetPattern ); } + + static ConversionComponent localized(FormatStyle dateStyle, FormatStyle timeStyle) { + return (builder, stripLeadingZero) -> builder.appendLocalized(dateStyle, timeStyle); + } } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java index a91306809..78b15e2c3 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java @@ -116,7 +116,7 @@ public void itConvertsToLocaleSpecificDateTimeFormat() { "en-US" ) ) - .isEqualTo("10/11/18 13:09:45 - Thu Oct 11 13:09:45 2018"); + .isEqualTo("10/11/18 1:09:45 PM - Oct 11, 2018, 1:09:45 PM"); assertThat( filter.filter( 1539277785000L, @@ -126,7 +126,7 @@ public void itConvertsToLocaleSpecificDateTimeFormat() { "de-DE" ) ) - .isEqualTo("10/11/18 13:09:45 - Do. Okt. 11 13:09:45 2018"); + .isEqualTo("11.10.18 13:09:45 - 11.10.2018, 13:09:45"); } @Test diff --git a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java index cd441e6a3..acfdff5db 100644 --- a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java @@ -54,12 +54,12 @@ public void testWithNoPcts() { @Test public void testDateTime() { - assertThat(StrftimeFormatter.format(d, "%c")).isEqualTo("Wed Nov 06 14:22:00 2013"); + assertThat(StrftimeFormatter.format(d, "%c")).isEqualTo("Nov 6, 2013, 2:22:00 PM"); } @Test public void testDate() { - assertThat(StrftimeFormatter.format(d, "%x")).isEqualTo("11/06/13"); + assertThat(StrftimeFormatter.format(d, "%x")).isEqualTo("11/6/13"); } @Test @@ -69,12 +69,12 @@ public void testDayOfWeekNumber() { @Test public void testTime() { - assertThat(StrftimeFormatter.format(d, "%X")).isEqualTo("14:22:00"); + assertThat(StrftimeFormatter.format(d, "%X")).isEqualTo("2:22:00 PM"); } @Test public void testMicrosecs() { - assertThat(StrftimeFormatter.format(d, "%X %f")).isEqualTo("14:22:00 123000"); + assertThat(StrftimeFormatter.format(d, "%X %f")).isEqualTo("2:22:00 PM 123000"); } @Test From ae69ddb9991f11e5ef2747174bcad9a670652995 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 28 Oct 2022 15:07:51 -0400 Subject: [PATCH 041/637] Reconstruct all undefined variables when reconstructing call tag --- .../hubspot/jinjava/lib/tag/eager/EagerCallTag.java | 2 ++ src/test/java/com/hubspot/jinjava/EagerTest.java | 7 +++++++ ...null-variables-in-deferred-caller.expected.jinja | 10 ++++++++++ ...nstructs-null-variables-in-deferred-caller.jinja | 13 +++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 src/test/resources/eager/reconstructs-null-variables-in-deferred-caller.expected.jinja create mode 100644 src/test/resources/eager/reconstructs-null-variables-in-deferred-caller.jinja diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java index 7756f4ed2..242db372a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.lib.tag.eager; import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; +import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; @@ -127,6 +128,7 @@ public String eagerInterpret( StringBuilder result = new StringBuilder( prefixToPreserveState.toString() + joiner.toString() ); + interpreter.getContext().setDynamicVariableResolver(s -> DeferredValue.instance()); if (!tagNode.getChildren().isEmpty()) { result.append( EagerReconstructionUtils diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index bbd80ec9d..e5d4bb948 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1167,4 +1167,11 @@ public void itHandlesDeferredForLoopVarFromMacroSecondPass() { "handles-deferred-for-loop-var-from-macro.expected" ); } + + @Test + public void itReconstructsNullVariablesInDeferredCaller() { + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-null-variables-in-deferred-caller" + ); + } } diff --git a/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller.expected.jinja b/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller.expected.jinja new file mode 100644 index 000000000..3301885ed --- /dev/null +++ b/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller.expected.jinja @@ -0,0 +1,10 @@ +{% if deferred %} +{% macro foo(var) %} +{% set second_list = [] %} +{% do second_list.append(var) %} +{{ caller() }} +{% endmacro %}{% call foo(deferred) %} +[] +{{ second_list }} +{% endcall %} +{% endif %} diff --git a/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller.jinja b/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller.jinja new file mode 100644 index 000000000..744281882 --- /dev/null +++ b/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller.jinja @@ -0,0 +1,13 @@ +{% set first_list = [] %} +{% macro foo(var) %} +{% set second_list = [] %} +{% do second_list.append(var) %} +{{ caller() }} +{% endmacro %} + +{% if deferred %} +{% call foo(deferred) %} +{{ first_list }} +{{ second_list }} +{% endcall %} +{% endif %} From e8f6896eeede3fe5e6beb72420cd9848dea68d28 Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Sat, 29 Oct 2022 17:46:28 -0400 Subject: [PATCH 042/637] Update title filter to ignore special characters --- .../jinjava/lib/filter/TitleFilter.java | 38 ++++++++++++++++--- .../jinjava/lib/filter/TitleFilterTest.java | 18 +++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java index 19da5635b..ec6b94d23 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java @@ -1,10 +1,10 @@ package com.hubspot.jinjava.lib.filter; +import com.google.common.base.Splitter; import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import org.apache.commons.lang3.text.WordUtils; /** * Return a titlecased version of the value. I.e. words will start with uppercase letters, all remaining characters are lowercase. @@ -23,6 +23,8 @@ ) public class TitleFilter implements Filter { + private static final Splitter WORD_SPLITTER = Splitter.on(' '); + @Override public String getName() { return "title"; @@ -30,10 +32,36 @@ public String getName() { @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - if (var instanceof String) { - String value = (String) var; - return WordUtils.capitalize(value.toLowerCase()); + if (var == null) { + return null; } - return var; + + String value = var.toString(); + + char[] chars = value.toCharArray(); + boolean titleCased = false; + + for (int i = 0; i < chars.length; i++) { + if (chars[i] == ' ') { + titleCased = false; + continue; + } + + char original = chars[i]; + if (titleCased) { + chars[i] = Character.toLowerCase(original); + } else { + if (charCanBeTitlecased(original)) { + chars[i] = Character.toTitleCase(original); + titleCased = true; + } + } + } + + return new String(chars); + } + + private boolean charCanBeTitlecased(char c) { + return Character.toLowerCase(c) != Character.toTitleCase(c); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java index d3cb49bfd..954370115 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java @@ -12,6 +12,12 @@ public void itTitleCasesNormalString() { .isEqualTo("This Is String"); } + @Test + public void itPreservesWhitespace() { + assertThat(new TitleFilter().filter("this is string ", null)) + .isEqualTo("This Is String "); + } + @Test public void itDoesNotChangeAlreadyTitleCasedString() { assertThat(new TitleFilter().filter("This Is String", null)) @@ -23,4 +29,16 @@ public void itLowercasesOtherUppercasedCharactersInString() { assertThat(new TitleFilter().filter("this is sTRING", null)) .isEqualTo("This Is String"); } + + @Test + public void itIgnoresParenthesesWhenCapitalizing() { + assertThat(new TitleFilter().filter("test (company) name", null)) + .isEqualTo("Test (Company) Name"); + } + + @Test + public void itIgnoresMultipleSpecialCharactersWhenCapitalizing() { + assertThat(new TitleFilter().filter("@@@@mcoley t@est !@#$%^&*()_+plop", null)) + .isEqualTo("@@@@Mcoley T@est !@#$%^&*()_+Plop"); + } } From 104f74204548fdf1ddb2a626f22d1aecedca9ce3 Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Sat, 29 Oct 2022 17:57:07 -0400 Subject: [PATCH 043/637] Make pretty --- src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java | 1 - .../java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java index ec6b94d23..5fd4cf82d 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java @@ -22,7 +22,6 @@ snippets = { @JinjavaSnippet(code = "{{ \"My title should be titlecase\"|title }} ") } ) public class TitleFilter implements Filter { - private static final Splitter WORD_SPLITTER = Splitter.on(' '); @Override diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java index 954370115..7b1161bcc 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java @@ -33,7 +33,7 @@ public void itLowercasesOtherUppercasedCharactersInString() { @Test public void itIgnoresParenthesesWhenCapitalizing() { assertThat(new TitleFilter().filter("test (company) name", null)) - .isEqualTo("Test (Company) Name"); + .isEqualTo("Test (Company) Name"); } @Test From 4c85a6a76e653064ac105d4e9f8f8aa12262763f Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Sat, 29 Oct 2022 17:58:21 -0400 Subject: [PATCH 044/637] remove --- src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java index 5fd4cf82d..7b0dc791a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java @@ -1,6 +1,5 @@ package com.hubspot.jinjava.lib.filter; -import com.google.common.base.Splitter; import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; @@ -22,7 +21,6 @@ snippets = { @JinjavaSnippet(code = "{{ \"My title should be titlecase\"|title }} ") } ) public class TitleFilter implements Filter { - private static final Splitter WORD_SPLITTER = Splitter.on(' '); @Override public String getName() { From 15543d227a44c0b0a2b888909c4d10088c7ad33e Mon Sep 17 00:00:00 2001 From: Matt Coley Date: Mon, 31 Oct 2022 14:16:52 -0400 Subject: [PATCH 045/637] Revert "Update title filter to ignore special characters" --- .../jinjava/lib/filter/TitleFilter.java | 35 +++---------------- .../jinjava/lib/filter/TitleFilterTest.java | 18 ---------- 2 files changed, 5 insertions(+), 48 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java index 7b0dc791a..19da5635b 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java @@ -4,6 +4,7 @@ import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import org.apache.commons.lang3.text.WordUtils; /** * Return a titlecased version of the value. I.e. words will start with uppercase letters, all remaining characters are lowercase. @@ -29,36 +30,10 @@ public String getName() { @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - if (var == null) { - return null; + if (var instanceof String) { + String value = (String) var; + return WordUtils.capitalize(value.toLowerCase()); } - - String value = var.toString(); - - char[] chars = value.toCharArray(); - boolean titleCased = false; - - for (int i = 0; i < chars.length; i++) { - if (chars[i] == ' ') { - titleCased = false; - continue; - } - - char original = chars[i]; - if (titleCased) { - chars[i] = Character.toLowerCase(original); - } else { - if (charCanBeTitlecased(original)) { - chars[i] = Character.toTitleCase(original); - titleCased = true; - } - } - } - - return new String(chars); - } - - private boolean charCanBeTitlecased(char c) { - return Character.toLowerCase(c) != Character.toTitleCase(c); + return var; } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java index 7b1161bcc..d3cb49bfd 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java @@ -12,12 +12,6 @@ public void itTitleCasesNormalString() { .isEqualTo("This Is String"); } - @Test - public void itPreservesWhitespace() { - assertThat(new TitleFilter().filter("this is string ", null)) - .isEqualTo("This Is String "); - } - @Test public void itDoesNotChangeAlreadyTitleCasedString() { assertThat(new TitleFilter().filter("This Is String", null)) @@ -29,16 +23,4 @@ public void itLowercasesOtherUppercasedCharactersInString() { assertThat(new TitleFilter().filter("this is sTRING", null)) .isEqualTo("This Is String"); } - - @Test - public void itIgnoresParenthesesWhenCapitalizing() { - assertThat(new TitleFilter().filter("test (company) name", null)) - .isEqualTo("Test (Company) Name"); - } - - @Test - public void itIgnoresMultipleSpecialCharactersWhenCapitalizing() { - assertThat(new TitleFilter().filter("@@@@mcoley t@est !@#$%^&*()_+plop", null)) - .isEqualTo("@@@@Mcoley T@est !@#$%^&*()_+Plop"); - } } From b37e877f5ba7e54e672ba37a4eb4766a7a710ff5 Mon Sep 17 00:00:00 2001 From: Matt Coley Date: Mon, 31 Oct 2022 14:17:05 -0400 Subject: [PATCH 046/637] Revert "Revert "Update title filter to ignore special characters"" --- .../jinjava/lib/filter/TitleFilter.java | 35 ++++++++++++++++--- .../jinjava/lib/filter/TitleFilterTest.java | 18 ++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java index 19da5635b..7b0dc791a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java @@ -4,7 +4,6 @@ import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import org.apache.commons.lang3.text.WordUtils; /** * Return a titlecased version of the value. I.e. words will start with uppercase letters, all remaining characters are lowercase. @@ -30,10 +29,36 @@ public String getName() { @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - if (var instanceof String) { - String value = (String) var; - return WordUtils.capitalize(value.toLowerCase()); + if (var == null) { + return null; } - return var; + + String value = var.toString(); + + char[] chars = value.toCharArray(); + boolean titleCased = false; + + for (int i = 0; i < chars.length; i++) { + if (chars[i] == ' ') { + titleCased = false; + continue; + } + + char original = chars[i]; + if (titleCased) { + chars[i] = Character.toLowerCase(original); + } else { + if (charCanBeTitlecased(original)) { + chars[i] = Character.toTitleCase(original); + titleCased = true; + } + } + } + + return new String(chars); + } + + private boolean charCanBeTitlecased(char c) { + return Character.toLowerCase(c) != Character.toTitleCase(c); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java index d3cb49bfd..7b1161bcc 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java @@ -12,6 +12,12 @@ public void itTitleCasesNormalString() { .isEqualTo("This Is String"); } + @Test + public void itPreservesWhitespace() { + assertThat(new TitleFilter().filter("this is string ", null)) + .isEqualTo("This Is String "); + } + @Test public void itDoesNotChangeAlreadyTitleCasedString() { assertThat(new TitleFilter().filter("This Is String", null)) @@ -23,4 +29,16 @@ public void itLowercasesOtherUppercasedCharactersInString() { assertThat(new TitleFilter().filter("this is sTRING", null)) .isEqualTo("This Is String"); } + + @Test + public void itIgnoresParenthesesWhenCapitalizing() { + assertThat(new TitleFilter().filter("test (company) name", null)) + .isEqualTo("Test (Company) Name"); + } + + @Test + public void itIgnoresMultipleSpecialCharactersWhenCapitalizing() { + assertThat(new TitleFilter().filter("@@@@mcoley t@est !@#$%^&*()_+plop", null)) + .isEqualTo("@@@@Mcoley T@est !@#$%^&*()_+Plop"); + } } From 77e5d60f5d06fddd7fd3265fd265cd484e509c5e Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Mon, 31 Oct 2022 19:59:08 -0400 Subject: [PATCH 047/637] add test for whitespace --- .../java/com/hubspot/jinjava/lib/filter/TitleFilter.java | 2 +- .../com/hubspot/jinjava/lib/filter/TitleFilterTest.java | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java index 7b0dc791a..4bc07c02e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/TitleFilter.java @@ -39,7 +39,7 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) boolean titleCased = false; for (int i = 0; i < chars.length; i++) { - if (chars[i] == ' ') { + if (Character.isWhitespace(chars[i])) { titleCased = false; continue; } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java index 7b1161bcc..a0c89fb39 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/TitleFilterTest.java @@ -41,4 +41,10 @@ public void itIgnoresMultipleSpecialCharactersWhenCapitalizing() { assertThat(new TitleFilter().filter("@@@@mcoley t@est !@#$%^&*()_+plop", null)) .isEqualTo("@@@@Mcoley T@est !@#$%^&*()_+Plop"); } + + @Test + public void itRespectsNewlinesAndTabs() { + assertThat(new TitleFilter().filter("test\t(company)\nname", null)) + .isEqualTo("Test\t(Company)\nName"); + } } From 3c3b41ad5e00632e23e6731b108859dcb7a671e1 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 2 Nov 2022 14:04:04 -0400 Subject: [PATCH 048/637] Don't use arbitrary booleans in public methods --- .../com/hubspot/jinjava/el/ExpressionResolver.java | 12 +++++++++++- .../jinjava/interpret/JinjavaInterpreter.java | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java b/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java index 8d724241c..3ddd44b7d 100644 --- a/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java @@ -68,7 +68,17 @@ public Object resolveExpression(String expression) { return resolveExpression(expression, true); } - public Object resolveExpression(String expression, boolean addToResolvedExpressions) { + /** + * Resolve expression against current context without adding the expression to the set of resolved expressions. + * + * @param expression Jinja expression. + * @return Value of expression. + */ + public Object resolveExpressionSilently(String expression) { + return resolveExpression(expression, false); + } + + private Object resolveExpression(String expression, boolean addToResolvedExpressions) { if (StringUtils.isBlank(expression)) { return null; } diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index 0a5361f6b..2e6b0ebdc 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -608,7 +608,7 @@ public JinjavaConfig getConfig() { * @return Value of expression. */ public Object resolveELExpressionSilently(String expression) { - return expressionResolver.resolveExpression(expression, false); + return expressionResolver.resolveExpressionSilently(expression); } /** From a018c325a3adca5022254628dbfd5a88530c4f39 Mon Sep 17 00:00:00 2001 From: Jeff Boulter Date: Thu, 3 Nov 2022 11:56:37 -0400 Subject: [PATCH 049/637] disallow adding lists to itself --- .../jinjava/objects/collections/PyList.java | 6 +++++ .../objects/collections/PyListTest.java | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/main/java/com/hubspot/jinjava/objects/collections/PyList.java b/src/main/java/com/hubspot/jinjava/objects/collections/PyList.java index 5d34fd9b1..136ab8661 100644 --- a/src/main/java/com/hubspot/jinjava/objects/collections/PyList.java +++ b/src/main/java/com/hubspot/jinjava/objects/collections/PyList.java @@ -25,10 +25,16 @@ public List toList() { } public boolean append(Object e) { + if (this == e) { + return false; + } return add(e); } public void insert(int i, Object e) { + if (this == e) { + return; + } if (i >= list.size()) { throw createOutOfRangeException(i); } diff --git a/src/test/java/com/hubspot/jinjava/objects/collections/PyListTest.java b/src/test/java/com/hubspot/jinjava/objects/collections/PyListTest.java index 7d1d71ee7..5cb9cf67a 100644 --- a/src/test/java/com/hubspot/jinjava/objects/collections/PyListTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/collections/PyListTest.java @@ -239,4 +239,26 @@ public void itReturnsNegativeOneForMissingObjectForIndexWithinBounds() { ) .isEqualTo("-1"); } + + @Test + public void itDisallowsInsertingSelf() { + assertThat( + jinjava.render( + "{% set test = [1,2] %}" + "{% do test.insert(0, test) %}" + "{{ test }}", + Collections.emptyMap() + ) + ) + .isEqualTo("[1, 2]"); + } + + @Test + public void itDisallowsAppendingSelf() { + assertThat( + jinjava.render( + "{% set test = [1, 2] %}" + "{% do test.append(test) %}" + "{{ test }}", + Collections.emptyMap() + ) + ) + .isEqualTo("[1, 2]"); + } } From 4202f391483d60eed32e0a205b058906f51db0ad Mon Sep 17 00:00:00 2001 From: Jeff Boulter Date: Fri, 4 Nov 2022 13:39:06 -0400 Subject: [PATCH 050/637] add NPE checks on limited collections --- .../collections/SizeLimitingPyList.java | 18 ++++++++++++++---- .../objects/collections/SizeLimitingPyMap.java | 3 +++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyList.java b/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyList.java index 8cfd81c35..905df07cc 100644 --- a/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyList.java +++ b/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyList.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import javax.annotation.Nonnull; public class SizeLimitingPyList extends PyList implements PyWrapper { private int maxSize; @@ -20,6 +21,9 @@ private SizeLimitingPyList(List list) { public SizeLimitingPyList(List list, int maxSize) { super(list); + if (list == null) { + throw new IllegalArgumentException("list is null"); + } if (maxSize <= 0) { throw new IllegalArgumentException("maxSize must be >= 1"); } @@ -31,25 +35,31 @@ public SizeLimitingPyList(List list, int maxSize) { } @Override - public boolean add(Object element) { + public boolean add(@Nonnull Object element) { checkSize(size() + 1); return super.add(element); } @Override - public void add(int index, Object element) { + public void add(int index, @Nonnull Object element) { checkSize(size() + 1); super.add(index, element); } @Override - public boolean addAll(int index, Collection elements) { + public boolean addAll(int index, @Nonnull Collection elements) { + if (elements == null || elements.isEmpty()) { + return false; + } checkSize(size() + elements.size()); return super.addAll(index, elements); } @Override - public boolean addAll(Collection elements) { + public boolean addAll(@Nonnull Collection elements) { + if (elements == null || elements.isEmpty()) { + return false; + } checkSize(size() + elements.size()); return super.addAll(elements); } diff --git a/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyMap.java b/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyMap.java index 4c791561c..f688ac568 100644 --- a/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyMap.java +++ b/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyMap.java @@ -19,6 +19,9 @@ private SizeLimitingPyMap(Map map) { public SizeLimitingPyMap(Map map, int maxSize) { super(map); + if (map == null) { + throw new IllegalArgumentException("map is null"); + } if (maxSize <= 0) { throw new IllegalArgumentException("maxSize must be >= 1"); } From cc6577508093bbf3a2342f1d60e0d80e789347b6 Mon Sep 17 00:00:00 2001 From: Jeff Boulter Date: Fri, 4 Nov 2022 14:04:10 -0400 Subject: [PATCH 051/637] remove Nonnull annotations --- .../jinjava/objects/collections/SizeLimitingPyList.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyList.java b/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyList.java index 905df07cc..13147e95c 100644 --- a/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyList.java +++ b/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyList.java @@ -35,13 +35,13 @@ public SizeLimitingPyList(List list, int maxSize) { } @Override - public boolean add(@Nonnull Object element) { + public boolean add(Object element) { checkSize(size() + 1); return super.add(element); } @Override - public void add(int index, @Nonnull Object element) { + public void add(int index, Object element) { checkSize(size() + 1); super.add(index, element); } From 718fd5ed94836c89c204da842b84d5e3df4dc985 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 8 Nov 2022 15:33:23 -0500 Subject: [PATCH 052/637] Include dotted-set variables to properly defer namespaces --- .../jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java | 1 + .../jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java | 1 + .../jinjava/lib/tag/eager/EagerSetTagStrategy.java | 9 ++++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java index e4d0434fc..f04771b12 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java @@ -111,6 +111,7 @@ protected Triple getPrefixTokenAndSuffix( .add(tagNode.getSymbols().getExpressionEndWithTag()); String prefixToPreserveState = getPrefixToPreserveState( eagerExecutionResult, + variables, interpreter ); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java index dd83295ce..1db49b82c 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java @@ -86,6 +86,7 @@ public Triple getPrefixTokenAndSuffix( .add(tagNode.getSymbols().getExpressionEndWithTag()); String prefixToPreserveState = getPrefixToPreserveState( eagerExecutionResult, + variables, interpreter ); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java index eec9d7886..4421dc6d2 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java @@ -11,6 +11,7 @@ import java.util.Set; import java.util.StringJoiner; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Triple; public abstract class EagerSetTagStrategy { @@ -117,6 +118,7 @@ protected abstract String buildImage( protected String getPrefixToPreserveState( EagerExecutionResult eagerExecutionResult, + String[] variables, JinjavaInterpreter interpreter ) { StringBuilder prefixToPreserveState = new StringBuilder(); @@ -127,7 +129,12 @@ protected String getPrefixToPreserveState( } prefixToPreserveState.append( EagerReconstructionUtils.reconstructFromContextBeforeDeferring( - eagerExecutionResult.getResult().getDeferredWords(), + Stream + .concat( + eagerExecutionResult.getResult().getDeferredWords().stream(), + Arrays.stream(variables).filter(var -> var.contains(".")) + ) + .collect(Collectors.toSet()), interpreter ) ); From 70bf6620084e78c064490edb183597bd8739a35c Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 8 Nov 2022 15:36:16 -0500 Subject: [PATCH 053/637] Add test to verify that namespace key sets will reconstruct the namespace properly --- src/test/java/com/hubspot/jinjava/EagerTest.java | 15 +++++++++++++++ ...-set-tags-using-period.expected.expected.jinja | 2 ++ ...space-for-set-tags-using-period.expected.jinja | 7 +++++++ ...ucts-namespace-for-set-tags-using-period.jinja | 10 ++++++++++ 4 files changed, 34 insertions(+) create mode 100644 src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.expected.jinja create mode 100644 src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.jinja create mode 100644 src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.jinja diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 320971781..706a222a3 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1181,4 +1181,19 @@ public void itReconstructsNullVariablesInDeferredCaller() { "reconstructs-null-variables-in-deferred-caller" ); } + + @Test + public void itReconstructsNamespaceForSetTagsUsingPeriod() { + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-namespace-for-set-tags-using-period" + ); + } + + @Test + public void itReconstructsNamespaceForSetTagsUsingPeriodSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "reconstructs-namespace-for-set-tags-using-period.expected" + ); + } } diff --git a/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.expected.jinja b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.expected.jinja new file mode 100644 index 000000000..bc1e2a177 --- /dev/null +++ b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.expected.jinja @@ -0,0 +1,2 @@ +namespace({'a': 'aa', 'b': 'b resolved'}) +namespace({'c': 'cc', 'd': 'd resolved'}) diff --git a/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.jinja b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.jinja new file mode 100644 index 000000000..1894f0699 --- /dev/null +++ b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.jinja @@ -0,0 +1,7 @@ +{% set ns1 = namespace({'a': 'aa'}) %}{% set ns1.b = 'b ' ~ deferred %} + + +{% set ns2 = namespace({'c': 'cc'}) %}{% set ns2.d %}d {{ deferred }}{% endset %} + +{{ ns1 }} +{{ ns2 }} diff --git a/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.jinja b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.jinja new file mode 100644 index 000000000..b380d12fa --- /dev/null +++ b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.jinja @@ -0,0 +1,10 @@ +{% set ns1 = namespace({'a': 'aa'}) %} +{% set ns1.b = 'b ' ~ deferred %} + +{% set ns2 = namespace({'c': 'cc'}) %} +{% set ns2.d -%} +d {{ deferred }} +{%- endset %} + +{{ ns1 }} +{{ ns2 }} From 8435482334538ebb9970e03d7eebe4e794cf318b Mon Sep 17 00:00:00 2001 From: Nathaniel Pautzke Date: Wed, 9 Nov 2022 10:15:59 -0600 Subject: [PATCH 054/637] update test --- .../jinjava/lib/tag/eager/EagerCycleTagTest.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java index d55dae006..b18b0149b 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java @@ -51,7 +51,7 @@ public void teardown() { } @Test - public void itHandlesDeferredCycle() { + public void itAddCycleTagAsADeferredToken() { String template = "{% for item in deferred %}{% cycle 'item-1','item-2' %}{% endfor %}"; assertThat(interpreter.render(template)).isEqualTo(template); @@ -64,4 +64,12 @@ public void itHandlesDeferredCycle() { assertThat(maybeDeferredToken.get().getToken().getImage()) .isEqualTo("{% cycle 'item-1','item-2' %}"); } + + @Test + public void itHandlesDeferredCycle() { + interpreter.getContext().put("deferred", DeferredValue.instance()); + String template = + "{% set l = [] %}{% for item in deferred %}{% cycle l.append(deferred),5 %}{% endfor %}{{ l }}"; + assertThat(interpreter.render(template)).isEqualTo(template); + } } From 7a50bf93174244ada1c4b20d27bb6491fd92d881 Mon Sep 17 00:00:00 2001 From: Tyler Kindy Date: Thu, 10 Nov 2022 17:19:15 -0500 Subject: [PATCH 055/637] Add test for overriding built-in filters (#951) --- .../hubspot/jinjava/FilterOverrideTest.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/test/java/com/hubspot/jinjava/FilterOverrideTest.java diff --git a/src/test/java/com/hubspot/jinjava/FilterOverrideTest.java b/src/test/java/com/hubspot/jinjava/FilterOverrideTest.java new file mode 100644 index 000000000..dd9bb2815 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/FilterOverrideTest.java @@ -0,0 +1,41 @@ +package com.hubspot.jinjava; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.filter.Filter; +import java.util.HashMap; +import org.junit.Test; + +public class FilterOverrideTest { + + @Test + public void itAllowsUsersToOverrideBuiltInFilters() { + Jinjava jinjava = new Jinjava(); + String template = "{{ 5 | add(6) }}"; + + assertThat(jinjava.render(template, new HashMap<>())).isEqualTo("11"); + + jinjava.getGlobalContext().registerClasses(DescriptiveAddFilter.class); + assertThat(jinjava.render(template, new HashMap<>())).isEqualTo("5 + 6 = 11"); + } + + public static class DescriptiveAddFilter implements Filter { + + @Override + public String getName() { + return "add"; + } + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + return ( + var + + " + " + + args[0] + + " = " + + (Integer.parseInt(var.toString()) + Integer.parseInt(args[0])) + ); + } + } +} From 707351912e8ca11d44b4481e5f25f50c024a98f1 Mon Sep 17 00:00:00 2001 From: Tyler Kindy Date: Mon, 14 Nov 2022 11:25:06 -0500 Subject: [PATCH 056/637] Sort registered filters alphabetically (#952) --- .../jinjava/lib/filter/FilterLibrary.java | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java index 42cbf2e8b..592101ec9 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java @@ -27,95 +27,95 @@ public FilterLibrary(boolean registerDefaults, Set disabled) { @Override protected void registerDefaults() { registerClasses( + AbsFilter.class, + AddFilter.class, AttrFilter.class, - PrettyPrintFilter.class, - DefaultFilter.class, - DAliasedDefaultFilter.class, - FileSizeFormatFilter.class, - UrlizeFilter.class, + Base64DecodeFilter.class, + Base64EncodeFilter.class, BatchFilter.class, + BetweenTimesFilter.class, + BoolFilter.class, + CapitalizeFilter.class, + CenterFilter.class, CountFilter.class, + CutFilter.class, + DAliasedDefaultFilter.class, + DateTimeFormatFilter.class, + DatetimeFilter.class, + DefaultFilter.class, DictSortFilter.class, + DifferenceFilter.class, + DivideFilter.class, + DivisibleFilter.class, + EAliasedEscapeFilter.class, + EscapeFilter.class, + EscapeJinjavaFilter.class, + EscapeJsFilter.class, + EscapeJsonFilter.class, + FileSizeFormatFilter.class, FirstFilter.class, + FloatFilter.class, + ForceEscapeFilter.class, + FormatFilter.class, + FromJsonFilter.class, + FromYamlFilter.class, GroupByFilter.class, + IndentFilter.class, + IntFilter.class, + IntersectFilter.class, + IpAddrFilter.class, + Ipv4Filter.class, + Ipv6Filter.class, JoinFilter.class, LastFilter.class, LengthFilter.class, ListFilter.class, + LogFilter.class, + LowerFilter.class, MapFilter.class, - RejectAttrFilter.class, + Md5Filter.class, + MinusTimeFilter.class, + MultiplyFilter.class, + PlusTimeFilter.class, + PrettyPrintFilter.class, + RandomFilter.class, + RegexReplaceFilter.class, RejectFilter.class, + RejectAttrFilter.class, + RenderFilter.class, + ReplaceFilter.class, + ReverseFilter.class, + RootFilter.class, + RoundFilter.class, + SafeFilter.class, SelectFilter.class, SelectAttrFilter.class, - SliceFilter.class, ShuffleFilter.class, + SliceFilter.class, SortFilter.class, SplitFilter.class, - DatetimeFilter.class, - DateTimeFormatFilter.class, - UnixTimestampFilter.class, - PlusTimeFilter.class, - MinusTimeFilter.class, - BetweenTimesFilter.class, - StringToTimeFilter.class, + StringFilter.class, StringToDateFilter.class, - UnionFilter.class, - IntersectFilter.class, - DifferenceFilter.class, - SymmetricDifferenceFilter.class, - UniqueFilter.class, - AbsFilter.class, - AddFilter.class, - RootFilter.class, - LogFilter.class, - BoolFilter.class, - CutFilter.class, - DivideFilter.class, - DivisibleFilter.class, - FloatFilter.class, - IntFilter.class, - Md5Filter.class, - MultiplyFilter.class, - RandomFilter.class, - ReverseFilter.class, - RoundFilter.class, - SumFilter.class, - IpAddrFilter.class, - Ipv4Filter.class, - Ipv6Filter.class, - EscapeFilter.class, - EAliasedEscapeFilter.class, - EscapeJsFilter.class, - ForceEscapeFilter.class, + StringToTimeFilter.class, StripTagsFilter.class, - UrlEncodeFilter.class, - UrlDecodeFilter.class, - XmlAttrFilter.class, - EscapeJsonFilter.class, - EscapeJinjavaFilter.class, - CapitalizeFilter.class, - CenterFilter.class, - FormatFilter.class, - IndentFilter.class, - LowerFilter.class, + SumFilter.class, + SymmetricDifferenceFilter.class, + TitleFilter.class, + ToJsonFilter.class, + ToYamlFilter.class, + TrimFilter.class, TruncateFilter.class, TruncateHtmlFilter.class, + UnionFilter.class, + UniqueFilter.class, + UnixTimestampFilter.class, UpperFilter.class, - ReplaceFilter.class, - RegexReplaceFilter.class, - StringFilter.class, - SafeFilter.class, - TitleFilter.class, - TrimFilter.class, + UrlDecodeFilter.class, + UrlEncodeFilter.class, + UrlizeFilter.class, WordCountFilter.class, WordWrapFilter.class, - ToJsonFilter.class, - FromJsonFilter.class, - ToYamlFilter.class, - FromYamlFilter.class, - RenderFilter.class, - Base64EncodeFilter.class, - Base64DecodeFilter.class + XmlAttrFilter.class ); } From a9ad9ab07190e4356a54250b4b8d7ff1e7c3937b Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 17 Nov 2022 10:47:20 -0500 Subject: [PATCH 057/637] Make deferred macro temp variable names more unique by utilizing the hash code which includes the import path --- .../el/ext/eager/MacroFunctionTempVariable.java | 11 ++++++++--- .../com/hubspot/jinjava/lib/fn/MacroFunction.java | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/MacroFunctionTempVariable.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/MacroFunctionTempVariable.java index d65a9eae2..8250454a2 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/MacroFunctionTempVariable.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/MacroFunctionTempVariable.java @@ -4,15 +4,20 @@ import java.util.Objects; public class MacroFunctionTempVariable implements PyishBlockSetSerializable { - private static final String CONTEXT_KEY_PREFIX = "__macro_%s_temp_variable_%d__"; + private static final String CONTEXT_KEY_PREFIX = "__macro_%s_%d_temp_variable_%d__"; private final String deferredResult; public MacroFunctionTempVariable(String deferredResult) { this.deferredResult = deferredResult; } - public static String getVarName(String macroFunctionName, int callCount) { - return String.format(CONTEXT_KEY_PREFIX, macroFunctionName, callCount); + public static String getVarName(String macroFunctionName, int hashCode, int callCount) { + return String.format( + CONTEXT_KEY_PREFIX, + macroFunctionName, + Math.abs(hashCode), + callCount + ); } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java index cb07e89c0..c63f9453b 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java @@ -89,6 +89,7 @@ public Object doEvaluate( ) { String tempVarName = MacroFunctionTempVariable.getVarName( getName(), + hashCode(), currentCallCount ); interpreter From f992386077f2f07a6b7f9bbfdaab5b15382d6098 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 17 Nov 2022 10:53:57 -0500 Subject: [PATCH 058/637] Update expected tests after adding hashcode --- .../eager/eagerly-defers-macro.expected.jinja | 8 ++++---- .../eager/fully-defers-filtered-macro.expected.jinja | 2 +- ...s-deferred-for-loop-var-from-macro.expected.jinja | 12 ++++++------ ...ts-deferred-fromed-macro-in-output.expected.jinja | 2 +- ...ts-block-set-variables-in-for-loop.expected.jinja | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/test/resources/eager/eagerly-defers-macro.expected.jinja b/src/test/resources/eager/eagerly-defers-macro.expected.jinja index bbf836e2d..529762396 100644 --- a/src/test/resources/eager/eagerly-defers-macro.expected.jinja +++ b/src/test/resources/eager/eagerly-defers-macro.expected.jinja @@ -1,6 +1,6 @@ -{% set __macro_big_guy_temp_variable_0__ %} +{% set __macro_big_guy_1311704000_temp_variable_0__ %} {% if deferred %}I am foo{% else %}I am bar{% endif %} -{% endset %}{% print __macro_big_guy_temp_variable_0__ %} -{% set __macro_big_guy_temp_variable_1__ %} +{% endset %}{% print __macro_big_guy_1311704000_temp_variable_0__ %} +{% set __macro_big_guy_1311704000_temp_variable_1__ %} {% if deferred %}No more foo{% else %}I am bar{% endif %} -{% endset %}{% print __macro_big_guy_temp_variable_1__ %} +{% endset %}{% print __macro_big_guy_1311704000_temp_variable_1__ %} diff --git a/src/test/resources/eager/fully-defers-filtered-macro.expected.jinja b/src/test/resources/eager/fully-defers-filtered-macro.expected.jinja index 352034b90..3fd6290f0 100644 --- a/src/test/resources/eager/fully-defers-filtered-macro.expected.jinja +++ b/src/test/resources/eager/fully-defers-filtered-macro.expected.jinja @@ -2,4 +2,4 @@ A flashy {{ deferred }}.{% endmacro %}{{ flashy(flashy('bar')) }} --- -{% set __macro_silly_temp_variable_0__ %}A silly {{ deferred }}.{% endset %}{{ filter:upper.filter(__macro_silly_temp_variable_0__, ____int3rpr3t3r____) }} +{% set __macro_silly_2092874071_temp_variable_0__ %}A silly {{ deferred }}.{% endset %}{{ filter:upper.filter(__macro_silly_2092874071_temp_variable_0__, ____int3rpr3t3r____) }} diff --git a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja index c650a5d45..9c50b1240 100644 --- a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja +++ b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja @@ -1,13 +1,13 @@ -{% set __macro_getData_temp_variable_0__ %} +{% set __macro_getData_357124436_temp_variable_0__ %} {% for __ignored__ in [0] %} -{% set __macro_doIt_temp_variable_0__ %} +{% set __macro_doIt_1327224118_temp_variable_0__ %} {{ deferred ~ '{\"a\":\"a\"}' }} -{% endset %}{{ filter:upper.filter(__macro_doIt_temp_variable_0__, ____int3rpr3t3r____) }} +{% endset %}{{ filter:upper.filter(__macro_doIt_1327224118_temp_variable_0__, ____int3rpr3t3r____) }} -{% set __macro_doIt_temp_variable_1__ %} +{% set __macro_doIt_1327224118_temp_variable_1__ %} {{ deferred ~ '{\"b\":\"b\"}' }} -{% endset %}{{ filter:upper.filter(__macro_doIt_temp_variable_1__, ____int3rpr3t3r____) }} +{% endset %}{{ filter:upper.filter(__macro_doIt_1327224118_temp_variable_1__, ____int3rpr3t3r____) }} {% endfor %} -{% endset %}{{ filter:upper.filter(__macro_getData_temp_variable_0__, ____int3rpr3t3r____) }} +{% endset %}{{ filter:upper.filter(__macro_getData_357124436_temp_variable_0__, ____int3rpr3t3r____) }} diff --git a/src/test/resources/eager/puts-deferred-fromed-macro-in-output.expected.jinja b/src/test/resources/eager/puts-deferred-fromed-macro-in-output.expected.jinja index 9241dd338..be059f126 100644 --- a/src/test/resources/eager/puts-deferred-fromed-macro-in-output.expected.jinja +++ b/src/test/resources/eager/puts-deferred-fromed-macro-in-output.expected.jinja @@ -1 +1 @@ -{% set myname = deferred + 3 %}{% set __macro_getPath_temp_variable_1__ %}Hello {{ myname }}{% endset %}{% print __macro_getPath_temp_variable_1__ %} +{% set myname = deferred + 3 %}{% set __macro_getPath_331491059_temp_variable_1__ %}Hello {{ myname }}{% endset %}{% print __macro_getPath_331491059_temp_variable_1__ %} diff --git a/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop.expected.jinja b/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop.expected.jinja index 331e4f754..a1b04e857 100644 --- a/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop.expected.jinja +++ b/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop.expected.jinja @@ -1,5 +1,5 @@ {% for i in range(deferred) %} -{% set __macro_foo_temp_variable_0__ %} +{% set __macro_foo_97643642_temp_variable_0__ %} {{ deferred }} -{% endset %}{{ filter:int.filter(__macro_foo_temp_variable_0__, ____int3rpr3t3r____) + 3 }} +{% endset %}{{ filter:int.filter(__macro_foo_97643642_temp_variable_0__, ____int3rpr3t3r____) + 3 }} {% endfor %} From a6fcfabedebda0fa8f1ce9233a44bf43e3e0f486 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 17 Nov 2022 11:10:03 -0500 Subject: [PATCH 059/637] Test that deferred macro temp variable names are unique across different import paths --- src/test/java/com/hubspot/jinjava/EagerTest.java | 13 +++++++++++++ .../uses-unique-macro-names.expected.expected.jinja | 3 +++ .../eager/uses-unique-macro-names.expected.jinja | 10 ++++++++++ .../resources/eager/uses-unique-macro-names.jinja | 8 ++++++++ .../resources/tags/macrotag/macro-with-filter.jinja | 4 ++++ 5 files changed, 38 insertions(+) create mode 100644 src/test/resources/eager/uses-unique-macro-names.expected.expected.jinja create mode 100644 src/test/resources/eager/uses-unique-macro-names.expected.jinja create mode 100644 src/test/resources/eager/uses-unique-macro-names.jinja create mode 100644 src/test/resources/tags/macrotag/macro-with-filter.jinja diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 320971781..6dc0e4e31 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1181,4 +1181,17 @@ public void itReconstructsNullVariablesInDeferredCaller() { "reconstructs-null-variables-in-deferred-caller" ); } + + @Test + public void itUsesUniqueMacroNames() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "uses-unique-macro-names" + ); + } + + @Test + public void itUsesUniqueMacroNamesSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput("uses-unique-macro-names.expected"); + } } diff --git a/src/test/resources/eager/uses-unique-macro-names.expected.expected.jinja b/src/test/resources/eager/uses-unique-macro-names.expected.expected.jinja new file mode 100644 index 000000000..b82681932 --- /dev/null +++ b/src/test/resources/eager/uses-unique-macro-names.expected.expected.jinja @@ -0,0 +1,3 @@ +GOODBYE RESOLVED + +HELLO RESOLVED diff --git a/src/test/resources/eager/uses-unique-macro-names.expected.jinja b/src/test/resources/eager/uses-unique-macro-names.expected.jinja new file mode 100644 index 000000000..713b24841 --- /dev/null +++ b/src/test/resources/eager/uses-unique-macro-names.expected.jinja @@ -0,0 +1,10 @@ +{% set myname = deferred %} + +{% set __macro_foo_97643642_temp_variable_0__ %} +Goodbye {{ myname }} +{% endset %}{% set a = filter:upper.filter(__macro_foo_97643642_temp_variable_0__, ____int3rpr3t3r____) %} +{% set __ignored__ %}{% set current_path = 'macro-with-filter.jinja' %} +{% set __macro_foo_927217348_temp_variable_0__ %}Hello {{ myname }}{% endset %}{% set b = filter:upper.filter(__macro_foo_927217348_temp_variable_0__, ____int3rpr3t3r____) %} +{% set current_path = '' %}{% endset %} +{{ a }} +{{ b }} diff --git a/src/test/resources/eager/uses-unique-macro-names.jinja b/src/test/resources/eager/uses-unique-macro-names.jinja new file mode 100644 index 000000000..88d437614 --- /dev/null +++ b/src/test/resources/eager/uses-unique-macro-names.jinja @@ -0,0 +1,8 @@ +{% set myname = deferred %} +{% macro foo() %} +Goodbye {{ myname }} +{% endmacro %} +{% set a = foo()|upper %} +{% import 'macro-with-filter.jinja' %} +{{ a }} +{{ b }} diff --git a/src/test/resources/tags/macrotag/macro-with-filter.jinja b/src/test/resources/tags/macrotag/macro-with-filter.jinja new file mode 100644 index 000000000..fd1869569 --- /dev/null +++ b/src/test/resources/tags/macrotag/macro-with-filter.jinja @@ -0,0 +1,4 @@ +{% macro foo() -%} +Hello {{ myname }} +{%- endmacro %} +{% set b = foo()|upper %} From 8bea6531f82f658d9e91bbb63081cea370d5721e Mon Sep 17 00:00:00 2001 From: Tyler Kindy Date: Thu, 17 Nov 2022 13:54:46 -0500 Subject: [PATCH 060/637] Add Babel-like datetime formatting filters (#953) This PR adds three new filters for formatting datetime objects using either built-in or custom formats using [the Unicode Locale Data Markup Language](https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns). These filters are inspired by [functions of the same names](https://babel.pocoo.org/en/latest/dates.html) from Python's popular Babel i18n library. The new `format_datetime` filter technically supersedes the existing `datetimeformat` filter (and its alias, `date`). The latter uses the C/Unix `strftime` pattern syntax as opposed to LDML, but accomplishes essentially the same task. As such, I've marked `datetimeformat` as deprecated. For backwards compatibility, it'll stick around for the foreseeable future, probably forever. --- .../lib/filter/DateTimeFormatFilter.java | 7 +- .../jinjava/lib/filter/DatetimeFilter.java | 6 +- .../jinjava/lib/filter/FilterLibrary.java | 6 + .../lib/filter/time/DateTimeFormatHelper.java | 87 +++++++ .../lib/filter/time/FormatDateFilter.java | 58 +++++ .../lib/filter/time/FormatDatetimeFilter.java | 60 +++++ .../lib/filter/time/FormatTimeFilter.java | 58 +++++ .../lib/filter/time/FormatDateFilterTest.java | 224 ++++++++++++++++ .../filter/time/FormatDatetimeFilterTest.java | 240 ++++++++++++++++++ .../lib/filter/time/FormatTimeFilterTest.java | 224 ++++++++++++++++ 10 files changed, 968 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/hubspot/jinjava/lib/filter/time/DateTimeFormatHelper.java create mode 100644 src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilter.java create mode 100644 src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilter.java create mode 100644 src/main/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilter.java create mode 100644 src/test/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilterTest.java create mode 100644 src/test/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilterTest.java create mode 100644 src/test/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilterTest.java diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilter.java index 39f230a9b..270e4a8c4 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilter.java @@ -7,6 +7,10 @@ import com.hubspot.jinjava.lib.fn.Functions; import com.hubspot.jinjava.objects.date.StrftimeFormatter; +/** + * @deprecated Superseded by {@link com.hubspot.jinjava.lib.filter.time.FormatDatetimeFilter} + */ +@Deprecated @JinjavaDoc( value = "Formats a date object", input = @JinjavaParam( @@ -38,7 +42,8 @@ @JinjavaSnippet( code = "{% content.updated|datetimeformat('%a %A %w %d %e %b %B %m %y %Y %H %I %k %l %p %M %S %f %z %Z %j %U %W %c %x %X %%') %}" ) - } + }, + deprecated = true ) public class DateTimeFormatFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/DatetimeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/DatetimeFilter.java index 1f94eb8a0..8a1038f4a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/DatetimeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/DatetimeFilter.java @@ -18,7 +18,11 @@ import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -@JinjavaDoc(value = "", aliasOf = "datetimeformat") +/** + * @deprecated Superseded by {@link com.hubspot.jinjava.lib.filter.time.FormatDatetimeFilter} + */ +@Deprecated +@JinjavaDoc(value = "", aliasOf = "datetimeformat", deprecated = true) public class DatetimeFilter extends DateTimeFormatFilter { @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java index 592101ec9..6fac24a36 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java @@ -16,6 +16,9 @@ package com.hubspot.jinjava.lib.filter; import com.hubspot.jinjava.lib.SimpleLibrary; +import com.hubspot.jinjava.lib.filter.time.FormatDateFilter; +import com.hubspot.jinjava.lib.filter.time.FormatDatetimeFilter; +import com.hubspot.jinjava.lib.filter.time.FormatTimeFilter; import java.util.Set; public class FilterLibrary extends SimpleLibrary { @@ -57,6 +60,9 @@ protected void registerDefaults() { FloatFilter.class, ForceEscapeFilter.class, FormatFilter.class, + FormatDateFilter.class, + FormatDatetimeFilter.class, + FormatTimeFilter.class, FromJsonFilter.class, FromYamlFilter.class, GroupByFilter.class, diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/time/DateTimeFormatHelper.java b/src/main/java/com/hubspot/jinjava/lib/filter/time/DateTimeFormatHelper.java new file mode 100644 index 000000000..0f4026633 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/time/DateTimeFormatHelper.java @@ -0,0 +1,87 @@ +package com.hubspot.jinjava.lib.filter.time; + +import com.hubspot.jinjava.interpret.InvalidArgumentException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.fn.Functions; +import com.hubspot.jinjava.objects.date.InvalidDateFormatException; +import java.time.DateTimeException; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.IllformedLocaleException; +import java.util.Locale; +import java.util.Optional; +import java.util.function.Function; + +final class DateTimeFormatHelper { + private final String name; + private final Function cannedFormatterFunction; + + DateTimeFormatHelper( + String name, + Function cannedFormatterFunction + ) { + this.name = name; + this.cannedFormatterFunction = cannedFormatterFunction; + } + + String format(Object var, JinjavaInterpreter interpreter, String... args) { + String format = arg(args, 0).orElse("medium"); + ZoneId zoneId = arg(args, 1).map(this::parseZone).orElse(ZoneOffset.UTC); + Locale locale = arg(args, 2) + .map(this::parseLocale) + .orElseGet(() -> interpreter.getConfig().getLocale()); + + return buildFormatter(format) + .withLocale(locale) + .format(Functions.getDateTimeArg(var, zoneId)); + } + + private static Optional arg(String[] args, int index) { + return args.length > index ? Optional.ofNullable(args[index]) : Optional.empty(); + } + + private ZoneId parseZone(String zone) { + try { + return ZoneId.of(zone); + } catch (DateTimeException e) { + throw new InvalidArgumentException( + JinjavaInterpreter.getCurrent(), + name, + "Invalid time zone: " + zone + ); + } + } + + private Locale parseLocale(String locale) { + try { + return new Locale.Builder().setLanguageTag(locale).build(); + } catch (IllformedLocaleException e) { + throw new InvalidArgumentException( + JinjavaInterpreter.getCurrent(), + name, + "Invalid locale: " + locale + ); + } + } + + private DateTimeFormatter buildFormatter(String format) { + switch (format) { + case "short": + return cannedFormatterFunction.apply(FormatStyle.SHORT); + case "medium": + return cannedFormatterFunction.apply(FormatStyle.MEDIUM); + case "long": + return cannedFormatterFunction.apply(FormatStyle.LONG); + case "full": + return cannedFormatterFunction.apply(FormatStyle.FULL); + default: + try { + return DateTimeFormatter.ofPattern(format); + } catch (IllegalArgumentException e) { + throw new InvalidDateFormatException(format, e); + } + } + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilter.java new file mode 100644 index 000000000..3dd2fc1d0 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilter.java @@ -0,0 +1,58 @@ +package com.hubspot.jinjava.lib.filter.time; + +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; +import com.hubspot.jinjava.doc.annotations.JinjavaParam; +import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.filter.Filter; +import java.time.format.DateTimeFormatter; + +@JinjavaDoc( + value = "Formats the date component of a date object", + input = @JinjavaParam( + value = "value", + desc = "The date object or Unix timestamp to format", + required = true + ), + params = { + @JinjavaParam( + value = "format", + defaultValue = "medium", + desc = "The format to use. One of 'short', 'medium', 'long', 'full', or a custom pattern following Unicode LDML\nhttps://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns" + ), + @JinjavaParam( + value = "timeZone", + defaultValue = "UTC", + desc = "Time zone of the output date in IANA TZDB format\nhttps://data.iana.org/time-zones/tzdb/" + ), + @JinjavaParam( + value = "locale", + defaultValue = "Locale specified on JinjavaConfig", + desc = "The locale to use for locale-aware formats" + ) + }, + snippets = { + @JinjavaSnippet(code = "{{ content.updated | format_date('long') }}"), + @JinjavaSnippet(code = "{{ content.updated | format_date('yyyyy.MMMM.dd') }}"), + @JinjavaSnippet( + code = "{{ content.updated | format_date('medium', 'America/New_York', 'de-DE') }}" + ) + } +) +public class FormatDateFilter implements Filter { + private static final String NAME = "format_date"; + private static final DateTimeFormatHelper HELPER = new DateTimeFormatHelper( + NAME, + DateTimeFormatter::ofLocalizedDate + ); + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + return HELPER.format(var, interpreter, args); + } + + @Override + public String getName() { + return NAME; + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilter.java new file mode 100644 index 000000000..3fa8f743a --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilter.java @@ -0,0 +1,60 @@ +package com.hubspot.jinjava.lib.filter.time; + +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; +import com.hubspot.jinjava.doc.annotations.JinjavaParam; +import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.filter.Filter; +import java.time.format.DateTimeFormatter; + +@JinjavaDoc( + value = "Formats both the date and time components of a date object", + input = @JinjavaParam( + value = "value", + desc = "The date object or Unix timestamp to format", + required = true + ), + params = { + @JinjavaParam( + value = "format", + defaultValue = "medium", + desc = "The format to use. One of 'short', 'medium', 'long', 'full', or a custom pattern following Unicode LDML\nhttps://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns" + ), + @JinjavaParam( + value = "timeZone", + defaultValue = "UTC", + desc = "Time zone of the output date in IANA TZDB format\nhttps://data.iana.org/time-zones/tzdb/" + ), + @JinjavaParam( + value = "locale", + defaultValue = "Locale specified on JinjavaConfig", + desc = "The locale to use for locale-aware formats" + ) + }, + snippets = { + @JinjavaSnippet(code = "{{ content.updated | format_datetime('long') }}"), + @JinjavaSnippet( + code = "{{ content.updated | format_datetime('yyyyy.MMMM.dd GGG hh:mm a') }}" + ), + @JinjavaSnippet( + code = "{{ content.updated | format_datetime('medium', 'America/New_York', 'de-DE') }}" + ) + } +) +public class FormatDatetimeFilter implements Filter { + private static final String NAME = "format_datetime"; + private static final DateTimeFormatHelper HELPER = new DateTimeFormatHelper( + NAME, + DateTimeFormatter::ofLocalizedDateTime + ); + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + return HELPER.format(var, interpreter, args); + } + + @Override + public String getName() { + return NAME; + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilter.java new file mode 100644 index 000000000..859bab252 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilter.java @@ -0,0 +1,58 @@ +package com.hubspot.jinjava.lib.filter.time; + +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; +import com.hubspot.jinjava.doc.annotations.JinjavaParam; +import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.filter.Filter; +import java.time.format.DateTimeFormatter; + +@JinjavaDoc( + value = "Formats the time component of a date object", + input = @JinjavaParam( + value = "value", + desc = "The date object or Unix timestamp to format", + required = true + ), + params = { + @JinjavaParam( + value = "format", + defaultValue = "medium", + desc = "The format to use. One of 'short', 'medium', 'long', 'full', or a custom pattern following Unicode LDML\nhttps://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns" + ), + @JinjavaParam( + value = "timeZone", + defaultValue = "UTC", + desc = "Time zone of the output date in IANA TZDB format\nhttps://data.iana.org/time-zones/tzdb/" + ), + @JinjavaParam( + value = "locale", + defaultValue = "Locale specified on JinjavaConfig", + desc = "The locale to use for locale-aware formats" + ) + }, + snippets = { + @JinjavaSnippet(code = "{{ content.updated | format_time('long') }}"), + @JinjavaSnippet(code = "{{ content.updated | format_time('hh:mm a') }}"), + @JinjavaSnippet( + code = "{{ content.updated | format_time('medium', 'America/New_York', 'de-DE') }}" + ) + } +) +public class FormatTimeFilter implements Filter { + private static final String NAME = "format_time"; + private static final DateTimeFormatHelper HELPER = new DateTimeFormatHelper( + NAME, + DateTimeFormatter::ofLocalizedTime + ); + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + return HELPER.format(var, interpreter, args); + } + + @Override + public String getName() { + return NAME; + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilterTest.java new file mode 100644 index 000000000..252074e7b --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilterTest.java @@ -0,0 +1,224 @@ +package com.hubspot.jinjava.lib.filter.time; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.interpret.RenderResult; +import com.hubspot.jinjava.objects.date.PyishDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import org.junit.Before; +import org.junit.Test; + +public class FormatDateFilterTest { + private static final ZonedDateTime DATE_TIME = ZonedDateTime.of( + 2022, + 11, + 10, + 22, + 49, + 7, + 0, + ZoneOffset.UTC + ); + + Jinjava jinjava; + + @Before + public void setUp() throws Exception { + jinjava = new Jinjava(); + jinjava.getGlobalContext().registerClasses(FormatDateFilter.class); + } + + @Test + public void itFormatsNumbers() { + assertThat( + jinjava.render("{{ d | format_date }}", ImmutableMap.of("d", 1668120547000L)) + ) + .isEqualTo("Nov 10, 2022"); + } + + @Test + public void itFormatsPyishDates() { + PyishDate pyishDate = new PyishDate(1668120547000L); + + assertThat(jinjava.render("{{ d | format_date }}", ImmutableMap.of("d", pyishDate))) + .isEqualTo("Nov 10, 2022"); + } + + @Test + public void itFormatsZonedDateTime() { + assertThat(jinjava.render("{{ d | format_date }}", ImmutableMap.of("d", DATE_TIME))) + .isEqualTo("Nov 10, 2022"); + } + + @Test + public void itHandlesInvalidDateInput() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_date }}", + ImmutableMap.of("d", "nonsense") + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Input to function must be a date object, was: class java.lang.String"); + } + + @Test + public void itUsesShortFormat() { + assertThat( + jinjava.render("{{ d | format_date('short') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isEqualTo("11/10/22"); + } + + @Test + public void itUsesMediumFormat() { + assertThat( + jinjava.render("{{ d | format_date('medium') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isEqualTo("Nov 10, 2022"); + } + + @Test + public void itUsesLongFormat() { + assertThat( + jinjava.render("{{ d | format_date('long') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isEqualTo("November 10, 2022"); + } + + @Test + public void itUsesFullFormat() { + assertThat( + jinjava.render("{{ d | format_date('full') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isEqualTo("Thursday, November 10, 2022"); + } + + @Test + public void itUsesCustomFormats() { + assertThat( + jinjava.render( + "{{ d | format_date('yyyyy.MMMM.dd') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("02022.November.10"); + } + + @Test + public void itHandlesInvalidFormats() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_date('fake pattern') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Invalid date format") + .contains("Unknown pattern letter: f"); + } + + @Test + public void itUsesGivenTimeZone() { + assertThat( + jinjava.render( + "{{ d | format_date('long', 'Asia/Jakarta') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("November 11, 2022"); + } + + @Test + public void itHandlesInvalidTimeZones() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_date('long', 'not a real time zone') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Invalid time zone: not a real time zone"); + } + + @Test + public void itHandlesEmptyTimeZones() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_date('long', '') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()).contains("Invalid time zone: "); + } + + @Test + public void itUsesGivenLocale() { + assertThat( + jinjava.render( + "{{ d | format_date('medium', 'America/New_York', 'de-DE') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("10.11.2022"); + } + + @Test + public void itHandlesInvalidLocales() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_date('medium', 'America/New_York', 'not a real locale') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Invalid locale: not a real locale"); + } + + @Test + public void itHandlesEmptyLocales() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_date('medium', 'America/New_York', '') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()).contains("Invalid locale: "); + } + + @Test + public void itUsesMediumIfNullFormatPassed() { + assertThat( + jinjava.render( + "{{ d | format_date(null, 'America/New_York', 'de-DE') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("10.11.2022"); + } + + @Test + public void itUsesUtcIfNullZonePassed() { + assertThat( + jinjava.render( + "{{ d | format_date('short', null, 'de-DE') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("10.11.22"); + } + + @Test + public void itUsesJinjavaConfigIfNullLocalePassed() { + assertThat( + jinjava.render( + "{{ d | format_date('short', 'America/New_York', null) }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("11/10/22"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilterTest.java new file mode 100644 index 000000000..a1d4c7266 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilterTest.java @@ -0,0 +1,240 @@ +package com.hubspot.jinjava.lib.filter.time; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.interpret.RenderResult; +import com.hubspot.jinjava.objects.date.PyishDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import org.junit.Before; +import org.junit.Test; + +public class FormatDatetimeFilterTest { + private static final ZonedDateTime DATE_TIME = ZonedDateTime.of( + 2022, + 11, + 10, + 22, + 49, + 7, + 0, + ZoneOffset.UTC + ); + + Jinjava jinjava; + + @Before + public void setUp() throws Exception { + jinjava = new Jinjava(); + jinjava.getGlobalContext().registerClasses(FormatDatetimeFilter.class); + } + + @Test + public void itFormatsNumbers() { + assertThat( + jinjava.render("{{ d | format_datetime }}", ImmutableMap.of("d", 1668120547000L)) + ) + .isEqualTo("Nov 10, 2022, 10:49:07 PM"); + } + + @Test + public void itFormatsPyishDates() { + PyishDate pyishDate = new PyishDate(1668120547000L); + + assertThat( + jinjava.render("{{ d | format_datetime }}", ImmutableMap.of("d", pyishDate)) + ) + .isEqualTo("Nov 10, 2022, 10:49:07 PM"); + } + + @Test + public void itFormatsZonedDateTime() { + assertThat( + jinjava.render("{{ d | format_datetime }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isEqualTo("Nov 10, 2022, 10:49:07 PM"); + } + + @Test + public void itHandlesInvalidDateInput() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_datetime }}", + ImmutableMap.of("d", "nonsense") + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Input to function must be a date object, was: class java.lang.String"); + } + + @Test + public void itUsesShortFormat() { + assertThat( + jinjava.render( + "{{ d | format_datetime('short') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("11/10/22, 10:49 PM"); + } + + @Test + public void itUsesMediumFormat() { + assertThat( + jinjava.render( + "{{ d | format_datetime('medium') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("Nov 10, 2022, 10:49:07 PM"); + } + + @Test + public void itUsesLongFormat() { + assertThat( + jinjava.render( + "{{ d | format_datetime('long') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("November 10, 2022 at 10:49:07 PM Z"); + } + + @Test + public void itUsesFullFormat() { + assertThat( + jinjava.render( + "{{ d | format_datetime('full') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("Thursday, November 10, 2022 at 10:49:07 PM Z"); + } + + @Test + public void itUsesCustomFormats() { + assertThat( + jinjava.render( + "{{ d | format_datetime('yyyyy.MMMM.dd GGG hh:mm a') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("02022.November.10 AD 10:49 PM"); + } + + @Test + public void itHandlesInvalidFormats() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_datetime('fake pattern') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Invalid date format") + .contains("Unknown pattern letter: f"); + } + + @Test + public void itUsesGivenTimeZone() { + assertThat( + jinjava.render( + "{{ d | format_datetime('long', 'America/New_York') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("November 10, 2022 at 5:49:07 PM EST"); + } + + @Test + public void itHandlesInvalidTimeZones() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_datetime('long', 'not a real time zone') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Invalid time zone: not a real time zone"); + } + + @Test + public void itHandlesEmptyTimeZones() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_datetime('long', '') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()).contains("Invalid time zone: "); + } + + @Test + public void itUsesGivenLocale() { + assertThat( + jinjava.render( + "{{ d | format_datetime('medium', 'America/New_York', 'de-DE') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("10.11.2022, 17:49:07"); + } + + @Test + public void itHandlesInvalidLocales() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_datetime('medium', 'America/New_York', 'not a real locale') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Invalid locale: not a real locale"); + } + + @Test + public void itHandlesEmptyLocales() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_datetime('medium', 'America/New_York', '') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()).contains("Invalid locale: "); + } + + @Test + public void itUsesMediumIfNullFormatPassed() { + assertThat( + jinjava.render( + "{{ d | format_datetime(null, 'America/New_York', 'de-DE') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("10.11.2022, 17:49:07"); + } + + @Test + public void itUsesUtcIfNullZonePassed() { + assertThat( + jinjava.render( + "{{ d | format_datetime('short', null, 'de-DE') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("10.11.22, 22:49"); + } + + @Test + public void itUsesJinjavaConfigIfNullLocalePassed() { + assertThat( + jinjava.render( + "{{ d | format_datetime('short', 'America/New_York', null) }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("11/10/22, 5:49 PM"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilterTest.java new file mode 100644 index 000000000..f41f45731 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilterTest.java @@ -0,0 +1,224 @@ +package com.hubspot.jinjava.lib.filter.time; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.interpret.RenderResult; +import com.hubspot.jinjava.objects.date.PyishDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import org.junit.Before; +import org.junit.Test; + +public class FormatTimeFilterTest { + private static final ZonedDateTime DATE_TIME = ZonedDateTime.of( + 2022, + 11, + 10, + 22, + 49, + 7, + 0, + ZoneOffset.UTC + ); + + Jinjava jinjava; + + @Before + public void setUp() throws Exception { + jinjava = new Jinjava(); + jinjava.getGlobalContext().registerClasses(FormatTimeFilter.class); + } + + @Test + public void itFormatsNumbers() { + assertThat( + jinjava.render("{{ d | format_time }}", ImmutableMap.of("d", 1668120547000L)) + ) + .isEqualTo("10:49:07 PM"); + } + + @Test + public void itFormatsPyishDates() { + PyishDate pyishDate = new PyishDate(1668120547000L); + + assertThat(jinjava.render("{{ d | format_time }}", ImmutableMap.of("d", pyishDate))) + .isEqualTo("10:49:07 PM"); + } + + @Test + public void itFormatsZonedDateTime() { + assertThat(jinjava.render("{{ d | format_time }}", ImmutableMap.of("d", DATE_TIME))) + .isEqualTo("10:49:07 PM"); + } + + @Test + public void itHandlesInvalidDateInput() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_time }}", + ImmutableMap.of("d", "nonsense") + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Input to function must be a date object, was: class java.lang.String"); + } + + @Test + public void itUsesShortFormat() { + assertThat( + jinjava.render("{{ d | format_time('short') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isEqualTo("10:49 PM"); + } + + @Test + public void itUsesMediumFormat() { + assertThat( + jinjava.render("{{ d | format_time('medium') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isEqualTo("10:49:07 PM"); + } + + @Test + public void itUsesLongFormat() { + assertThat( + jinjava.render("{{ d | format_time('long') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isEqualTo("10:49:07 PM Z"); + } + + @Test + public void itUsesFullFormat() { + assertThat( + jinjava.render("{{ d | format_time('full') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isEqualTo("10:49:07 PM Z"); + } + + @Test + public void itUsesCustomFormats() { + assertThat( + jinjava.render( + "{{ d | format_time('hh:mm a') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("10:49 PM"); + } + + @Test + public void itHandlesInvalidFormats() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_time('fake pattern') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Invalid date format") + .contains("Unknown pattern letter: f"); + } + + @Test + public void itUsesGivenTimeZone() { + assertThat( + jinjava.render( + "{{ d | format_time('long', 'America/New_York') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("5:49:07 PM EST"); + } + + @Test + public void itHandlesInvalidTimeZones() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_time('long', 'not a real time zone') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Invalid time zone: not a real time zone"); + } + + @Test + public void itHandlesEmptyTimeZones() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_time('long', '') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()).contains("Invalid time zone: "); + } + + @Test + public void itUsesGivenLocale() { + assertThat( + jinjava.render( + "{{ d | format_time('medium', 'America/New_York', 'de-DE') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("17:49:07"); + } + + @Test + public void itHandlesInvalidLocales() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_time('medium', 'America/New_York', 'not a real locale') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Invalid locale: not a real locale"); + } + + @Test + public void itHandlesEmptyLocales() { + RenderResult result = jinjava.renderForResult( + "{{ d | format_time('medium', 'America/New_York', '') }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(result.getOutput()).isEqualTo(""); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()).contains("Invalid locale: "); + } + + @Test + public void itUsesMediumIfNullFormatPassed() { + assertThat( + jinjava.render( + "{{ d | format_time(null, 'America/New_York', 'de-DE') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("17:49:07"); + } + + @Test + public void itUsesUtcIfNullZonePassed() { + assertThat( + jinjava.render( + "{{ d | format_time('short', null, 'de-DE') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("22:49"); + } + + @Test + public void itUsesJinjavaConfigIfNullLocalePassed() { + assertThat( + jinjava.render( + "{{ d | format_time('short', 'America/New_York', null) }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isEqualTo("5:49 PM"); + } +} From bf9b540705ea2fbcae85e66d4452405402a78a3b Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 17 Nov 2022 14:33:10 -0500 Subject: [PATCH 061/637] Always add spacing when ending an object in the pyish object mapper To prevent situations where something is serialized and it appears to have the end expression symbol in it --- .../objects/serialization/PyishObjectMapper.java | 16 ---------------- .../serialization/PyishPrettyPrinter.java | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 6778e9b4f..436071444 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -57,22 +57,6 @@ public static String getAsPyishStringOrThrow(Object val) if (maxStringLength.map(max -> string.length() > max).orElse(false)) { throw new OutputTooBigException(maxStringLength.get(), string.length()); } - if ( - interpreterMaybe - .map( - interpreter -> - interpreter - .getConfig() - .getTokenScannerSymbols() - .getExpressionEnd() - .equals("}}") - ) - .orElse(true) && - string.contains("}}") && - !string.contains("{{") - ) { - return String.join("} ", string.split("}(?=})")); - } return string; } diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishPrettyPrinter.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishPrettyPrinter.java index b0d5dc51f..a89121bb7 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishPrettyPrinter.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishPrettyPrinter.java @@ -41,6 +41,6 @@ public void writeEndObject(JsonGenerator jg, int nrOfEntries) throws IOException if (!this._objectIndenter.isInline()) { --this._nesting; } - jg.writeRaw('}'); + jg.writeRaw("} "); } } From 8ee4b42d7668b4d6e22d3816b101e64e688e5e48 Mon Sep 17 00:00:00 2001 From: Stevie Gutz Date: Fri, 18 Nov 2022 07:35:08 -0500 Subject: [PATCH 062/637] Pin build to JDK 11 --- .build-jdk11 | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .build-jdk11 diff --git a/.build-jdk11 b/.build-jdk11 new file mode 100644 index 000000000..e69de29bb From dbd7126a131f15b9e79cf1c0017492d73c28e58b Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 18 Nov 2022 10:30:19 -0500 Subject: [PATCH 063/637] Update tests to expect space after closing curly brace --- src/test/java/com/hubspot/jinjava/EagerTest.java | 2 +- .../java/com/hubspot/jinjava/lib/tag/MacroTagTest.java | 2 +- .../hubspot/jinjava/lib/tag/eager/EagerForTagTest.java | 4 ++-- .../hubspot/jinjava/lib/tag/eager/EagerImportTagTest.java | 2 +- .../objects/serialization/PyishObjectMapperTest.java | 6 +++--- ...not-override-import-modification-in-for.expected.jinja | 8 ++++---- .../eager/handles-deferred-import-vars.expected.jinja | 2 +- .../eager/handles-deferred-in-namespace.expected.jinja | 4 ++-- .../handles-double-import-modification.expected.jinja | 6 +++--- .../eager/handles-same-name-import-var.expected.jinja | 4 ++-- .../eager/reconstructs-types-properly.expected.jinja | 2 +- 11 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 6dc0e4e31..e6156c7b4 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -485,7 +485,7 @@ public void itMarksVariablesUsedAsMapKeysAsDeferred() { assertThat(deferredValue2).isInstanceOf(DeferredValue.class); assertThat(output) .contains( - "{% set varSetInside = {'key': 'value'}[deferredValue2.nonexistentprop] %}" + "{% set varSetInside = {'key': 'value'} [deferredValue2.nonexistentprop] %}" ); } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/MacroTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/MacroTagTest.java index 1a52f5ad4..84a1f0160 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/MacroTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/MacroTagTest.java @@ -316,7 +316,7 @@ public void itPreventsRecursionForMacroWithVar() { ""; Node node = new TreeParser(interpreter, jinja).buildTree(); assertThat(interpreter.render(node)) - .isEqualTo("{'f': {'val': '{'f': {'val': '{{ self }}'}}'}}"); + .isEqualTo("{'f': {'val': '{'f': {'val': '{{ self }}'} }'} }"); } @Test diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java index 2609fca1b..279757d4d 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java @@ -151,10 +151,10 @@ public void itDoesntAllowChangesInDeferredForWithSameHashCode() { ); assertThat(result) .isEqualTo( - "{% set foo = {'a': 'a'} %}{% for i in range(0, deferred) %}\n" + + "{% set foo = {'a': 'a'} %}{% for i in range(0, deferred) %}\n" + "bar{{ foo }}\n" + "{% do foo.clear() %}\n" + - "{% do foo.update({'b': 'b'}) %}\n" + + "{% do foo.update({'b': 'b'} ) %}\n" + "{% endfor %}\n" + "{{ foo }}" ); 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 401e6c1c7..1d79b84c2 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 @@ -462,7 +462,7 @@ public void itHandlesQuadLayerInDeferredIf() { ); assertThat(result) .isEqualTo( - "{% if deferred %}{% set __ignored__ %}{% set current_path = 'import-tree-b.jinja' %}{% set a,foo_b = {'foo_a': 'a', 'import_resource_path': 'import-tree-a.jinja', 'something': 'somn'},null %}{% set b = {} %}{% set __ignored__ %}{% set current_path = 'import-tree-a.jinja' %}{% set a = {} %}{% set something = 'somn' %}{% do a.update({'something': something}) %}\n" + + "{% if deferred %}{% set __ignored__ %}{% set current_path = 'import-tree-b.jinja' %}{% set a,foo_b = {'foo_a': 'a', 'import_resource_path': 'import-tree-a.jinja', 'something': 'somn'} ,null %}{% set b = {} %}{% set __ignored__ %}{% set current_path = 'import-tree-a.jinja' %}{% set a = {} %}{% set something = 'somn' %}{% do a.update({'something': something}) %}\n" + "{% set foo_a = 'a' %}{% do a.update({'foo_a': foo_a}) %}\n" + "{% do a.update({'foo_a': 'a','import_resource_path': 'import-tree-a.jinja','something': 'somn'}) %}{% set current_path = 'import-tree-b.jinja' %}{% endset %}\n" + "{% set foo_b = 'b' + a.foo_a %}{% do b.update({'foo_b': foo_b}) %}\n" + diff --git a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java index 2b888438d..17dc8c5e6 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -18,7 +18,7 @@ public void itSerializesMapWithNullKeysAsEmptyString() { map.put("foo", "bar"); map.put(null, "null"); assertThat(PyishObjectMapper.getAsPyishString(map)) - .isEqualTo("{'': 'null', 'foo': 'bar'}"); + .isEqualTo("{'': 'null', 'foo': 'bar'} "); } @Test @@ -28,7 +28,7 @@ public void itSerializesMapEntrySet() throws JsonProcessingException { map.put("bar", ImmutableMap.of("foobar", new ArrayList<>())); String result = PyishObjectMapper.getAsPyishString(map.items()); assertThat(result) - .isEqualTo("[fn:map_entry('bar', {'foobar': []}), fn:map_entry('foo', 'bar')]"); + .isEqualTo("[fn:map_entry('bar', {'foobar': []} ), fn:map_entry('foo', 'bar')]"); } @Test @@ -37,6 +37,6 @@ public void itSerializesMapWithNullValues() { map.put("foo", "bar"); map.put("foobar", null); assertThat(PyishObjectMapper.getAsPyishString(map)) - .isEqualTo("{'foobar': null, 'foo': 'bar'}"); + .isEqualTo("{'foobar': null, 'foo': 'bar'} "); } } diff --git a/src/test/resources/eager/does-not-override-import-modification-in-for.expected.jinja b/src/test/resources/eager/does-not-override-import-modification-in-for.expected.jinja index 0e21ec7bc..cb56c613d 100644 --- a/src/test/resources/eager/does-not-override-import-modification-in-for.expected.jinja +++ b/src/test/resources/eager/does-not-override-import-modification-in-for.expected.jinja @@ -1,5 +1,5 @@ {% for __ignored__ in [0] %} -{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set bar1 = {} %}{% set bar1,foo = {},'start' %}{% if deferred %} +{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set bar1 = {} %}{% set bar1,foo = {} ,'start' %}{% if deferred %} {% set foo = 'starta' %}{% do bar1.update({'foo': foo}) %} @@ -8,7 +8,7 @@ {% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}{% do bar1.update({'foo': foo}) %} {% do bar1.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% endset %} {{ bar1.foo }} -{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set bar2 = {} %}{% set bar2,foo = {},'start' %}{% if deferred %} +{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set bar2 = {} %}{% set bar2,foo = {} ,'start' %}{% if deferred %} {% set foo = 'starta' %}{% do bar2.update({'foo': foo}) %} @@ -18,7 +18,7 @@ {% do bar2.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% endset %} {{ bar2.foo }} -{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set bar1 = {} %}{% set bar1,foo = {},'start' %}{% if deferred %} +{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set bar1 = {} %}{% set bar1,foo = {} ,'start' %}{% if deferred %} {% set foo = 'starta' %}{% do bar1.update({'foo': foo}) %} @@ -27,7 +27,7 @@ {% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}{% do bar1.update({'foo': foo}) %} {% do bar1.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% endset %} {{ bar1.foo }} -{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set bar2 = {} %}{% set bar2,foo = {},'start' %}{% if deferred %} +{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set bar2 = {} %}{% set bar2,foo = {} ,'start' %}{% if deferred %} {% set foo = 'starta' %}{% do bar2.update({'foo': foo}) %} diff --git a/src/test/resources/eager/handles-deferred-import-vars.expected.jinja b/src/test/resources/eager/handles-deferred-import-vars.expected.jinja index 8210627d6..f7008add8 100644 --- a/src/test/resources/eager/handles-deferred-import-vars.expected.jinja +++ b/src/test/resources/eager/handles-deferred-import-vars.expected.jinja @@ -5,7 +5,7 @@ Hello {{ myname }} bar: {{ bar }} --- {% set myname = deferred + 7 %}{% set __ignored__ %}{% set current_path = 'macro-and-set.jinja' %}{% set simple = {} %} -{% set bar = myname + 19 %}{% set simple = {} %}{% do simple.update({'bar': bar}) %} +{% set bar = myname + 19 %}{% set simple = {} %}{% do simple.update({'bar': bar}) %} Hello {{ myname }} {% do simple.update({'import_resource_path': 'macro-and-set.jinja'}) %}{% set current_path = '' %}{% endset %}simple.foo: {% set deferred_import_resource_path = 'macro-and-set.jinja' %}{% macro simple.foo() %}Hello {{ myname }}{% endmacro %}{% set deferred_import_resource_path = null %}{{ simple.foo() }} simple.bar: {{ simple.bar }} diff --git a/src/test/resources/eager/handles-deferred-in-namespace.expected.jinja b/src/test/resources/eager/handles-deferred-in-namespace.expected.jinja index 3a3108fb7..76cf0b9ea 100644 --- a/src/test/resources/eager/handles-deferred-in-namespace.expected.jinja +++ b/src/test/resources/eager/handles-deferred-in-namespace.expected.jinja @@ -1,5 +1,5 @@ -{% set ns2 = namespace({}) %}{% do ns2.update({'a': deferred}) %} -{% set ns1 = namespace({'a': false}) %}{% if deferred %} +{% set ns2 = namespace({} ) %}{% do ns2.update({'a': deferred}) %} +{% set ns1 = namespace({'a': false} ) %}{% if deferred %} {% set ns1.a = true %} {% endif %} {{ ns1.a }} diff --git a/src/test/resources/eager/handles-double-import-modification.expected.jinja b/src/test/resources/eager/handles-double-import-modification.expected.jinja index 34ca58f1e..bda97e368 100644 --- a/src/test/resources/eager/handles-double-import-modification.expected.jinja +++ b/src/test/resources/eager/handles-double-import-modification.expected.jinja @@ -1,4 +1,4 @@ -{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set foo = null %}{% set bar1 = {} %}{% set bar1 = {} %}{% if deferred %} +{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set foo = null %}{% set bar1 = {} %}{% set bar1 = {} %}{% if deferred %} {% set foo = 'a' %}{% do bar1.update({'foo': foo}) %} @@ -7,7 +7,7 @@ {% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}{% do bar1.update({'foo': foo}) %} {% do bar1.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% endset %} --- -{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set foo = null %}{% set bar2 = {} %}{% set bar2 = {} %}{% if deferred %} +{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set foo = null %}{% set bar2 = {} %}{% set bar2 = {} %}{% if deferred %} {% set foo = 'a' %}{% do bar2.update({'foo': foo}) %} @@ -17,4 +17,4 @@ {% do bar2.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% endset %} --- {{ bar1.foo }} -{{ bar2.foo }} \ No newline at end of file +{{ bar2.foo }} diff --git a/src/test/resources/eager/handles-same-name-import-var.expected.jinja b/src/test/resources/eager/handles-same-name-import-var.expected.jinja index 34d8af234..f773a8751 100644 --- a/src/test/resources/eager/handles-same-name-import-var.expected.jinja +++ b/src/test/resources/eager/handles-same-name-import-var.expected.jinja @@ -1,6 +1,6 @@ {% if deferred %} -{% set __ignored__ %}{% set current_path = '../settag/set-var-and-deferred.jinja' %}{% set current_path,value = null,null %}{% set my_var = {} %}{% set my_var = {} %}{% if deferred %} -{% set __ignored__ %}{% set current_path = '../settag/set-var-and-deferred.jinja' %}{% do my_var.update({'current_path': current_path}) %}{% set value = null %}{% do my_var.update({'value': value}) %}{% set my_var = {} %}{% set my_var = {'foo': 'bar'} %}{% set my_var = {'my_var': {'foo': 'bar'} } %} +{% set __ignored__ %}{% set current_path = '../settag/set-var-and-deferred.jinja' %}{% set current_path,value = null,null %}{% set my_var = {} %}{% set my_var = {} %}{% if deferred %} +{% set __ignored__ %}{% set current_path = '../settag/set-var-and-deferred.jinja' %}{% do my_var.update({'current_path': current_path}) %}{% set value = null %}{% do my_var.update({'value': value}) %}{% set my_var = {} %}{% set my_var = {'foo': 'bar'} %}{% set my_var = {'my_var': {'foo': 'bar'} } %} {% set value = deferred %}{% do my_var.update({'value': value}) %}{% do my_var.update({'value': value}) %} {% do my_var.update({'import_resource_path': '../settag/set-var-and-deferred.jinja', 'value': value}) %}{% set current_path = '' %}{% do my_var.update({'current_path': current_path}) %}{% endset %}{% do my_var.update({'__ignored__': __ignored__}) %} {{ my_var }} diff --git a/src/test/resources/eager/reconstructs-types-properly.expected.jinja b/src/test/resources/eager/reconstructs-types-properly.expected.jinja index 760060d05..0d978cddd 100644 --- a/src/test/resources/eager/reconstructs-types-properly.expected.jinja +++ b/src/test/resources/eager/reconstructs-types-properly.expected.jinja @@ -1 +1 @@ -{{ {'bool': 'true', 'num': '1'} ~ deferred }} +{{ {'bool': 'true', 'num': '1'} ~ deferred }} From f9f34a6f98153a93d8d0c2f42bb7a65c8affe611 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 21 Nov 2022 08:46:49 -0500 Subject: [PATCH 064/637] Remove hardcoded value for french grouping separator This will make these tests work in both java 11 and java 17 > CLDR has changed the grouping separator for French in v34, from U+00A0 to U+202F https://bugs.openjdk.org/browse/JDK-8225245?focusedCommentId=14268624&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-14268624 --- .../hubspot/jinjava/lib/filter/FloatFilterTest.java | 12 +++++++++++- .../hubspot/jinjava/lib/filter/IntFilterTest.java | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/FloatFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/FloatFilterTest.java index c69be685a..24b5418d1 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/FloatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FloatFilterTest.java @@ -21,6 +21,7 @@ import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; import java.nio.charset.StandardCharsets; +import java.text.DecimalFormatSymbols; import java.time.ZoneOffset; import java.util.Locale; import org.junit.Before; @@ -114,6 +115,15 @@ public void itDoesntInterpretUsCommasAndPeriodsWithFrenchLocale() { @Test public void itInterpretsFrenchCommasAndPeriodsWithFrenchLocale() { interpreter = new Jinjava(FRENCH_LOCALE_CONFIG).newInterpreter(); - assertThat(filter.filter("123\u00A0123,45", interpreter)).isEqualTo(123123.45f); + assertThat( + filter.filter( + String.format( + "123%c123,45", + DecimalFormatSymbols.getInstance(Locale.FRENCH).getGroupingSeparator() + ), + interpreter + ) + ) + .isEqualTo(123123.45f); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/IntFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/IntFilterTest.java index 1fd199ffb..972fe6661 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/IntFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/IntFilterTest.java @@ -6,6 +6,7 @@ import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; import java.nio.charset.StandardCharsets; +import java.text.DecimalFormatSymbols; import java.time.ZoneOffset; import java.util.Locale; import org.junit.Before; @@ -110,7 +111,16 @@ public void itDoesntInterpretUsCommasAndPeriodsWithFrenchLocale() { @Test public void itInterpretsFrenchCommasAndPeriodsWithFrenchLocale() { interpreter = new Jinjava(FRENCH_LOCALE_CONFIG).newInterpreter(); - assertThat(filter.filter("123\u00A0123,12", interpreter)).isEqualTo(123123); + assertThat( + filter.filter( + String.format( + "123%c123,12", + DecimalFormatSymbols.getInstance(Locale.FRENCH).getGroupingSeparator() + ), + interpreter + ) + ) + .isEqualTo(123123); } @Test From 48d093585ef6ddcf281f4d2a147c993380587992 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 21 Nov 2022 08:50:39 -0500 Subject: [PATCH 065/637] Check string length in eager execution, if it is too long then execute in deferred execution mode --- .../jinjava/interpret/JinjavaInterpreter.java | 9 +++ .../jinjava/lib/tag/eager/EagerForTag.java | 63 +++++++------------ .../jinjava/lib/tag/eager/EagerIfTag.java | 20 +----- .../lib/tag/eager/EagerIncludeTag.java | 4 +- .../lib/tag/eager/EagerStateChangingTag.java | 2 +- .../lib/tag/eager/EagerTagDecorator.java | 23 +++++-- .../serialization/PyishObjectMapper.java | 8 +-- .../lib/tag/eager/EagerTagDecoratorTest.java | 3 - 8 files changed, 55 insertions(+), 77 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index 2e6b0ebdc..fffc9a072 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -126,6 +126,15 @@ public JinjavaInterpreter(JinjavaInterpreter orig) { scopeDepth = orig.getScopeDepth() + 1; } + public static void checkStringLength(String string) { + Optional maxStringLength = getCurrentMaybe() + .map(interpreter -> interpreter.getConfig().getMaxStringLength()) + .filter(max -> max > 0); + if (maxStringLength.map(max -> string.length() > max).orElse(false)) { + throw new OutputTooBigException(maxStringLength.get(), string.length()); + } + } + /** * @deprecated use {{@link #getConfig()}} */ diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 3b2e127f8..019e8e673 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -7,9 +7,6 @@ import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.OutputTooBigException; -import com.hubspot.jinjava.interpret.TemplateError; -import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.tag.ForTag; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; @@ -37,7 +34,7 @@ public EagerForTag(ForTag forTag) { } @Override - public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { + public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { Set addedTokens = new HashSet<>(); EagerExecutionResult result = EagerReconstructionUtils.executeInChildContext( eagerInterpreter -> { @@ -51,42 +48,28 @@ public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { interpreter, EagerChildContextConfig.newBuilder().withCheckForContextChanges(true).build() ); - try { - if ( - result.getResult().getResolutionState() == ResolutionState.NONE || - ( - !result.getResult().isFullyResolved() && - !result.getSpeculativeBindings().isEmpty() - ) - ) { - EagerIfTag.resetBindingsForNextBranch(interpreter, result); - interpreter.getContext().removeDeferredTokens(addedTokens); - throw new DeferredValueException( - result.getResult().getResolutionState() == ResolutionState.NONE - ? result.getResult().toString() - : "Modification inside partially evaluated for loop" - ); - } - if (result.getResult().isFullyResolved()) { - return result.getResult().toString(true); - } else { - return EagerReconstructionUtils.wrapInChildScope( - result.getResult().toString(true), - interpreter - ); - } - } catch (DeferredValueException | TemplateSyntaxException e) { - try { - return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( - eagerInterpret(tagNode, interpreter, e), - interpreter - ); - } catch (OutputTooBigException e1) { - interpreter.addError(TemplateError.fromOutputTooBigException(e1)); - throw new DeferredValueException( - String.format("Output too big for eager execution: %s", e1.getMessage()) - ); - } + if ( + result.getResult().getResolutionState() == ResolutionState.NONE || + ( + !result.getResult().isFullyResolved() && + !result.getSpeculativeBindings().isEmpty() + ) + ) { + EagerIfTag.resetBindingsForNextBranch(interpreter, result); + interpreter.getContext().removeDeferredTokens(addedTokens); + throw new DeferredValueException( + result.getResult().getResolutionState() == ResolutionState.NONE + ? result.getResult().toString() + : "Modification inside partially evaluated for loop" + ); + } + if (result.getResult().isFullyResolved()) { + return result.getResult().toString(true); + } else { + return EagerReconstructionUtils.wrapInChildScope( + result.getResult().toString(true), + interpreter + ); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java index a65f61ebf..b4421bb91 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java @@ -4,8 +4,6 @@ import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.OutputTooBigException; -import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.tag.ElseIfTag; import com.hubspot.jinjava.lib.tag.ElseTag; @@ -33,22 +31,8 @@ public EagerIfTag(IfTag ifTag) { } @Override - public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { - try { - return getTag().interpret(tagNode, interpreter); - } catch (DeferredValueException | TemplateSyntaxException e) { - try { - return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( - eagerInterpret(tagNode, interpreter, e), - interpreter - ); - } catch (OutputTooBigException e1) { - interpreter.addError(TemplateError.fromOutputTooBigException(e1)); - throw new DeferredValueException( - String.format("Output too big for eager execution: %s", e1.getMessage()) - ); - } - } + public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { + return getTag().interpret(tagNode, interpreter); } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIncludeTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIncludeTag.java index b0d3bf175..abb613a80 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIncludeTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIncludeTag.java @@ -17,9 +17,9 @@ public EagerIncludeTag(IncludeTag tag) { } @Override - public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { + public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { int numDeferredTokensStart = interpreter.getContext().getDeferredTokens().size(); - String output = super.interpret(tagNode, interpreter); + String output = super.innerInterpret(tagNode, interpreter); if (interpreter.getContext().getDeferredTokens().size() > numDeferredTokensStart) { HelperStringTokenizer helper = new HelperStringTokenizer(tagNode.getHelpers()); String path = StringUtils.trimToEmpty(helper.next()); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java index f72048b33..0862a2bd8 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java @@ -18,7 +18,7 @@ public EagerStateChangingTag(T tag) { } @Override - public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { + public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { return eagerInterpret(tagNode, interpreter, null); } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java index cea34a9fc..7c6447104 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java @@ -6,7 +6,6 @@ import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.OutputTooBigException; -import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.tag.Tag; import com.hubspot.jinjava.tree.Node; @@ -34,17 +33,24 @@ public T getTag() { } @Override - public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { + public final String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { try { - return tag.interpret(tagNode, interpreter); - } catch (DeferredValueException | TemplateSyntaxException e) { + String output = innerInterpret(tagNode, interpreter); + JinjavaInterpreter.checkStringLength(output); + return output; + } catch (DeferredValueException | TemplateSyntaxException | OutputTooBigException e) { try { return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( - eagerInterpret(tagNode, interpreter, e), + eagerInterpret( + tagNode, + interpreter, + e instanceof InterpretException + ? (InterpretException) e + : new InterpretException("Exception with default render", e) + ), interpreter ); } catch (OutputTooBigException e1) { - interpreter.addError(TemplateError.fromOutputTooBigException(e1)); throw new DeferredValueException( String.format("Output too big for eager execution: %s", e1.getMessage()) ); @@ -52,6 +58,10 @@ public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { } } + protected String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { + return tag.interpret(tagNode, interpreter); + } + @Override public String getName() { return tag.getName(); @@ -166,6 +176,7 @@ public String renderChildren(TagNode tagNode, JinjavaInterpreter interpreter) { * @return The image of the token which has been evaluated as much as possible. */ public final String getEagerImage(Token token, JinjavaInterpreter interpreter) { + interpreter.setLineNumber(token.getLineNumber()); String eagerImage; if (token instanceof TagToken) { eagerImage = getEagerTagImage((TagToken) token, interpreter); diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 6778e9b4f..5b018e49b 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -9,7 +9,6 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.util.WhitespaceUtils; import java.io.IOException; import java.util.Objects; @@ -51,12 +50,7 @@ public static String getAsPyishStringOrThrow(Object val) throws JsonProcessingException { String string = PYISH_OBJECT_WRITER.writeValueAsString(val); Optional interpreterMaybe = JinjavaInterpreter.getCurrentMaybe(); - Optional maxStringLength = interpreterMaybe - .map(interpreter -> interpreter.getConfig().getMaxStringLength()) - .filter(max -> max > 0); - if (maxStringLength.map(max -> string.length() > max).orElse(false)) { - throw new OutputTooBigException(maxStringLength.get(), string.length()); - } + JinjavaInterpreter.checkStringLength(string); if ( interpreterMaybe .map( diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecoratorTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecoratorTest.java index 5e6b54543..74424a156 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecoratorTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecoratorTest.java @@ -113,9 +113,6 @@ public void itLimitsInterpretLength() { ); assertThatThrownBy(() -> eagerTagDecorator.interpret(tagNode, interpreter)) .isInstanceOf(DeferredValueException.class); - assertThat(interpreter.getErrors()).hasSize(1); - assertThat(interpreter.getErrors().get(0).getReason()) - .isEqualTo(ErrorReason.OUTPUT_TOO_BIG); } @Test From f3fd619d806d6629d07c84fc256a41568b314aa1 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 21 Nov 2022 09:01:39 -0500 Subject: [PATCH 066/637] Test turning on deferred execution mode when output is too large --- .../lib/tag/eager/EagerForTagTest.java | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java index 2609fca1b..0cc781c41 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java @@ -93,9 +93,31 @@ public void itLimitsLength() { MAX_OUTPUT_SIZE ) ); - assertThat(interpreter.getErrors()).hasSize(1); - assertThat(interpreter.getErrors().get(0).getReason()) - .isEqualTo(ErrorReason.OUTPUT_TOO_BIG); + assertThat(interpreter.getContext().getDeferredNodes()).hasSize(1); + } + + @Test + public void itUsesDeferredExecutionModeWhenChildrenAreLarge() { + assertThat( + interpreter.render( + String.format( + "{%% for item in range(%d) %%}1234567890{%% endfor %%}", + MAX_OUTPUT_SIZE / 10 + ) + ) + ) + .hasSize((int) MAX_OUTPUT_SIZE); + assertThat(interpreter.getContext().getDeferredTokens()).isEmpty(); + assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); + assertThat(interpreter.getErrors()).isEmpty(); + String tooBigInput = String.format( + "{%% for item in range(%d) %%}1234567890{%% endfor %%}", + MAX_OUTPUT_SIZE / 10 + 1 + ); + assertThat(interpreter.render(tooBigInput)).isEqualTo(tooBigInput); + assertThat(interpreter.getContext().getDeferredTokens()).hasSize(1); + assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); + assertThat(interpreter.getErrors()).isEmpty(); } @Test From 77c669d5e543eecf30e3207abccb077999e40fcb Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 21 Nov 2022 09:17:43 -0500 Subject: [PATCH 067/637] Check versus max output size because max string length should just be used for truncating --- .../com/hubspot/jinjava/interpret/JinjavaInterpreter.java | 4 ++-- .../hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java | 2 +- .../jinjava/objects/serialization/PyishObjectMapper.java | 2 +- .../com/hubspot/jinjava/util/EagerExpressionResolver.java | 3 ++- .../hubspot/jinjava/lib/tag/eager/EagerForTagTest.java | 8 ++++---- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index fffc9a072..b65980556 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -126,9 +126,9 @@ public JinjavaInterpreter(JinjavaInterpreter orig) { scopeDepth = orig.getScopeDepth() + 1; } - public static void checkStringLength(String string) { + public static void checkOutputSize(String string) { Optional maxStringLength = getCurrentMaybe() - .map(interpreter -> interpreter.getConfig().getMaxStringLength()) + .map(interpreter -> interpreter.getConfig().getMaxOutputSize()) .filter(max -> max > 0); if (maxStringLength.map(max -> string.length() > max).orElse(false)) { throw new OutputTooBigException(maxStringLength.get(), string.length()); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java index 7c6447104..18516e527 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java @@ -36,7 +36,7 @@ public T getTag() { public final String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { try { String output = innerInterpret(tagNode, interpreter); - JinjavaInterpreter.checkStringLength(output); + JinjavaInterpreter.checkOutputSize(output); return output; } catch (DeferredValueException | TemplateSyntaxException | OutputTooBigException e) { try { diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 5b018e49b..b543fd868 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -50,7 +50,7 @@ public static String getAsPyishStringOrThrow(Object val) throws JsonProcessingException { String string = PYISH_OBJECT_WRITER.writeValueAsString(val); Optional interpreterMaybe = JinjavaInterpreter.getCurrentMaybe(); - JinjavaInterpreter.checkStringLength(string); + JinjavaInterpreter.checkOutputSize(string); if ( interpreterMaybe .map( diff --git a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java index 19b3288ab..ff4049cd3 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java @@ -7,6 +7,7 @@ import com.hubspot.jinjava.el.ext.ExtendedParser; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.interpret.UnknownTokenException; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; @@ -107,7 +108,7 @@ public static String getValueAsJinjavaStringSafe(Object val) { return pyishString; } } - } catch (JsonProcessingException ignored) {} + } catch (JsonProcessingException | OutputTooBigException ignored) {} throw new DeferredValueException("Can not convert deferred result to string"); } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java index 0cc781c41..307340c43 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java @@ -87,13 +87,13 @@ public void itHandlesNestedDeferredForLoop() { @Test public void itLimitsLength() { - interpreter.render( + String out = interpreter.render( String.format( "{%% for item in (range(1000, %s)) + deferred %%}{%% endfor %%}", MAX_OUTPUT_SIZE ) ); - assertThat(interpreter.getContext().getDeferredNodes()).hasSize(1); + assertThat(interpreter.getContext().getDeferredTokens()).hasSize(1); } @Test @@ -102,11 +102,11 @@ public void itUsesDeferredExecutionModeWhenChildrenAreLarge() { interpreter.render( String.format( "{%% for item in range(%d) %%}1234567890{%% endfor %%}", - MAX_OUTPUT_SIZE / 10 + MAX_OUTPUT_SIZE / 10 - 1 ) ) ) - .hasSize((int) MAX_OUTPUT_SIZE); + .hasSize((int) MAX_OUTPUT_SIZE - 10); assertThat(interpreter.getContext().getDeferredTokens()).isEmpty(); assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); assertThat(interpreter.getErrors()).isEmpty(); From bedf7012ce5effacad5931bfbb1c3d381ea7ee0a Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 21 Nov 2022 16:32:14 -0500 Subject: [PATCH 068/637] Check that string is not null before calling .length() --- .../com/hubspot/jinjava/interpret/JinjavaInterpreter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index b65980556..a3a392019 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -130,7 +130,9 @@ public static void checkOutputSize(String string) { Optional maxStringLength = getCurrentMaybe() .map(interpreter -> interpreter.getConfig().getMaxOutputSize()) .filter(max -> max > 0); - if (maxStringLength.map(max -> string.length() > max).orElse(false)) { + if ( + maxStringLength.map(max -> string != null && string.length() > max).orElse(false) + ) { throw new OutputTooBigException(maxStringLength.get(), string.length()); } } From 7a7be47fc4cc125e6046f6aa96e92e7430fadbc9 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 22 Nov 2022 12:05:41 -0500 Subject: [PATCH 069/637] Test that words inside nested expressions get deferred --- .../jinjava/util/EagerExpressionResolverTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java index 6cfe1771a..d274b724c 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java @@ -60,6 +60,7 @@ private JinjavaInterpreter getInterpreter(boolean evaluateMapKeys) throws Except .withLegacyOverrides( LegacyOverrides.newBuilder().withEvaluateMapKeys(evaluateMapKeys).build() ) + .withNestedInterpretationEnabled(true) .build() ); jinjava @@ -627,6 +628,19 @@ public void itHandlesDeferredChoice() { .isEqualTo("deferred"); } + @Test + public void itGetsDeferredWordsFromNestedExpression() { + context.put("foo", 4); + EagerExpressionResult eagerExpressionResult = eagerResolveExpression( + "deferred.append('{{ foo }}')" + ); + interpreter.getContext().setThrowInterpreterErrors(true); + String partiallyResolved = eagerExpressionResult.toString(); + assertThat(partiallyResolved).isEqualTo("deferred.append('{{ foo }}')"); + assertThat(eagerExpressionResult.getDeferredWords()) + .containsExactlyInAnyOrder("deferred.append", "foo"); + } + @Test public void itHandlesDeferredNamedParameter() { context.put("foo", "foo"); From fdbf02b7e5ec80318a9be8e8238b9ee2c8bd62a3 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 22 Nov 2022 12:07:37 -0500 Subject: [PATCH 070/637] Handle logic for retrieving deferred words from nested expressions --- .../jinjava/util/EagerExpressionResolver.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java index 19b3288ab..5b392d395 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java @@ -9,8 +9,13 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.interpret.UnknownTokenException; +import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import com.hubspot.jinjava.tree.ExpressionNode; +import com.hubspot.jinjava.tree.Node; +import com.hubspot.jinjava.tree.parse.ExpressionToken; +import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult.ResolutionState; import java.util.Arrays; import java.util.Collection; @@ -23,6 +28,7 @@ import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.el.ELException; import org.apache.commons.lang3.StringUtils; @@ -116,6 +122,10 @@ private static Set findDeferredWords( String partiallyResolved, JinjavaInterpreter interpreter ) { + TokenScannerSymbols scannerSymbols = interpreter.getConfig().getTokenScannerSymbols(); + boolean nestedInterpretationEnabled = interpreter + .getConfig() + .isNestedInterpretationEnabled(); boolean throwInterpreterErrorsStart = interpreter .getContext() .getThrowInterpreterErrors(); @@ -133,6 +143,15 @@ private static Set findDeferredWords( c = value[curPos]; if (inQuote) { if (c == quoteChar && prevChar != '\\') { + String quoted = partiallyResolved.substring(prevQuotePos, curPos); + if (nestedInterpretationEnabled) { + getDeferredWordsInsideNestedExpression( + interpreter, + scannerSymbols, + words, + quoted + ); + } inQuote = false; prevQuotePos = curPos; } @@ -160,6 +179,55 @@ private static Set findDeferredWords( } } + private static void getDeferredWordsInsideNestedExpression( + JinjavaInterpreter interpreter, + TokenScannerSymbols scannerSymbols, + Set words, + String quoted + ) { + if ( + quoted.contains(scannerSymbols.getExpressionStartWithTag()) && + quoted.contains(scannerSymbols.getExpressionEndWithTag()) + ) { + throw new DeferredValueException( + "Cannot get words inside nested interpretation tags" + ); + } + + if ( + quoted.contains(scannerSymbols.getExpressionStart()) && + quoted.contains(scannerSymbols.getExpressionEnd()) + ) { + List expressionNodes = getExpressionNodes(quoted, interpreter); + words.addAll( + expressionNodes + .stream() + .map(expressionNode -> ((ExpressionToken) expressionNode.getMaster()).getExpr()) + .map(expr -> findDeferredWords(expr, interpreter)) + .flatMap(Set::stream) + .collect(Collectors.toSet()) + ); + } + } + + private static List getExpressionNodes( + String input, + JinjavaInterpreter interpreter + ) { + Node root = interpreter.parse(input); + return getExpressionNodes(root).collect(Collectors.toList()); + } + + private static Stream getExpressionNodes(Node parent) { + if (parent instanceof ExpressionNode) { + return Stream.of((ExpressionNode) parent); + } + return parent + .getChildren() + .stream() + .flatMap(EagerExpressionResolver::getExpressionNodes); + } + // Knowing that there are no quotes between start and end, // split up the words in `partiallyResolved` and return whichever ones can't be resolved. private static Set findDeferredWordsInSubstring( From f56f94e0caa9a17f060166bf6350a787ea7f1435 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 22 Nov 2022 12:19:42 -0500 Subject: [PATCH 071/637] Add tests for multiple nested expressions and fix escaping --- .../jinjava/util/EagerExpressionResolver.java | 20 +++++++++----- .../util/EagerExpressionResolverTest.java | 26 ++++++++++++++++++- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java index 5b392d395..70015289a 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java @@ -133,7 +133,7 @@ private static Set findDeferredWords( interpreter.getContext().setThrowInterpreterErrors(true); Set words = new HashSet<>(); char[] value = partiallyResolved.toCharArray(); - int prevQuotePos = 0; + int prevQuotePos = -1; int curPos = 0; char c; char prevChar = 0; @@ -143,13 +143,12 @@ private static Set findDeferredWords( c = value[curPos]; if (inQuote) { if (c == quoteChar && prevChar != '\\') { - String quoted = partiallyResolved.substring(prevQuotePos, curPos); if (nestedInterpretationEnabled) { getDeferredWordsInsideNestedExpression( interpreter, scannerSymbols, words, - quoted + partiallyResolved.substring(prevQuotePos, curPos + 1) ); } inQuote = false; @@ -161,17 +160,23 @@ private static Set findDeferredWords( words.addAll( findDeferredWordsInSubstring( partiallyResolved, - prevQuotePos, + prevQuotePos + 1, curPos, interpreter ) ); + prevQuotePos = curPos; } prevChar = c; curPos++; } words.addAll( - findDeferredWordsInSubstring(partiallyResolved, prevQuotePos, curPos, interpreter) + findDeferredWordsInSubstring( + partiallyResolved, + prevQuotePos + 1, + curPos, + interpreter + ) ); return words; } finally { @@ -198,7 +203,10 @@ private static void getDeferredWordsInsideNestedExpression( quoted.contains(scannerSymbols.getExpressionStart()) && quoted.contains(scannerSymbols.getExpressionEnd()) ) { - List expressionNodes = getExpressionNodes(quoted, interpreter); + List expressionNodes = getExpressionNodes( + WhitespaceUtils.unquoteAndUnescape(quoted), + interpreter + ); words.addAll( expressionNodes .stream() diff --git a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java index d274b724c..6ae3dd932 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java @@ -630,7 +630,6 @@ public void itHandlesDeferredChoice() { @Test public void itGetsDeferredWordsFromNestedExpression() { - context.put("foo", 4); EagerExpressionResult eagerExpressionResult = eagerResolveExpression( "deferred.append('{{ foo }}')" ); @@ -641,6 +640,31 @@ public void itGetsDeferredWordsFromNestedExpression() { .containsExactlyInAnyOrder("deferred.append", "foo"); } + @Test + public void itGetsDeferredWordsFromAdjacentNestedExpression() { + EagerExpressionResult eagerExpressionResult = eagerResolveExpression( + "deferred.append('{{ foo }} and {{ bar }}')" + ); + interpreter.getContext().setThrowInterpreterErrors(true); + String partiallyResolved = eagerExpressionResult.toString(); + assertThat(partiallyResolved).isEqualTo("deferred.append('{{ foo }} and {{ bar }}')"); + assertThat(eagerExpressionResult.getDeferredWords()) + .containsExactlyInAnyOrder("deferred.append", "foo", "bar"); + } + + @Test + public void itGetsDeferredWordsFromMultipleNestedExpression() { + EagerExpressionResult eagerExpressionResult = eagerResolveExpression( + "deferred.append('{{ foo ~ \\'and {{ bar }}\\' }}')" + ); + interpreter.getContext().setThrowInterpreterErrors(true); + String partiallyResolved = eagerExpressionResult.toString(); + assertThat(partiallyResolved) + .isEqualTo("deferred.append('{{ foo ~ \\'and {{ bar }}\\' }}')"); + assertThat(eagerExpressionResult.getDeferredWords()) + .containsExactlyInAnyOrder("deferred.append", "foo", "bar"); + } + @Test public void itHandlesDeferredNamedParameter() { context.put("foo", "foo"); From f27cff1bef95179f9bc1141cd16eebf9a86f5118 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 22 Nov 2022 12:21:30 -0500 Subject: [PATCH 072/637] Test multiple deferred words getting added from nested expression --- .../jinjava/util/EagerExpressionResolverTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java index 6ae3dd932..a4fcfd11d 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java @@ -665,6 +665,18 @@ public void itGetsDeferredWordsFromMultipleNestedExpression() { .containsExactlyInAnyOrder("deferred.append", "foo", "bar"); } + @Test + public void itGetsMultipleDeferredWordsNestedExpression() { + EagerExpressionResult eagerExpressionResult = eagerResolveExpression( + "deferred.append('{{ foo.append(bar) }}')" + ); + interpreter.getContext().setThrowInterpreterErrors(true); + String partiallyResolved = eagerExpressionResult.toString(); + assertThat(partiallyResolved).isEqualTo("deferred.append('{{ foo.append(bar) }}')"); + assertThat(eagerExpressionResult.getDeferredWords()) + .containsExactlyInAnyOrder("deferred.append", "foo.append", "bar"); + } + @Test public void itHandlesDeferredNamedParameter() { context.put("foo", "foo"); From a6b7305b748b7ed4007694431fe21f058deeb3cf Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 22 Nov 2022 12:24:32 -0500 Subject: [PATCH 073/637] Test that nested tags are not supported --- .../com/hubspot/jinjava/util/EagerExpressionResolver.java | 1 - .../hubspot/jinjava/util/EagerExpressionResolverTest.java | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java index 70015289a..ab9e82834 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java @@ -9,7 +9,6 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.interpret.UnknownTokenException; -import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.objects.serialization.PyishSerializable; import com.hubspot.jinjava.tree.ExpressionNode; diff --git a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java index a4fcfd11d..be2824921 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java @@ -677,6 +677,11 @@ public void itGetsMultipleDeferredWordsNestedExpression() { .containsExactlyInAnyOrder("deferred.append", "foo.append", "bar"); } + @Test(expected = DeferredValueException.class) + public void itFailsWhenThereIsANestedTag() { + eagerResolveExpression("deferred.append('{% do foo.append(bar) %}')"); + } + @Test public void itHandlesDeferredNamedParameter() { context.put("foo", "foo"); From 33e6278056aa298fbe90e858cabf4b2e4b9ce471 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 22 Nov 2022 13:10:37 -0500 Subject: [PATCH 074/637] Add test demonstrating need for deferring words inside deferred nested expression --- src/test/java/com/hubspot/jinjava/EagerTest.java | 7 +++++++ ...cts-words-from-inside-nested-expressions.expected.jinja | 2 ++ ...reconstructs-words-from-inside-nested-expressions.jinja | 4 ++++ 3 files changed, 13 insertions(+) create mode 100644 src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.expected.jinja create mode 100644 src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.jinja diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 6dc0e4e31..c6c7d3fe2 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1194,4 +1194,11 @@ public void itUsesUniqueMacroNamesSecondPass() { interpreter.getContext().put("deferred", "resolved"); expectedTemplateInterpreter.assertExpectedOutput("uses-unique-macro-names.expected"); } + + @Test + public void itReconstructsWordsFromInsideNestedExpressions() { + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-words-from-inside-nested-expressions" + ); + } } diff --git a/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.expected.jinja b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.expected.jinja new file mode 100644 index 000000000..d8f51399d --- /dev/null +++ b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.expected.jinja @@ -0,0 +1,2 @@ +{% set foo = 'a' %}{% do deferred.append('{{ foo }}') %} +{{ deferred[0] }} diff --git a/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.jinja b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.jinja new file mode 100644 index 000000000..87d3e664d --- /dev/null +++ b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.jinja @@ -0,0 +1,4 @@ +{% set foo = 'a' %} +{% set bar = 'b' %} +{% do deferred.append('{{ foo }}') %} +{{ deferred[0] }} From 9ace34df48ded1252ec17cdd241ad0d055e9ff95 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 22 Nov 2022 16:11:07 -0500 Subject: [PATCH 075/637] Add to test to demonstrate why {{ foo }} should not be resolved early --- src/test/java/com/hubspot/jinjava/EagerTest.java | 8 ++++++++ ...from-inside-nested-expressions.expected.expected.jinja | 2 ++ ...ts-words-from-inside-nested-expressions.expected.jinja | 1 + ...econstructs-words-from-inside-nested-expressions.jinja | 1 + 4 files changed, 12 insertions(+) create mode 100644 src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.expected.expected.jinja diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index c6c7d3fe2..d03c03525 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1201,4 +1201,12 @@ public void itReconstructsWordsFromInsideNestedExpressions() { "reconstructs-words-from-inside-nested-expressions" ); } + + @Test + public void itReconstructsWordsFromInsideNestedExpressionsSecondPass() { + interpreter.getContext().put("deferred", new PyList(new ArrayList<>())); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "reconstructs-words-from-inside-nested-expressions.expected" + ); + } } diff --git a/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.expected.expected.jinja b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.expected.expected.jinja new file mode 100644 index 000000000..ac18b1763 --- /dev/null +++ b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.expected.expected.jinja @@ -0,0 +1,2 @@ +a +{{ foo }} diff --git a/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.expected.jinja b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.expected.jinja index d8f51399d..b25302fc8 100644 --- a/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.expected.jinja +++ b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.expected.jinja @@ -1,2 +1,3 @@ {% set foo = 'a' %}{% do deferred.append('{{ foo }}') %} {{ deferred[0] }} +{% print deferred[0] %} diff --git a/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.jinja b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.jinja index 87d3e664d..49db98681 100644 --- a/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.jinja +++ b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions.jinja @@ -2,3 +2,4 @@ {% set bar = 'b' %} {% do deferred.append('{{ foo }}') %} {{ deferred[0] }} +{% print deferred[0] %} From ad26e1e694cebe992e7ee5b3f2bfe56319494abe Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 23 Nov 2022 12:39:49 -0500 Subject: [PATCH 076/637] Add `current_path` to meta context variables --- .../jinjava/mode/EagerExecutionMode.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/mode/EagerExecutionMode.java b/src/main/java/com/hubspot/jinjava/mode/EagerExecutionMode.java index ab9a7f966..3d8055c0b 100644 --- a/src/main/java/com/hubspot/jinjava/mode/EagerExecutionMode.java +++ b/src/main/java/com/hubspot/jinjava/mode/EagerExecutionMode.java @@ -5,11 +5,21 @@ import com.hubspot.jinjava.lib.expression.EagerExpressionStrategy; import com.hubspot.jinjava.lib.tag.eager.EagerTagDecorator; import com.hubspot.jinjava.lib.tag.eager.EagerTagFactory; +import com.hubspot.jinjava.loader.RelativePathResolver; import java.util.Optional; public class EagerExecutionMode implements ExecutionMode { private static final ExecutionMode INSTANCE = new EagerExecutionMode(); + // These meta context variables should never be removed from the set of meta context variables + public static final ImmutableSet STATIC_META_CONTEXT_VARIABLES = ImmutableSet.of( + Context.GLOBAL_MACROS_SCOPE_KEY, + Context.IMPORT_RESOURCE_PATH_KEY, + Context.DEFERRED_IMPORT_RESOURCE_PATH_KEY, + Context.IMPORT_RESOURCE_ALIAS_KEY, + RelativePathResolver.CURRENT_PATH_CONTEXT_KEY + ); + protected EagerExecutionMode() {} public static ExecutionMode instance() { @@ -41,15 +51,6 @@ public void prepareContext(Context context) { .filter(Optional::isPresent) .forEach(maybeEagerTag -> context.registerTag(maybeEagerTag.get())); context.setExpressionStrategy(new EagerExpressionStrategy()); - context - .getMetaContextVariables() - .addAll( - ImmutableSet.of( - Context.GLOBAL_MACROS_SCOPE_KEY, - Context.IMPORT_RESOURCE_PATH_KEY, - Context.DEFERRED_IMPORT_RESOURCE_PATH_KEY, - Context.IMPORT_RESOURCE_ALIAS_KEY - ) - ); + context.getMetaContextVariables().addAll(STATIC_META_CONTEXT_VARIABLES); } } From 7c2bed71c4f92417cb7afb20683767318c08034e Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 23 Nov 2022 12:40:12 -0500 Subject: [PATCH 077/637] Don't allow static meta context variables to be removed --- .../jinjava/lib/tag/eager/EagerForTag.java | 13 +++++-------- .../lib/tag/eager/EagerSetTagStrategy.java | 13 ++++--------- .../jinjava/util/EagerReconstructionUtils.java | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 019e8e673..d3a3fa2c6 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -1,6 +1,5 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.google.common.collect.Sets; import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; import com.hubspot.jinjava.interpret.DeferredValue; @@ -185,13 +184,11 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter eagerExpressionResult.getDeferredWords(), interpreter ); - Set metaLoopVars = Sets - .intersection( - interpreter.getContext().getMetaContextVariables(), - Sets.newHashSet(loopVars) - ) - .immutableCopy(); - interpreter.getContext().getMetaContextVariables().removeAll(metaLoopVars); + EagerReconstructionUtils.removeMetaContextVariables( + loopVars.stream(), + interpreter.getContext() + ); + interpreter .getContext() .handleDeferredToken( diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java index eec9d7886..8ada05a84 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java @@ -1,6 +1,5 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.google.common.collect.Sets; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.SetTag; import com.hubspot.jinjava.tree.TagNode; @@ -8,7 +7,6 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.StringJoiner; import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.Triple; @@ -37,13 +35,10 @@ public String run(TagNode tagNode, JinjavaInterpreter interpreter) { expression = tagNode.getHelpers(); } - Set metaLoopVars = Sets - .intersection( - interpreter.getContext().getMetaContextVariables(), - Arrays.stream(variables).map(String::trim).collect(Collectors.toSet()) - ) - .immutableCopy(); - interpreter.getContext().getMetaContextVariables().removeAll(metaLoopVars); + EagerReconstructionUtils.removeMetaContextVariables( + Arrays.stream(variables).map(String::trim), + interpreter.getContext() + ); EagerExecutionResult eagerExecutionResult = getEagerExecutionResult( tagNode, diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index a0db0f891..44ca3f7e6 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.util; +import com.google.common.collect.Sets; import com.hubspot.jinjava.el.ext.AbstractCallableMethod; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.Context.Library; @@ -20,6 +21,7 @@ import com.hubspot.jinjava.lib.tag.SetTag; import com.hubspot.jinjava.lib.tag.eager.DeferredToken; import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; +import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.objects.collections.PyList; import com.hubspot.jinjava.objects.collections.PyMap; import com.hubspot.jinjava.objects.serialization.PyishBlockSetSerializable; @@ -725,6 +727,21 @@ public static String wrapInChildScope(String toWrap, JinjavaInterpreter interpre ); } + public static void removeMetaContextVariables( + Stream varStream, + Context context + ) { + Set metaSetVars = Sets + .intersection( + context.getMetaContextVariables(), + varStream + .filter(var -> !EagerExecutionMode.STATIC_META_CONTEXT_VARIABLES.contains(var)) + .collect(Collectors.toSet()) + ) + .immutableCopy(); + context.getMetaContextVariables().removeAll(metaSetVars); + } + public static class EagerChildContextConfig { private final boolean takeNewValue; private final boolean partialMacroEvaluation; From 2a35934388f1bae8cbf0be6f656ed9bdf79ba8f6 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 23 Nov 2022 12:40:40 -0500 Subject: [PATCH 078/637] Update teset to use other variable name than current_path --- .../eager/handles-same-name-import-var.expected.jinja | 8 ++++---- src/test/resources/tags/settag/set-var-and-deferred.jinja | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/resources/eager/handles-same-name-import-var.expected.jinja b/src/test/resources/eager/handles-same-name-import-var.expected.jinja index 34d8af234..7f63458f6 100644 --- a/src/test/resources/eager/handles-same-name-import-var.expected.jinja +++ b/src/test/resources/eager/handles-same-name-import-var.expected.jinja @@ -1,10 +1,10 @@ {% if deferred %} -{% set __ignored__ %}{% set current_path = '../settag/set-var-and-deferred.jinja' %}{% set current_path,value = null,null %}{% set my_var = {} %}{% set my_var = {} %}{% if deferred %} -{% set __ignored__ %}{% set current_path = '../settag/set-var-and-deferred.jinja' %}{% do my_var.update({'current_path': current_path}) %}{% set value = null %}{% do my_var.update({'value': value}) %}{% set my_var = {} %}{% set my_var = {'foo': 'bar'} %}{% set my_var = {'my_var': {'foo': 'bar'} } %} +{% set __ignored__ %}{% set current_path = '../settag/set-var-and-deferred.jinja' %}{% set path,value = null,null %}{% set my_var = {} %}{% set my_var = {} %}{% if deferred %} +{% set __ignored__ %}{% set path = '../settag/set-var-and-deferred.jinja' %}{% do my_var.update({'path': path}) %}{% set value = null %}{% do my_var.update({'value': value}) %}{% set my_var = {} %}{% set my_var = {'foo': 'bar'} %}{% set my_var = {'my_var': {'foo': 'bar'} } %} {% set value = deferred %}{% do my_var.update({'value': value}) %}{% do my_var.update({'value': value}) %} -{% do my_var.update({'import_resource_path': '../settag/set-var-and-deferred.jinja', 'value': value}) %}{% set current_path = '' %}{% do my_var.update({'current_path': current_path}) %}{% endset %}{% do my_var.update({'__ignored__': __ignored__}) %} +{% do my_var.update({'import_resource_path': '../settag/set-var-and-deferred.jinja', 'value': value}) %}{% set path = '' %}{% do my_var.update({'path': path}) %}{% endset %}{% do my_var.update({'__ignored__': __ignored__}) %} {{ my_var }} {% endif %} -{% do my_var.update({'current_path': current_path,'import_resource_path': '../settag/set-var-and-deferred.jinja','value': value}) %}{% set current_path = '' %}{% endset %} +{% do my_var.update({'path': path,'import_resource_path': '../settag/set-var-and-deferred.jinja','value': value}) %}{% set current_path = '' %}{% endset %} {{ my_var }} {% endif %} diff --git a/src/test/resources/tags/settag/set-var-and-deferred.jinja b/src/test/resources/tags/settag/set-var-and-deferred.jinja index 54da34656..129a80d32 100644 --- a/src/test/resources/tags/settag/set-var-and-deferred.jinja +++ b/src/test/resources/tags/settag/set-var-and-deferred.jinja @@ -1,6 +1,6 @@ {% if deferred %} -{% set __ignored__ %}{% set current_path = '../settag/set-var-and-deferred.jinja' %}{% set value = null %}{% set my_var = {} %}{% set my_var = {'foo': 'bar'} %}{% set my_var = {'my_var': my_var} %} +{% set __ignored__ %}{% set path = '../settag/set-var-and-deferred.jinja' %}{% set value = null %}{% set my_var = {} %}{% set my_var = {'foo': 'bar'} %}{% set my_var = {'my_var': my_var} %} {% set value = deferred %}{% do my_var.update({"value": value}) %} -{% do my_var.update({'import_resource_path': '../settag/set-var-and-deferred.jinja','value': value}) %}{% set current_path = '' %}{% endset %} +{% do my_var.update({'import_resource_path': '../settag/set-var-and-deferred.jinja','value': value}) %}{% set path = '' %}{% endset %} {{ my_var }} {% endif %} From 1e43c50b5977e6481b4568663614175547f4cb41 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 23 Nov 2022 12:57:15 -0500 Subject: [PATCH 079/637] Test not removing static meta context variables, but removing other ones --- .../util/EagerReconstructionUtilsTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java index 7df72f1ca..16000e85c 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java @@ -15,6 +15,7 @@ import com.hubspot.jinjava.lib.fn.MacroFunction; import com.hubspot.jinjava.lib.tag.eager.DeferredToken; import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; +import com.hubspot.jinjava.loader.RelativePathResolver; import com.hubspot.jinjava.mode.DefaultExecutionMode; import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.mode.PreserveRawExecutionMode; @@ -30,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -337,6 +339,31 @@ public void itIgnoresMetaContextVariables() { .isEmpty(); } + @Test + public void itDoesNotRemoveStaticMetaContextVariables() { + String variableName = "foo"; + interpreter.getContext().getMetaContextVariables().add(variableName); + assertThat(interpreter.getContext().getMetaContextVariables()).contains(variableName); + EagerReconstructionUtils.removeMetaContextVariables( + Stream.of(variableName), + interpreter.getContext() + ); + assertThat(interpreter.getContext().getMetaContextVariables()) + .doesNotContain(variableName); + } + + @Test + public void itRemovesOtherMetaContextVariables() { + assertThat(interpreter.getContext().getMetaContextVariables()) + .contains(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY); + EagerReconstructionUtils.removeMetaContextVariables( + Stream.of(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY), + interpreter.getContext() + ); + assertThat(interpreter.getContext().getMetaContextVariables()) + .contains(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY); + } + private static MacroFunction getMockMacroFunction(String image) { MacroFunction mockMacroFunction = mock(MacroFunction.class); when(mockMacroFunction.getName()).thenReturn("foo"); From 74cd08894454ef1404a0357f59881ab45fff422b Mon Sep 17 00:00:00 2001 From: Nathaniel Pautzke Date: Wed, 23 Nov 2022 14:21:11 -0600 Subject: [PATCH 080/637] update static assert import --- .../com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java index b18b0149b..89893aa3f 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java @@ -1,8 +1,7 @@ package com.hubspot.jinjava.lib.tag.eager; -import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; -import com.hubspot.jinjava.ExpectedNodeInterpreter; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.DeferredValue; @@ -11,8 +10,6 @@ import com.hubspot.jinjava.lib.tag.Tag; import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.tree.parse.TagToken; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; import org.junit.After; import org.junit.Before; From 8a929af2e349451cee231940c2cf71aca272d684 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 23 Nov 2022 17:16:28 -0500 Subject: [PATCH 081/637] Add option to discard session bindings when executing in child context --- .../util/EagerReconstructionUtils.java | 31 ++++++++++++++----- .../util/EagerReconstructionUtilsTest.java | 24 ++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 44ca3f7e6..05bb58833 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -175,7 +175,7 @@ public static EagerExecutionResult executeInChildContext( } // Don't create new call stacks to prevent hitting max recursion with this silent new scope - Map sessionBindings; + Map speculativeBindings; try (InterpreterScopeClosable c = interpreter.enterNonStackingScope()) { if (eagerChildContextConfig.forceDeferredExecutionMode) { interpreter.getContext().setDeferredExecutionMode(true); @@ -184,10 +184,13 @@ public static EagerExecutionResult executeInChildContext( .getContext() .setPartialMacroEvaluation(eagerChildContextConfig.partialMacroEvaluation); result = function.apply(interpreter); - sessionBindings = interpreter.getContext().getSessionBindings(); + speculativeBindings = + eagerChildContextConfig.discardSessionBindings + ? Collections.emptyMap() + : interpreter.getContext().getSessionBindings(); } - sessionBindings = - sessionBindings + speculativeBindings = + speculativeBindings .entrySet() .stream() .filter( @@ -201,7 +204,7 @@ public static EagerExecutionResult executeInChildContext( ) .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); if (eagerChildContextConfig.checkForContextChanges) { - sessionBindings.putAll( + speculativeBindings.putAll( interpreter .getContext() .entrySet() @@ -257,8 +260,8 @@ public static EagerExecutionResult executeInChildContext( ) ); } - sessionBindings = - sessionBindings + speculativeBindings = + speculativeBindings .entrySet() .stream() .filter(entry -> !metaContextVariables.contains(entry.getKey())) @@ -268,7 +271,7 @@ public static EagerExecutionResult executeInChildContext( ) // these are already set recursively .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); - return new EagerExecutionResult(result, sessionBindings); + return new EagerExecutionResult(result, speculativeBindings); } private static Object getObjectOrHashCode(Object o) { @@ -744,17 +747,21 @@ public static void removeMetaContextVariables( public static class EagerChildContextConfig { private final boolean takeNewValue; + + private final boolean discardSessionBindings; private final boolean partialMacroEvaluation; private final boolean checkForContextChanges; private final boolean forceDeferredExecutionMode; private EagerChildContextConfig( boolean takeNewValue, + boolean discardSessionBindings, boolean partialMacroEvaluation, boolean checkForContextChanges, boolean forceDeferredExecutionMode ) { this.takeNewValue = takeNewValue; + this.discardSessionBindings = discardSessionBindings; this.partialMacroEvaluation = partialMacroEvaluation; this.checkForContextChanges = checkForContextChanges; this.forceDeferredExecutionMode = forceDeferredExecutionMode; @@ -766,6 +773,8 @@ public static Builder newBuilder() { public static class Builder { private boolean takeNewValue; + + private boolean discardSessionBindings; private boolean partialMacroEvaluation; private boolean checkForContextChanges; private boolean forceDeferredExecutionMode; @@ -777,6 +786,11 @@ public Builder withTakeNewValue(boolean takeNewValue) { return this; } + public Builder withDiscardSessionBindings(boolean discardSessionBindings) { + this.discardSessionBindings = discardSessionBindings; + return this; + } + public Builder withPartialMacroEvaluation(boolean partialMacroEvaluation) { this.partialMacroEvaluation = partialMacroEvaluation; return this; @@ -795,6 +809,7 @@ public Builder withForceDeferredExecutionMode(boolean forceDeferredExecutionMode public EagerChildContextConfig build() { return new EagerChildContextConfig( takeNewValue, + discardSessionBindings, partialMacroEvaluation, checkForContextChanges, forceDeferredExecutionMode diff --git a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java index 16000e85c..e79e3b564 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java @@ -364,6 +364,30 @@ public void itRemovesOtherMetaContextVariables() { .contains(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY); } + @Test + public void itDiscardsSessionBindings() { + interpreter.getContext().put("foo", "bar"); + EagerExecutionResult withSessionBindings = EagerReconstructionUtils.executeInChildContext( + eagerInterpreter -> { + interpreter.getContext().put("foo", "foobar"); + return EagerExpressionResult.fromString(""); + }, + interpreter, + EagerChildContextConfig.newBuilder().withDiscardSessionBindings(false).build() + ); + EagerExecutionResult withoutSessionBindings = EagerReconstructionUtils.executeInChildContext( + eagerInterpreter -> { + interpreter.getContext().put("foo", "foobar"); + return EagerExpressionResult.fromString(""); + }, + interpreter, + EagerChildContextConfig.newBuilder().withDiscardSessionBindings(true).build() + ); + assertThat(withSessionBindings.getSpeculativeBindings()) + .containsEntry("foo", "foobar"); + assertThat(withoutSessionBindings.getSpeculativeBindings()).doesNotContainKey("foo"); + } + private static MacroFunction getMockMacroFunction(String image) { MacroFunction mockMacroFunction = mock(MacroFunction.class); when(mockMacroFunction.getName()).thenReturn("foo"); From abfe8c5f48868419e65c0fab5af0d162249a4a54 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 28 Nov 2022 11:52:51 -0500 Subject: [PATCH 082/637] Fix spacing in tests from hidden merge conflict --- ...amespace-for-set-tags-using-period.expected.expected.jinja | 4 ++-- ...structs-namespace-for-set-tags-using-period.expected.jinja | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.expected.jinja b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.expected.jinja index bc1e2a177..337506cd0 100644 --- a/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.expected.jinja +++ b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.expected.jinja @@ -1,2 +1,2 @@ -namespace({'a': 'aa', 'b': 'b resolved'}) -namespace({'c': 'cc', 'd': 'd resolved'}) +namespace({'a': 'aa', 'b': 'b resolved'} ) +namespace({'c': 'cc', 'd': 'd resolved'} ) diff --git a/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.jinja b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.jinja index 1894f0699..656c19e5b 100644 --- a/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.jinja +++ b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period.expected.jinja @@ -1,7 +1,7 @@ -{% set ns1 = namespace({'a': 'aa'}) %}{% set ns1.b = 'b ' ~ deferred %} +{% set ns1 = namespace({'a': 'aa'} ) %}{% set ns1.b = 'b ' ~ deferred %} -{% set ns2 = namespace({'c': 'cc'}) %}{% set ns2.d %}d {{ deferred }}{% endset %} +{% set ns2 = namespace({'c': 'cc'} ) %}{% set ns2.d %}d {{ deferred }}{% endset %} {{ ns1 }} {{ ns2 }} From 8e258bdcd3457d61a27d077cffc3c56c6690bc7d Mon Sep 17 00:00:00 2001 From: Tyler Kindy Date: Mon, 28 Nov 2022 13:06:23 -0500 Subject: [PATCH 083/637] Bind new date formatting filters as functions (#965) This PR binds the new date formatting filters added in #953 as functions. This allows them to be called with both filter and function syntax just like `datetimeformat` today. --- .../lib/filter/time/DateTimeFormatHelper.java | 12 +++- .../lib/filter/time/FormatDateFilter.java | 6 +- .../lib/filter/time/FormatDatetimeFilter.java | 6 +- .../lib/filter/time/FormatTimeFilter.java | 6 +- .../jinjava/lib/fn/FunctionLibrary.java | 33 ++++++++++ .../lib/fn/DateFormatFunctionsTest.java | 61 +++++++++++++++++++ 6 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/hubspot/jinjava/lib/fn/DateFormatFunctionsTest.java diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/time/DateTimeFormatHelper.java b/src/main/java/com/hubspot/jinjava/lib/filter/time/DateTimeFormatHelper.java index 0f4026633..dee655a96 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/time/DateTimeFormatHelper.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/time/DateTimeFormatHelper.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.lib.filter.time; +import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.InvalidArgumentException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.fn.Functions; @@ -26,12 +27,19 @@ final class DateTimeFormatHelper { this.cannedFormatterFunction = cannedFormatterFunction; } - String format(Object var, JinjavaInterpreter interpreter, String... args) { + String format(Object var, String... args) { String format = arg(args, 0).orElse("medium"); ZoneId zoneId = arg(args, 1).map(this::parseZone).orElse(ZoneOffset.UTC); Locale locale = arg(args, 2) .map(this::parseLocale) - .orElseGet(() -> interpreter.getConfig().getLocale()); + .orElseGet( + () -> + JinjavaInterpreter + .getCurrentMaybe() + .map(JinjavaInterpreter::getConfig) + .map(JinjavaConfig::getLocale) + .orElse(Locale.ENGLISH) + ); return buildFormatter(format) .withLocale(locale) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilter.java index 3dd2fc1d0..267daec0a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilter.java @@ -48,7 +48,11 @@ public class FormatDateFilter implements Filter { @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - return HELPER.format(var, interpreter, args); + return format(var, args); + } + + public static Object format(Object var, String... args) { + return HELPER.format(var, args); } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilter.java index 3fa8f743a..f5202eb55 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilter.java @@ -50,7 +50,11 @@ public class FormatDatetimeFilter implements Filter { @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - return HELPER.format(var, interpreter, args); + return format(var, args); + } + + public static Object format(Object var, String... args) { + return HELPER.format(var, args); } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilter.java index 859bab252..2b2268fe2 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilter.java @@ -48,7 +48,11 @@ public class FormatTimeFilter implements Filter { @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - return HELPER.format(var, interpreter, args); + return format(var, args); + } + + public static Object format(Object var, String... args) { + return HELPER.format(var, args); } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/FunctionLibrary.java b/src/main/java/com/hubspot/jinjava/lib/fn/FunctionLibrary.java index 63842ee29..f5990c4da 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/FunctionLibrary.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/FunctionLibrary.java @@ -2,6 +2,9 @@ import com.google.common.collect.Lists; import com.hubspot.jinjava.lib.SimpleLibrary; +import com.hubspot.jinjava.lib.filter.time.FormatDateFilter; +import com.hubspot.jinjava.lib.filter.time.FormatDatetimeFilter; +import com.hubspot.jinjava.lib.filter.time.FormatTimeFilter; import java.util.Set; public class FunctionLibrary extends SimpleLibrary { @@ -22,6 +25,36 @@ protected void registerDefaults() { String[].class ) ); + register( + new ELFunctionDefinition( + "", + "format_date", + FormatDateFilter.class, + "format", + Object.class, + String[].class + ) + ); + register( + new ELFunctionDefinition( + "", + "format_time", + FormatTimeFilter.class, + "format", + Object.class, + String[].class + ) + ); + register( + new ELFunctionDefinition( + "", + "format_datetime", + FormatDatetimeFilter.class, + "format", + Object.class, + String[].class + ) + ); register( new ELFunctionDefinition( "", diff --git a/src/test/java/com/hubspot/jinjava/lib/fn/DateFormatFunctionsTest.java b/src/test/java/com/hubspot/jinjava/lib/fn/DateFormatFunctionsTest.java new file mode 100644 index 000000000..c0d104f59 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/fn/DateFormatFunctionsTest.java @@ -0,0 +1,61 @@ +package com.hubspot.jinjava.lib.fn; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.Jinjava; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import org.junit.Before; +import org.junit.Test; + +public class DateFormatFunctionsTest { + Jinjava jinjava; + + @Before + public void setUp() throws Exception { + jinjava = new Jinjava(); + } + + @Test + public void itFormatsDates() { + assertThat( + jinjava.render( + "{{ format_date(d, 'medium') }}", + ImmutableMap.of( + "d", + ZonedDateTime.of(2022, 11, 28, 16, 30, 4, 0, ZoneOffset.UTC) + ) + ) + ) + .isEqualTo("Nov 28, 2022"); + } + + @Test + public void itFormatsTimes() { + assertThat( + jinjava.render( + "{{ format_time(d, 'medium') }}", + ImmutableMap.of( + "d", + ZonedDateTime.of(2022, 11, 28, 16, 30, 4, 0, ZoneOffset.UTC) + ) + ) + ) + .isEqualTo("4:30:04 PM"); + } + + @Test + public void itFormatsDateTimes() { + assertThat( + jinjava.render( + "{{ format_datetime(d, 'medium') }}", + ImmutableMap.of( + "d", + ZonedDateTime.of(2022, 11, 28, 16, 30, 4, 0, ZoneOffset.UTC) + ) + ) + ) + .isEqualTo("Nov 28, 2022, 4:30:04 PM"); + } +} From 6cfd9cca5d1aeed8ff29d1a10298b220be9d1f70 Mon Sep 17 00:00:00 2001 From: Jeff Boulter Date: Mon, 5 Dec 2022 11:53:51 -0500 Subject: [PATCH 084/637] add unescapeHtml filter --- .../lib/filter/UnescapeHtmlFilter.java | 46 +++++++++++++++++++ .../lib/filter/UnescapeHtmlFilterTest.java | 25 ++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/main/java/com/hubspot/jinjava/lib/filter/UnescapeHtmlFilter.java create mode 100644 src/test/java/com/hubspot/jinjava/lib/filter/UnescapeHtmlFilterTest.java diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/UnescapeHtmlFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/UnescapeHtmlFilter.java new file mode 100644 index 000000000..2f6934ad9 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/UnescapeHtmlFilter.java @@ -0,0 +1,46 @@ +/********************************************************************** + * Copyright (c) 2022 HubSpot Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **********************************************************************/ +package com.hubspot.jinjava.lib.filter; + +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; +import com.hubspot.jinjava.doc.annotations.JinjavaParam; +import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import java.util.Objects; +import org.apache.commons.lang3.StringEscapeUtils; + +@JinjavaDoc( + value = "Converts HTML entities in string s to Unicode characters.", + input = @JinjavaParam(value = "s", desc = "String to unescape", required = true), + snippets = { + @JinjavaSnippet( + code = "{% set escaped_string = \"
This & that
\" %}\n" + + "{{ escaped_string|unescape_html }}" + ) + } +) +public class UnescapeHtmlFilter implements Filter { + + @Override + public Object filter(Object object, JinjavaInterpreter interpreter, String... arg) { + return StringEscapeUtils.unescapeHtml4(Objects.toString(object, "")); + } + + @Override + public String getName() { + return "unescape_html"; + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/UnescapeHtmlFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/UnescapeHtmlFilterTest.java new file mode 100644 index 000000000..75dbeeca2 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/UnescapeHtmlFilterTest.java @@ -0,0 +1,25 @@ +package com.hubspot.jinjava.lib.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseInterpretingTest; +import org.junit.Before; +import org.junit.Test; + +public class UnescapeHtmlFilterTest extends BaseInterpretingTest { + UnescapeHtmlFilter f; + + @Before + public void setup() { + f = new UnescapeHtmlFilter(); + } + + @Test + public void itUnescapes() { + assertThat(f.filter("", interpreter)).isEqualTo(""); + assertThat(f.filter("me & you", interpreter)).isEqualTo("me & you"); + assertThat(f.filter("jeff's & jack's bogüs journey", interpreter)) + .isEqualTo("jeff's & jack's bogüs journey"); + assertThat(f.filter(1, interpreter)).isEqualTo("1"); + } +} From e54afd97973f5cc6782adf90cec217a8612289c8 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 5 Dec 2022 18:32:36 -0500 Subject: [PATCH 085/637] Register UnescapeHtmlFilter --- src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java index 6fac24a36..544230d9c 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java @@ -112,6 +112,7 @@ protected void registerDefaults() { TrimFilter.class, TruncateFilter.class, TruncateHtmlFilter.class, + UnescapeHtmlFilter.class, UnionFilter.class, UniqueFilter.class, UnixTimestampFilter.class, From 2369646073b8aac91b2daa3314f57cdc75e8058a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Dec 2022 00:37:43 +0000 Subject: [PATCH 086/637] Bump commons-net from 3.3 to 3.9.0 Bumps commons-net from 3.3 to 3.9.0. --- updated-dependencies: - dependency-name: commons-net:commons-net dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 78969d2ff..e6e1e6d4b 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ commons-net commons-net - 3.3 + 3.9.0 com.googlecode.java-ipv6 From 69fe6c1cd2484e4a4979ffa98fa7e84c0a519c89 Mon Sep 17 00:00:00 2001 From: manheychiu <119883507+manheychiu@users.noreply.github.com> Date: Tue, 6 Dec 2022 09:25:00 +0000 Subject: [PATCH 087/637] Add null check before collecting to a map in eager (#968) This is causing `NullPointerException` in `java.util.stream.Collectors::uniqKeysMapAccumulator`. Therefore, a null check is added. Co-authored-by: Manhey Chiu --- .../util/EagerReconstructionUtils.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 05bb58833..cbb59ebf9 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -29,6 +29,7 @@ import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; +import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -102,16 +103,16 @@ public static EagerExecutionResult executeInChildContext( .entrySet() .stream() .filter(e -> !metaContextVariables.contains(e.getKey())) - .filter( - entry -> - !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null + .filter(e -> !(e.getValue() instanceof DeferredValue)) + .map( + e -> + new AbstractMap.SimpleImmutableEntry<>( + e.getKey(), + getObjectOrHashCode(e.getValue()) + ) ) - .collect( - Collectors.toMap( - Entry::getKey, - entry -> getObjectOrHashCode(entry.getValue()) - ) - ); + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); initiallyResolvedAsStrings = new HashMap<>(); // This creates a stringified snapshot of the context // so it can be disabled via the config because it may cause performance issues. From 33c3e328aeb70bde9dd2fb04a2d0640b26ea25b6 Mon Sep 17 00:00:00 2001 From: Libo Song Date: Fri, 9 Dec 2022 13:30:05 -0500 Subject: [PATCH 088/637] Fix stack overflow for self-nested pymap. --- .../hubspot/jinjava/objects/collections/PyMap.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/hubspot/jinjava/objects/collections/PyMap.java b/src/main/java/com/hubspot/jinjava/objects/collections/PyMap.java index 05c715831..cf867bfed 100644 --- a/src/main/java/com/hubspot/jinjava/objects/collections/PyMap.java +++ b/src/main/java/com/hubspot/jinjava/objects/collections/PyMap.java @@ -58,4 +58,15 @@ public void putAll(Map m) { } super.putAll(m); } + + @Override + public int hashCode() { + int h = 0; + for (Entry entry : map.entrySet()) { + if (entry.getValue() != map && entry.getValue() != this) { + h += entry.hashCode(); + } + } + return h; + } } From 60e931fddf7be8d72c9fa350172059de90e4cb49 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 13 Dec 2022 14:53:20 -0500 Subject: [PATCH 089/637] Handle alternate exptest syntax for is false and is true exp tests --- .../jinjava/el/ext/ExtendedParser.java | 3 +- .../jinjava/el/ExpressionResolverTest.java | 34 +++++++++++++++ .../jinjava/el/ext/ExtendedParserTest.java | 43 ++++++++++++++++++- 3 files changed, 78 insertions(+), 2 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 fb26213c0..9546d8672 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java @@ -281,9 +281,10 @@ protected AstNode nonliteral() throws ScanException, ParseException { switch (getToken().getSymbol()) { case IDENTIFIER: String name = consumeToken().getImage(); + Symbol lookahead = lookahead(0).getSymbol(); if ( getToken().getSymbol() == COLON && - lookahead(0).getSymbol() == IDENTIFIER && + (lookahead == IDENTIFIER || lookahead == FALSE || lookahead == TRUE) && (lookahead(1).getSymbol() == LPAREN || (isPossibleExpTestOrFilter(name))) ) { // ns:f(...) consumeToken(); diff --git a/src/test/java/com/hubspot/jinjava/el/ExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/el/ExpressionResolverTest.java index fe0f731c3..e23668a11 100644 --- a/src/test/java/com/hubspot/jinjava/el/ExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ExpressionResolverTest.java @@ -602,6 +602,40 @@ public void itResolvesLazyExpressionsInNested() { assertThat(interpreter.getErrorsCopy()).isEmpty(); } + @Test + public void itResolvesAlternateExpTestSyntax() { + assertThat(interpreter.render("{% if 2 is even %}yes{% endif %}")).isEqualTo("yes"); + + assertThat( + interpreter.render( + "{% if exptest:even.evaluate(2, ____int3rpr3t3r____) %}yes{% endif %}" + ) + ) + .isEqualTo("yes"); + assertThat( + interpreter.render( + "{% if exptest:false.evaluate(false, ____int3rpr3t3r____) %}yes{% endif %}" + ) + ) + .isEqualTo("yes"); + } + + @Test + public void itResolvesAlternateExpTestSyntaxForTrueAndFalseExpTests() { + assertThat( + interpreter.render( + "{% if exptest:false.evaluate(false, ____int3rpr3t3r____) %}yes{% endif %}" + ) + ) + .isEqualTo("yes"); + assertThat( + interpreter.render( + "{% if exptest:true.evaluate(true, ____int3rpr3t3r____) %}yes{% endif %}" + ) + ) + .isEqualTo("yes"); + } + public String result(String value, TestClass testClass) { testClass.touch(); return value; 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 8513d6267..d8bd1667a 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/ExtendedParserTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/ExtendedParserTest.java @@ -3,8 +3,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import com.hubspot.jinjava.lib.exptest.IsEvenExpTest; +import com.hubspot.jinjava.lib.exptest.IsFalseExpTest; +import com.hubspot.jinjava.lib.exptest.IsTrueExpTest; import de.odysseus.el.tree.impl.Builder; +import de.odysseus.el.tree.impl.Builder.Feature; import de.odysseus.el.tree.impl.ast.AstBinary; +import de.odysseus.el.tree.impl.ast.AstDot; import de.odysseus.el.tree.impl.ast.AstIdentifier; import de.odysseus.el.tree.impl.ast.AstMethod; import de.odysseus.el.tree.impl.ast.AstNested; @@ -127,6 +132,39 @@ public void itParsesExpTestLikeDictionary() { assertThat(astNode).isInstanceOf(AstDict.class); } + @Test + public void itResolvesAlternateExpTestSyntax() { + AstNode regularSyntax = buildExpressionNodes("#{2 is even}"); + + assertThat(regularSyntax).isInstanceOf(AstMethod.class); + assertThat(regularSyntax.getChild(0)).isInstanceOf(AstDot.class); + assertThat(regularSyntax.getChild(1)).isInstanceOf(AstParameters.class); + AstNode alternateSyntax = buildExpressionNodes( + "#{exptest:even.evaluate(2, ____int3rpr3t3r____)}" + ); + + assertThat(alternateSyntax).isInstanceOf(AstMethod.class); + assertThat(alternateSyntax.getChild(0)).isInstanceOf(AstDot.class); + assertThat(alternateSyntax.getChild(1)).isInstanceOf(AstParameters.class); + } + + @Test + public void itResolvesAlternateExpTestSyntaxForTrueAndFalseExpTests() { + AstNode falseExpTest = buildExpressionNodes( + "#{exptest:false.evaluate(2, ____int3rpr3t3r____)}" + ); + assertThat(falseExpTest).isInstanceOf(AstMethod.class); + assertThat(falseExpTest.getChild(0)).isInstanceOf(AstDot.class); + assertThat(falseExpTest.getChild(1)).isInstanceOf(AstParameters.class); + + AstNode trueExpTest = buildExpressionNodes( + "#{exptest:true.evaluate(2, ____int3rpr3t3r____)}" + ); + assertThat(trueExpTest).isInstanceOf(AstMethod.class); + assertThat(trueExpTest.getChild(0)).isInstanceOf(AstDot.class); + assertThat(trueExpTest.getChild(1)).isInstanceOf(AstParameters.class); + } + private void assertForExpression( AstNode astNode, String leftExpected, @@ -164,7 +202,10 @@ private void assertLeftAndRightByOperator( } private AstNode buildExpressionNodes(String input) { - ExtendedCustomParser extendedParser = new ExtendedCustomParser(new Builder(), input); + ExtendedCustomParser extendedParser = new ExtendedCustomParser( + new Builder(Feature.METHOD_INVOCATIONS), + input + ); extendedParser.consumeTokenExpose(); extendedParser.consumeTokenExpose(); From 8f0aa4ce680d2c3eef767e07b550912cda78d267 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 13 Dec 2022 14:57:41 -0500 Subject: [PATCH 090/637] Refactor out unnecessary call to `lookahead(0)` --- .../jinjava/el/ext/ExtendedParser.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 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 9546d8672..637bafaf8 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java @@ -281,15 +281,16 @@ protected AstNode nonliteral() throws ScanException, ParseException { switch (getToken().getSymbol()) { case IDENTIFIER: String name = consumeToken().getImage(); - Symbol lookahead = lookahead(0).getSymbol(); - if ( - getToken().getSymbol() == COLON && - (lookahead == IDENTIFIER || lookahead == FALSE || lookahead == TRUE) && - (lookahead(1).getSymbol() == LPAREN || (isPossibleExpTestOrFilter(name))) - ) { // ns:f(...) - consumeToken(); - name += ":" + getToken().getImage(); - consumeToken(); + if (getToken().getSymbol() == COLON) { + Symbol lookahead = lookahead(0).getSymbol(); + if ( + (lookahead == IDENTIFIER || lookahead == FALSE || lookahead == TRUE) && + (lookahead(1).getSymbol() == LPAREN || (isPossibleExpTestOrFilter(name))) + ) { // ns:f(...) + consumeToken(); + name += ":" + getToken().getImage(); + consumeToken(); + } } if (getToken().getSymbol() == LPAREN) { // function v = function(name, params()); From ea72b16dc29c70d39da9ad547f1e00025cbb152a Mon Sep 17 00:00:00 2001 From: Libo Song Date: Tue, 13 Dec 2022 17:08:48 -0500 Subject: [PATCH 091/637] Refactor, extract code blocks to separate functions. --- .../util/EagerReconstructionUtils.java | 233 ++++++++++++------ 1 file changed, 151 insertions(+), 82 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index cbb59ebf9..5652c4d52 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -29,7 +29,6 @@ import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; -import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -98,78 +97,9 @@ public static EagerExecutionResult executeInChildContext( final Map initiallyResolvedAsStrings; if (eagerChildContextConfig.checkForContextChanges) { initiallyResolvedHashes = - interpreter - .getContext() - .entrySet() - .stream() - .filter(e -> !metaContextVariables.contains(e.getKey())) - .filter(e -> !(e.getValue() instanceof DeferredValue)) - .map( - e -> - new AbstractMap.SimpleImmutableEntry<>( - e.getKey(), - getObjectOrHashCode(e.getValue()) - ) - ) - .filter(e -> e.getValue() != null) - .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); - initiallyResolvedAsStrings = new HashMap<>(); - // This creates a stringified snapshot of the context - // so it can be disabled via the config because it may cause performance issues. - Stream> entryStream; - if (!interpreter.getConfig().getExecutionMode().useEagerContextReverting()) { - entryStream = - interpreter - .getContext() - .getCombinedScope() - .entrySet() - .stream() - .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) - .filter( - entry -> - EagerExpressionResolver.isResolvableObject(entry.getValue(), 4, 400) // TODO make this configurable - ); - } else { - entryStream = - interpreter - .getContext() - .entrySet() - .stream() - .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) - .filter( - entry -> - EagerExpressionResolver.isResolvableObject(entry.getValue(), 4, 400) // TODO make this configurable - ); - } - entryStream.forEach( - entry -> { - RevertibleObject revertibleObject = interpreter - .getRevertibleObjects() - .get(entry.getKey()); - Object hashCode = initiallyResolvedHashes.get(entry.getKey()); - try { - if ( - revertibleObject == null || !hashCode.equals(revertibleObject.getHashCode()) - ) { - revertibleObject = - new RevertibleObject( - hashCode, - PyishObjectMapper.getAsPyishStringOrThrow(entry.getValue()) - ); - interpreter.getRevertibleObjects().put(entry.getKey(), revertibleObject); - } - revertibleObject - .getPyishString() - .ifPresent( - pyishString -> initiallyResolvedAsStrings.put(entry.getKey(), pyishString) - ); - } catch (Exception e) { - interpreter - .getRevertibleObjects() - .put(entry.getKey(), new RevertibleObject(hashCode)); - } - } - ); + getInitiallyResolvedHashes(interpreter, metaContextVariables); + initiallyResolvedAsStrings = + getInitiallyResolvedAsStrings(interpreter, initiallyResolvedHashes); } else { initiallyResolvedHashes = Collections.emptyMap(); initiallyResolvedAsStrings = Collections.emptyMap(); @@ -262,19 +192,158 @@ public static EagerExecutionResult executeInChildContext( ); } speculativeBindings = - speculativeBindings - .entrySet() - .stream() - .filter(entry -> !metaContextVariables.contains(entry.getKey())) - .filter( - entry -> - !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null - ) // these are already set recursively - .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + filterSpeculativeBindings(speculativeBindings, metaContextVariables); return new EagerExecutionResult(result, speculativeBindings); } + private static Map getInitiallyResolvedAsStrings( + JinjavaInterpreter interpreter, + Map initiallyResolvedHashes + ) { + Map initiallyResolvedAsStrings = new HashMap<>(); + // This creates a stringified snapshot of the context + // so it can be disabled via the config because it may cause performance issues. + Stream> entryStream; + if (!interpreter.getConfig().getExecutionMode().useEagerContextReverting()) { + entryStream = + interpreter + .getContext() + .getCombinedScope() + .entrySet() + .stream() + .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) + .filter( + entry -> EagerExpressionResolver.isResolvableObject(entry.getValue(), 4, 400) // TODO make this configurable + ); + } else { + entryStream = + interpreter + .getContext() + .entrySet() + .stream() + .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) + .filter( + entry -> EagerExpressionResolver.isResolvableObject(entry.getValue(), 4, 400) // TODO make this configurable + ); + } + entryStream.forEach( + entry -> + putInitiallyResolvedAsString( + interpreter, + entry, + initiallyResolvedHashes, + initiallyResolvedAsStrings + ) + ); + return initiallyResolvedAsStrings; + } + + private static void putInitiallyResolvedAsString( + JinjavaInterpreter interpreter, + Entry entry, + Map initiallyResolvedHashes, + Map initiallyResolvedAsStrings + ) { + RevertibleObject revertibleObject = interpreter + .getRevertibleObjects() + .get(entry.getKey()); + Object hashCode = initiallyResolvedHashes.get(entry.getKey()); + try { + if (revertibleObject == null || !hashCode.equals(revertibleObject.getHashCode())) { + revertibleObject = + new RevertibleObject( + hashCode, + PyishObjectMapper.getAsPyishStringOrThrow(entry.getValue()) + ); + interpreter.getRevertibleObjects().put(entry.getKey(), revertibleObject); + } + revertibleObject + .getPyishString() + .ifPresent( + pyishString -> initiallyResolvedAsStrings.put(entry.getKey(), pyishString) + ); + } catch (Exception e) { + interpreter + .getRevertibleObjects() + .put(entry.getKey(), new RevertibleObject(hashCode)); + } + } + + private static Map getInitiallyResolvedHashes( + JinjavaInterpreter interpreter, + Set metaContextVariables + ) { + Set> entrySet = interpreter.getContext().entrySet(); + Map resolved = new HashMap<>(); + for (Entry entry : entrySet) { + if (metaContextVariables.contains(entry.getKey())) { + continue; + } + if (entry.getValue() instanceof DeferredValue || entry.getValue() == null) { + continue; + } + resolved.put(entry.getKey(), getObjectOrHashCode(entry.getValue())); + } + return resolved; + } + + private static Object mapSpeculativeValue( + Map.Entry entry, + EagerChildContextConfig eagerChildContextConfig, + Map initiallyResolvedHashes, + Map initiallyResolvedAsStrings, + JinjavaInterpreter interpreter + ) { + if (eagerChildContextConfig.takeNewValue) { + if (entry.getValue() instanceof DeferredValue) { + return ((DeferredValue) entry.getValue()).getOriginalValue(); + } + return entry.getValue(); + } + + if ( + entry.getValue() instanceof DeferredValue && + initiallyResolvedHashes + .get(entry.getKey()) + .equals( + getObjectOrHashCode(((DeferredValue) entry.getValue()).getOriginalValue()) + ) + ) { + return ((DeferredValue) entry.getValue()).getOriginalValue(); + } + + // This is necessary if a state-changing function, such as .update() + // or .append() is run against a variable in the context. + // It will revert the effects when takeNewValue is false. + if (initiallyResolvedAsStrings.containsKey(entry.getKey())) { + // convert to new list or map + try { + return interpreter.resolveELExpression( + initiallyResolvedAsStrings.get(entry.getKey()), + interpreter.getLineNumber() + ); + } catch (DeferredValueException ignored) {} + } + + // Previous value could not be mapped to a string + throw new DeferredValueException(entry.getKey()); + } + + private static Map filterSpeculativeBindings( + Map speculativeBindings, + Set metaContextVariables + ) { + return speculativeBindings + .entrySet() + .stream() + .filter(entry -> !metaContextVariables.contains(entry.getKey())) + .filter( + entry -> !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null + ) // these are already set recursively + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + } + private static Object getObjectOrHashCode(Object o) { if (o instanceof LazyExpression) { o = ((LazyExpression) o).get(); From 62cd20fba9163cd6faf92ae1e9643e1a68198e2e Mon Sep 17 00:00:00 2001 From: Libo Song Date: Tue, 13 Dec 2022 17:12:45 -0500 Subject: [PATCH 092/637] Refactor, extract code blocks to separate functions. --- .../util/EagerReconstructionUtils.java | 46 ++++--------------- 1 file changed, 8 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 5652c4d52..864015ec2 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -150,50 +150,20 @@ public static EagerExecutionResult executeInChildContext( .collect( Collectors.toMap( Entry::getKey, - e -> { - if (eagerChildContextConfig.takeNewValue) { - if (e.getValue() instanceof DeferredValue) { - return ((DeferredValue) e.getValue()).getOriginalValue(); - } - return e.getValue(); - } - - if ( - e.getValue() instanceof DeferredValue && - initiallyResolvedHashes - .get(e.getKey()) - .equals( - getObjectOrHashCode( - ((DeferredValue) e.getValue()).getOriginalValue() - ) - ) - ) { - return ((DeferredValue) e.getValue()).getOriginalValue(); - } - - // This is necessary if a state-changing function, such as .update() - // or .append() is run against a variable in the context. - // It will revert the effects when takeNewValue is false. - if (initiallyResolvedAsStrings.containsKey(e.getKey())) { - // convert to new list or map - try { - return interpreter.resolveELExpression( - initiallyResolvedAsStrings.get(e.getKey()), - interpreter.getLineNumber() - ); - } catch (DeferredValueException ignored) {} - } - - // Previous value could not be mapped to a string - throw new DeferredValueException(e.getKey()); - } + e -> + mapSpeculativeValue( + e, + eagerChildContextConfig, + initiallyResolvedHashes, + initiallyResolvedAsStrings, + interpreter + ) ) ) ); } speculativeBindings = filterSpeculativeBindings(speculativeBindings, metaContextVariables); - return new EagerExecutionResult(result, speculativeBindings); } From 0a01ea949320fe57e5929d36f7a9cec1f5f11e87 Mon Sep 17 00:00:00 2001 From: Libo Song Date: Tue, 13 Dec 2022 17:15:23 -0500 Subject: [PATCH 093/637] Refactor, get changed values. --- .../util/EagerReconstructionUtils.java | 63 ++++++++++++------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 864015ec2..0676a04c8 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -29,6 +29,7 @@ import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; +import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -136,30 +137,12 @@ public static EagerExecutionResult executeInChildContext( .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); if (eagerChildContextConfig.checkForContextChanges) { speculativeBindings.putAll( - interpreter - .getContext() - .entrySet() - .stream() - .filter(e -> initiallyResolvedHashes.containsKey(e.getKey())) - .filter( - e -> - !initiallyResolvedHashes - .get(e.getKey()) - .equals(getObjectOrHashCode(e.getValue())) - ) - .collect( - Collectors.toMap( - Entry::getKey, - e -> - mapSpeculativeValue( - e, - eagerChildContextConfig, - initiallyResolvedHashes, - initiallyResolvedAsStrings, - interpreter - ) - ) - ) + getChangedValues( + interpreter, + eagerChildContextConfig, + initiallyResolvedHashes, + initiallyResolvedAsStrings + ) ); } speculativeBindings = @@ -167,6 +150,38 @@ public static EagerExecutionResult executeInChildContext( return new EagerExecutionResult(result, speculativeBindings); } + private static Map getChangedValues( + JinjavaInterpreter interpreter, + EagerChildContextConfig eagerChildContextConfig, + Map initiallyResolvedHashes, + Map initiallyResolvedAsStrings + ) { + Set> initialEntrySet = initiallyResolvedHashes.entrySet(); + Context context = interpreter.getContext(); + Map changedValues = new HashMap<>(); + for (Entry entry : initialEntrySet) { + Object newValue = context.get(entry.getKey()); + if (newValue == null) { + continue; + } + if (entry.getValue().equals(getObjectOrHashCode(newValue))) { + continue; + } + // New value different, changes. + changedValues.put( + entry.getKey(), + mapSpeculativeValue( + new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), newValue), + eagerChildContextConfig, + initiallyResolvedHashes, + initiallyResolvedAsStrings, + interpreter + ) + ); + } + return changedValues; + } + private static Map getInitiallyResolvedAsStrings( JinjavaInterpreter interpreter, Map initiallyResolvedHashes From 6f5654daf87f14b72a80a88f642b4d2f524268d1 Mon Sep 17 00:00:00 2001 From: Libo Song Date: Tue, 13 Dec 2022 17:48:00 -0500 Subject: [PATCH 094/637] Save one map get. --- .../jinjava/util/EagerReconstructionUtils.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 0676a04c8..54a21c0d3 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -121,18 +121,20 @@ public static EagerExecutionResult executeInChildContext( ? Collections.emptyMap() : interpreter.getContext().getSessionBindings(); } + speculativeBindings = speculativeBindings .entrySet() .stream() .filter( - entry -> - entry.getValue() != null && - !entry.getValue().equals(interpreter.getContext().get(entry.getKey())) - ) - .filter( - entry -> - !(interpreter.getContext().get(entry.getKey()) instanceof DeferredValue) + entry -> { + Object newValue = interpreter.getContext().get(entry.getKey()); + return ( + entry.getValue() != null && + !entry.getValue().equals(newValue) && + !(newValue instanceof DeferredValue) + ); + } ) .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); if (eagerChildContextConfig.checkForContextChanges) { From dae2a1dcd9cf1bb2a936908678e1cf5f9cec3bae Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 16 Dec 2022 11:34:54 -0500 Subject: [PATCH 095/637] Disallow methods that modify lists or maps while in deferred eager execution mode --- .../jinjava/el/ext/JinjavaBeanELResolver.java | 37 ++ .../jinjava/lib/tag/eager/EagerForTag.java | 6 +- .../jinjava/lib/tag/eager/EagerImportTag.java | 51 ++- .../util/EagerReconstructionUtils.java | 352 ++++++++++++------ .../tags/eager/importtag/var-a.jinja | 0 .../tags/eager/importtag/var-b.jinja | 0 6 files changed, 324 insertions(+), 122 deletions(-) create mode 100644 src/test/resources/tags/eager/importtag/var-a.jinja create mode 100644 src/test/resources/tags/eager/importtag/var-b.jinja diff --git a/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java index 30549acc9..75a140aa2 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java @@ -2,7 +2,9 @@ import com.google.common.base.CaseFormat; import com.google.common.collect.ImmutableSet; +import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.util.EagerReconstructionUtils; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -33,6 +35,28 @@ public class JinjavaBeanELResolver extends BeanELResolver { .add("wait") .build(); + private static final Set DEFERRED_EXECUTION_RESTRICTED_METHODS = ImmutableSet + .builder() + .add("put") + .add("putAll") + .add("update") + .add("add") + .add("insert") + .add("pop") + .add("append") + .add("extend") + .add("clear") + .add("remove") + .add("addAll") + .add("removeAll") + .add("replace") + .add("replaceAll") + .add("putIfAbsent") + .add("sort") + .add("set") + .add("merge") + .build(); + /** * Creates a new read/write {@link JinjavaBeanELResolver}. */ @@ -86,6 +110,19 @@ public Object invoke( ); } + if ( + DEFERRED_EXECUTION_RESTRICTED_METHODS.contains(method.toString()) && + EagerReconstructionUtils.isDeferredExecutionMode() + ) { + throw new DeferredValueException( + String.format( + "Cannot run method '%s' in %s in deferred execution mode", + method, + base.getClass() + ) + ); + } + Object result = super.invoke(context, base, method, paramTypes, params); if (isRestrictedClass(result)) { diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index d3a3fa2c6..9a5de5432 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -45,7 +45,11 @@ public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { return expressionResult; }, interpreter, - EagerChildContextConfig.newBuilder().withCheckForContextChanges(true).build() + EagerChildContextConfig + .newBuilder() + .withCheckForContextChanges(true) + .withCreateReconstructedContext(true) + .build() ); if ( result.getResult().getResolutionState() == ResolutionState.NONE || diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java index ef45d3e18..c78ba29a3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java @@ -9,6 +9,7 @@ import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.lib.fn.eager.EagerMacroFunction; import com.hubspot.jinjava.lib.tag.ImportTag; import com.hubspot.jinjava.lib.tag.SetTag; import com.hubspot.jinjava.loader.RelativePathResolver; @@ -16,7 +17,9 @@ import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.tree.Node; import com.hubspot.jinjava.tree.parse.TagToken; +import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import com.hubspot.jinjava.util.EagerReconstructionUtils; +import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -92,7 +95,13 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter // If the template depends on deferred values it should not be rendered, // and all defined variables and macros should be deferred too. - if (!child.getContext().getDeferredNodes().isEmpty()) { + if ( + !child.getContext().getDeferredNodes().isEmpty() || + ( + interpreter.getContext().isDeferredExecutionMode() && + !child.getContext().getGlobalMacros().isEmpty() + ) + ) { ImportTag.handleDeferredNodesDuringImport( node, currentImportAlias, @@ -164,13 +173,28 @@ private String getSetTagForDeferredChildBindings( String currentImportAlias, Map childBindings ) { + if (Strings.isNullOrEmpty(currentImportAlias)) { + // defer imported variables + EagerReconstructionUtils.buildSetTag( + childBindings + .entrySet() + .stream() + .filter( + entry -> + !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null + ) + .collect(Collectors.toMap(Entry::getKey, entry -> "")), + interpreter, + true + ); + } return EagerReconstructionUtils.buildSetTag( childBindings .entrySet() .stream() + .filter(entry -> entry.getValue() instanceof DeferredValue) .filter(entry -> !interpreter.getContext().containsKey(entry.getKey())) .filter(entry -> !entry.getKey().equals(currentImportAlias)) - .filter(entry -> entry.getValue() instanceof DeferredValue) .collect( Collectors.toMap( Entry::getKey, @@ -341,15 +365,32 @@ public static void integrateChild( JinjavaInterpreter parent ) { childBindings.remove(SetTag.IGNORED_VARIABLE_NAME); + for (MacroFunction macro : child.getContext().getGlobalMacros().values()) { + if (parent.getContext().isDeferredExecutionMode()) { + macro.setDeferred(true); + } + } if (StringUtils.isBlank(currentImportAlias)) { for (MacroFunction macro : child.getContext().getGlobalMacros().values()) { parent.getContext().addGlobalMacro(macro); } childBindings.remove(Context.GLOBAL_MACROS_SCOPE_KEY); childBindings.remove(Context.IMPORT_RESOURCE_ALIAS_KEY); - parent - .getContext() - .putAll(ImportTag.getChildBindingsWithoutImportResourcePath(childBindings)); + Map childBindingsWithoutImportResourcePath = ImportTag.getChildBindingsWithoutImportResourcePath( + childBindings + ); + if (parent.getContext().isDeferredExecutionMode()) { + childBindingsWithoutImportResourcePath + .keySet() + .forEach( + key -> + parent + .getContext() + .put(key, DeferredValue.instance(parent.getContext().get(key))) + ); + } else { + parent.getContext().putAll(childBindingsWithoutImportResourcePath); + } } else { childBindings.putAll(child.getContext().getGlobalMacros()); Map mapForCurrentContextAlias = getMapForCurrentContextAlias( diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 05bb58833..24fa72fb9 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -29,6 +29,7 @@ import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; +import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -95,7 +96,7 @@ public static EagerExecutionResult executeInChildContext( Set metaContextVariables = interpreter.getContext().getMetaContextVariables(); final Map initiallyResolvedHashes; final Map initiallyResolvedAsStrings; - if (eagerChildContextConfig.checkForContextChanges) { + if (eagerChildContextConfig.createReconstructedContext) { initiallyResolvedHashes = interpreter .getContext() @@ -109,65 +110,30 @@ public static EagerExecutionResult executeInChildContext( .collect( Collectors.toMap( Entry::getKey, - entry -> getObjectOrHashCode(entry.getValue()) + entry -> getObjectOrHashCode(entry.getValue(), eagerChildContextConfig) ) ); initiallyResolvedAsStrings = new HashMap<>(); // This creates a stringified snapshot of the context // so it can be disabled via the config because it may cause performance issues. - Stream> entryStream; - if (!interpreter.getConfig().getExecutionMode().useEagerContextReverting()) { - entryStream = - interpreter - .getContext() - .getCombinedScope() - .entrySet() - .stream() - .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) - .filter( - entry -> - EagerExpressionResolver.isResolvableObject(entry.getValue(), 4, 400) // TODO make this configurable - ); - } else { - entryStream = - interpreter - .getContext() - .entrySet() - .stream() - .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) - .filter( - entry -> - EagerExpressionResolver.isResolvableObject(entry.getValue(), 4, 400) // TODO make this configurable - ); - } + Stream> entryStream = + ( + interpreter.getConfig().getExecutionMode().useEagerContextReverting() + ? interpreter.getContext().entrySet() + : interpreter.getContext().getCombinedScope().entrySet() + ).stream() + .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) + .filter( + entry -> EagerExpressionResolver.isResolvableObject(entry.getValue(), 4, 400) // TODO make this configurable + ); entryStream.forEach( - entry -> { - RevertibleObject revertibleObject = interpreter - .getRevertibleObjects() - .get(entry.getKey()); - Object hashCode = initiallyResolvedHashes.get(entry.getKey()); - try { - if ( - revertibleObject == null || !hashCode.equals(revertibleObject.getHashCode()) - ) { - revertibleObject = - new RevertibleObject( - hashCode, - PyishObjectMapper.getAsPyishStringOrThrow(entry.getValue()) - ); - interpreter.getRevertibleObjects().put(entry.getKey(), revertibleObject); - } - revertibleObject - .getPyishString() - .ifPresent( - pyishString -> initiallyResolvedAsStrings.put(entry.getKey(), pyishString) - ); - } catch (Exception e) { - interpreter - .getRevertibleObjects() - .put(entry.getKey(), new RevertibleObject(hashCode)); - } - } + entry -> + cacheRevertibleObject( + interpreter, + initiallyResolvedHashes, + initiallyResolvedAsStrings, + entry + ) ); } else { initiallyResolvedHashes = Collections.emptyMap(); @@ -189,6 +155,90 @@ public static EagerExecutionResult executeInChildContext( ? Collections.emptyMap() : interpreter.getContext().getSessionBindings(); } + if (eagerChildContextConfig.createReconstructedContext) { + speculativeBindings = + getAllSpeculativeBindings( + interpreter, + eagerChildContextConfig, + metaContextVariables, + initiallyResolvedHashes, + initiallyResolvedAsStrings, + speculativeBindings + ); + } else { + speculativeBindings = + getBasicSpeculativeBindings( + interpreter, + eagerChildContextConfig, + metaContextVariables, + speculativeBindings + ); + } + + return new EagerExecutionResult(result, speculativeBindings); + } + + private static Map getBasicSpeculativeBindings( + JinjavaInterpreter interpreter, + EagerChildContextConfig eagerChildContextConfig, + Set metaContextVariables, + Map speculativeBindings + ) { + return speculativeBindings + .entrySet() + .stream() + .filter(entry -> !metaContextVariables.contains(entry.getKey())) + .filter(entry -> !"loop".equals(entry.getKey())) + .map( + entry -> { + if ( + eagerChildContextConfig.takeNewValue && + !(entry.getValue() instanceof DeferredValue) && + entry.getValue() != null + ) { + return entry; + } + Object contextValue = interpreter.getContext().get(entry.getKey()); + if ( + contextValue instanceof DeferredValue && + ((DeferredValue) contextValue).getOriginalValue() != null + ) { + if ( + !eagerChildContextConfig.takeNewValue && + !EagerExpressionResolver.isResolvableObject( + ((DeferredValue) contextValue).getOriginalValue() + ) + ) { + throw new DeferredValueException(entry.getKey()); + } + return new AbstractMap.SimpleImmutableEntry<>( + entry.getKey(), + ((DeferredValue) contextValue).getOriginalValue() + ); + } + return null; + } + ) + .filter(Objects::nonNull) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> + entry.getValue() instanceof DeferredValue + ? ((DeferredValue) entry.getValue()).getOriginalValue() + : entry.getValue() + ) + ); + } + + private static Map getAllSpeculativeBindings( + JinjavaInterpreter interpreter, + EagerChildContextConfig eagerChildContextConfig, + Set metaContextVariables, + Map initiallyResolvedHashes, + Map initiallyResolvedAsStrings, + Map speculativeBindings + ) { speculativeBindings = speculativeBindings .entrySet() @@ -203,63 +253,33 @@ public static EagerExecutionResult executeInChildContext( !(interpreter.getContext().get(entry.getKey()) instanceof DeferredValue) ) .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); - if (eagerChildContextConfig.checkForContextChanges) { - speculativeBindings.putAll( - interpreter - .getContext() - .entrySet() - .stream() - .filter(e -> initiallyResolvedHashes.containsKey(e.getKey())) - .filter( + speculativeBindings.putAll( + interpreter + .getContext() + .entrySet() + .stream() + .filter(e -> initiallyResolvedHashes.containsKey(e.getKey())) + .filter( + e -> + !initiallyResolvedHashes + .get(e.getKey()) + .equals(getObjectOrHashCode(e.getValue(), eagerChildContextConfig)) + ) + .collect( + Collectors.toMap( + Entry::getKey, e -> - !initiallyResolvedHashes - .get(e.getKey()) - .equals(getObjectOrHashCode(e.getValue())) + getOriginalValue( + interpreter, + eagerChildContextConfig, + initiallyResolvedHashes, + initiallyResolvedAsStrings, + e + ) ) - .collect( - Collectors.toMap( - Entry::getKey, - e -> { - if (eagerChildContextConfig.takeNewValue) { - if (e.getValue() instanceof DeferredValue) { - return ((DeferredValue) e.getValue()).getOriginalValue(); - } - return e.getValue(); - } - - if ( - e.getValue() instanceof DeferredValue && - initiallyResolvedHashes - .get(e.getKey()) - .equals( - getObjectOrHashCode( - ((DeferredValue) e.getValue()).getOriginalValue() - ) - ) - ) { - return ((DeferredValue) e.getValue()).getOriginalValue(); - } - - // This is necessary if a state-changing function, such as .update() - // or .append() is run against a variable in the context. - // It will revert the effects when takeNewValue is false. - if (initiallyResolvedAsStrings.containsKey(e.getKey())) { - // convert to new list or map - try { - return interpreter.resolveELExpression( - initiallyResolvedAsStrings.get(e.getKey()), - interpreter.getLineNumber() - ); - } catch (DeferredValueException ignored) {} - } - - // Previous value could not be mapped to a string - throw new DeferredValueException(e.getKey()); - } - ) - ) - ); - } + ) + ); + speculativeBindings = speculativeBindings .entrySet() @@ -270,19 +290,99 @@ public static EagerExecutionResult executeInChildContext( !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null ) // these are already set recursively .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + return speculativeBindings; + } - return new EagerExecutionResult(result, speculativeBindings); + private static void cacheRevertibleObject( + JinjavaInterpreter interpreter, + Map initiallyResolvedHashes, + Map initiallyResolvedAsStrings, + Entry entry + ) { + RevertibleObject revertibleObject = interpreter + .getRevertibleObjects() + .get(entry.getKey()); + Object hashCode = initiallyResolvedHashes.get(entry.getKey()); + try { + if (revertibleObject == null || !hashCode.equals(revertibleObject.getHashCode())) { + revertibleObject = + new RevertibleObject( + hashCode, + PyishObjectMapper.getAsPyishStringOrThrow(entry.getValue()) + ); + interpreter.getRevertibleObjects().put(entry.getKey(), revertibleObject); + } + revertibleObject + .getPyishString() + .ifPresent( + pyishString -> initiallyResolvedAsStrings.put(entry.getKey(), pyishString) + ); + } catch (Exception e) { + interpreter + .getRevertibleObjects() + .put(entry.getKey(), new RevertibleObject(hashCode)); + } + } + + private static Object getOriginalValue( + JinjavaInterpreter interpreter, + EagerChildContextConfig eagerChildContextConfig, + Map initiallyResolvedHashes, + Map initiallyResolvedAsStrings, + Entry e + ) { + if (eagerChildContextConfig.takeNewValue) { + if (e.getValue() instanceof DeferredValue) { + return ((DeferredValue) e.getValue()).getOriginalValue(); + } + return e.getValue(); + } + + if ( + e.getValue() instanceof DeferredValue && + initiallyResolvedHashes + .get(e.getKey()) + .equals( + getObjectOrHashCode( + ((DeferredValue) e.getValue()).getOriginalValue(), + eagerChildContextConfig + ) + ) + ) { + return ((DeferredValue) e.getValue()).getOriginalValue(); + } + + // This is necessary if a state-changing function, such as .update() + // or .append() is run against a variable in the context. + // It will revert the effects when takeNewValue is false. + if (initiallyResolvedAsStrings.containsKey(e.getKey())) { + // convert to new list or map + try { + return interpreter.resolveELExpression( + initiallyResolvedAsStrings.get(e.getKey()), + interpreter.getLineNumber() + ); + } catch (DeferredValueException ignored) {} + } + + // Previous value could not be mapped to a string + throw new DeferredValueException(e.getKey()); } - private static Object getObjectOrHashCode(Object o) { + private static Object getObjectOrHashCode( + Object o, + EagerChildContextConfig eagerChildContextConfig + ) { if (o instanceof LazyExpression) { o = ((LazyExpression) o).get(); } - if (o instanceof PyList && !((PyList) o).toList().contains(o)) { - return o.hashCode(); - } - if (o instanceof PyMap && !((PyMap) o).toMap().containsValue(o)) { - return o.hashCode() + ((PyMap) o).keySet().hashCode(); + if (eagerChildContextConfig.createReconstructedContext) { + if (o instanceof PyList && !((PyList) o).toList().contains(o)) { + return o.hashCode(); + } + if (o instanceof PyMap && !((PyMap) o).toMap().containsValue(o)) { + return o.hashCode() + ((PyMap) o).keySet().hashCode(); + } } return o; } @@ -359,6 +459,7 @@ private static String reconstructMacroFunctionsBeforeDeferring( .newBuilder() .withForceDeferredExecutionMode(true) .withCheckForContextChanges(true) + .withCreateReconstructedContext(true) .build() ) ) @@ -745,12 +846,21 @@ public static void removeMetaContextVariables( context.getMetaContextVariables().removeAll(metaSetVars); } + public static Boolean isDeferredExecutionMode() { + return JinjavaInterpreter + .getCurrentMaybe() + .map(interpreter -> interpreter.getContext().isDeferredExecutionMode()) + .orElse(false); + } + public static class EagerChildContextConfig { private final boolean takeNewValue; private final boolean discardSessionBindings; private final boolean partialMacroEvaluation; private final boolean checkForContextChanges; + + private final boolean createReconstructedContext; private final boolean forceDeferredExecutionMode; private EagerChildContextConfig( @@ -758,12 +868,14 @@ private EagerChildContextConfig( boolean discardSessionBindings, boolean partialMacroEvaluation, boolean checkForContextChanges, + boolean createReconstructedContext, boolean forceDeferredExecutionMode ) { this.takeNewValue = takeNewValue; this.discardSessionBindings = discardSessionBindings; this.partialMacroEvaluation = partialMacroEvaluation; this.checkForContextChanges = checkForContextChanges; + this.createReconstructedContext = createReconstructedContext; this.forceDeferredExecutionMode = forceDeferredExecutionMode; } @@ -777,6 +889,8 @@ public static class Builder { private boolean discardSessionBindings; private boolean partialMacroEvaluation; private boolean checkForContextChanges; + + private boolean createReconstructedContext; private boolean forceDeferredExecutionMode; private Builder() {} @@ -801,6 +915,11 @@ public Builder withCheckForContextChanges(boolean checkForContextChanges) { return this; } + public Builder withCreateReconstructedContext(boolean createReconstructedContext) { + this.createReconstructedContext = createReconstructedContext; + return this; + } + public Builder withForceDeferredExecutionMode(boolean forceDeferredExecutionMode) { this.forceDeferredExecutionMode = forceDeferredExecutionMode; return this; @@ -812,6 +931,7 @@ public EagerChildContextConfig build() { discardSessionBindings, partialMacroEvaluation, checkForContextChanges, + createReconstructedContext, forceDeferredExecutionMode ); } diff --git a/src/test/resources/tags/eager/importtag/var-a.jinja b/src/test/resources/tags/eager/importtag/var-a.jinja new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/tags/eager/importtag/var-b.jinja b/src/test/resources/tags/eager/importtag/var-b.jinja new file mode 100644 index 000000000..e69de29bb From d4a5483dae4ef3b850d5dc711b344f323286eaf2 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 16 Dec 2022 11:36:26 -0500 Subject: [PATCH 096/637] Add more testing to eager import tag functionality --- .../lib/tag/eager/EagerImportTagTest.java | 69 +++++++++++++++++++ .../tags/eager/importtag/var-a.jinja | 1 + .../tags/eager/importtag/var-b.jinja | 1 + 3 files changed, 71 insertions(+) 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 1d79b84c2..31321dd19 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 @@ -675,6 +675,75 @@ public void itDoesNotSilentlyOverrideMacro() { assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); } + @Test + public void itDoesNotSilentlyOverrideMacroWithoutAlias() { + setupResourceLocator(); + String result = interpreter.render( + "{% import 'macro-a.jinja' %}\n" + + "{{ doer() }}\n" + + "{% if deferred %}\n" + + " {% import 'macro-b.jinja' %}\n" + + "{% endif %}\n" + + "{{ doer() }}" + ); + assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); + } + + @Test + public void itDoesNotSilentlyOverrideVariable() { + setupResourceLocator(); + String result = interpreter + .render( + "{% import 'var-a.jinja' as vars %}" + + "{{ vars.foo }}" + + "{% if deferred %}" + + " {%- import 'var-b.jinja' as vars %}" + + "{% endif %}" + + "{{ vars.foo }}" + ) + .trim(); + assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); + assertThat(result) + .isEqualTo( + "a" + + "{% set vars = {'foo': 'a', 'import_resource_path': 'var-a.jinja'} %}{% if deferred %}" + + "{% set __ignored__ %}{% set current_path = 'var-b.jinja' %}{% set vars = {} %}{% set foo = 'b' %}{% do vars.update({'foo': foo}) %}\n" + + "{% do vars.update({'foo': 'b','import_resource_path': 'var-b.jinja'}) %}{% set current_path = '' %}{% endset %}" + + "{% endif %}" + + "{{ vars.foo }}" + ); + interpreter.getContext().put("deferred", "resolved"); + assertThat(interpreter.render(result)).isEqualTo("ab"); + } + + @Test + public void itDoesNotSilentlyOverrideVariableWithoutAlias() { + setupResourceLocator(); + String result = interpreter + .render( + "{% import 'var-a.jinja' %}" + + "{{ foo }}" + + "{% if deferred %}" + + " {%- import 'var-b.jinja' %}" + + "{% endif %}" + + "{{ foo }}" + ) + .trim(); + assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); + assertThat(result) + .isEqualTo( + "a" + + "{% set foo = 'a' %}{% if deferred %}" + + "{% set __ignored__ %}{% set current_path = 'var-b.jinja' %}{% set foo = 'b' %}\n" + + "{% set current_path = '' %}{% endset %}" + + "{% endif %}" + + "{{ foo }}" + ); + + interpreter.getContext().put("deferred", "resolved"); + assertThat(interpreter.render(result)).isEqualTo("ab"); + } + private static JinjavaInterpreter getChildInterpreter( JinjavaInterpreter interpreter, String alias diff --git a/src/test/resources/tags/eager/importtag/var-a.jinja b/src/test/resources/tags/eager/importtag/var-a.jinja index e69de29bb..945acff03 100644 --- a/src/test/resources/tags/eager/importtag/var-a.jinja +++ b/src/test/resources/tags/eager/importtag/var-a.jinja @@ -0,0 +1 @@ +{% set foo = 'a' %} diff --git a/src/test/resources/tags/eager/importtag/var-b.jinja b/src/test/resources/tags/eager/importtag/var-b.jinja index e69de29bb..2ecfd14ef 100644 --- a/src/test/resources/tags/eager/importtag/var-b.jinja +++ b/src/test/resources/tags/eager/importtag/var-b.jinja @@ -0,0 +1 @@ +{% set foo = 'b' %} From 9a3b478f7797e49e7dad2bfa31e03ba16ca9b9c3 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 16 Dec 2022 14:19:52 -0500 Subject: [PATCH 097/637] Defer for tag node if DVE is thrown inside executeInChildContext reconstruction logic --- .../com/hubspot/jinjava/lib/tag/eager/EagerForTag.java | 6 ++++++ .../hubspot/jinjava/util/EagerReconstructionUtils.java | 10 +++++++--- .../hubspot/jinjava/lib/tag/eager/EagerForTagTest.java | 4 +++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 9a5de5432..7d7440ede 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -82,6 +82,12 @@ public String eagerInterpret( JinjavaInterpreter interpreter, InterpretException e ) { + if ( + e instanceof DeferredValueException && + e.getMessage().startsWith(EagerReconstructionUtils.CANNOT_RECONSTRUCT_MESSAGE) + ) { + throw e; + } LengthLimitingStringBuilder result = new LengthLimitingStringBuilder( interpreter.getConfig().getMaxOutputSize() ); diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 24fa72fb9..f81feabca 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -43,6 +43,7 @@ import java.util.stream.Stream; public class EagerReconstructionUtils { + public static final String CANNOT_RECONSTRUCT_MESSAGE = "Cannot reconstruct value"; /** * Execute the specified functions within a protected context. @@ -209,7 +210,9 @@ private static Map getBasicSpeculativeBindings( ((DeferredValue) contextValue).getOriginalValue() ) ) { - throw new DeferredValueException(entry.getKey()); + throw new DeferredValueException( + String.format("%s: %s", CANNOT_RECONSTRUCT_MESSAGE, entry.getKey()) + ); } return new AbstractMap.SimpleImmutableEntry<>( entry.getKey(), @@ -366,7 +369,9 @@ private static Object getOriginalValue( } // Previous value could not be mapped to a string - throw new DeferredValueException(e.getKey()); + throw new DeferredValueException( + String.format("%s: %s", CANNOT_RECONSTRUCT_MESSAGE, e.getKey()) + ); } private static Object getObjectOrHashCode( @@ -459,7 +464,6 @@ private static String reconstructMacroFunctionsBeforeDeferring( .newBuilder() .withForceDeferredExecutionMode(true) .withCheckForContextChanges(true) - .withCreateReconstructedContext(true) .build() ) ) diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java index f7cc0187b..bba145982 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java @@ -16,6 +16,7 @@ import java.util.Optional; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; public class EagerForTagTest extends ForTagTest { @@ -219,6 +220,7 @@ public void itDefersLoopVariable() { } @Test + // @Ignore // Not needed since append is disallowed in deferred execution mode public void itDoesNotSwallowDeferredValueException() { interpreter.getContext().registerTag(new EagerDoTag()); interpreter.getContext().registerTag(new EagerIfTag()); @@ -229,7 +231,7 @@ public void itDoesNotSwallowDeferredValueException() { "{% for i in range(401) %}" + "{{ my_list.append(i) }}" + "{% endfor %}" + - "{% for i in [0, 1] %}" + + "{% for i in my_list.append(1) ? [0, 1] : [0] %}" + "{% for j in deferred %}" + "{% if loop.first %}" + "{% do my_list.append(1) %}" + From c1eb86e6f0cb43ae8955f676c5ee82e80a806c0d Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 16 Dec 2022 15:45:35 -0500 Subject: [PATCH 098/637] Check if any new speculative bindings exist in second for loop run to ignore already deferred values --- .../jinjava/lib/tag/eager/EagerForTag.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 7d7440ede..0373ab985 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -125,21 +125,27 @@ public String eagerInterpret( ); } - EagerExecutionResult eagerExecutionResult = runLoopOnce(tagNode, interpreter); - if (!eagerExecutionResult.getSpeculativeBindings().isEmpty()) { + EagerExecutionResult firstRunResult = runLoopOnce(tagNode, interpreter); + if (!firstRunResult.getSpeculativeBindings().isEmpty()) { // Defer any variables that we tried to modify during the loop - prefix = eagerExecutionResult.getPrefixToPreserveState(true); + prefix = firstRunResult.getPrefixToPreserveState(true); } // Run for loop again now that the necessary values have been deferred - eagerExecutionResult = runLoopOnce(tagNode, interpreter); - if (!eagerExecutionResult.getSpeculativeBindings().isEmpty()) { + EagerExecutionResult secondRunResult = runLoopOnce(tagNode, interpreter); + if ( + secondRunResult + .getSpeculativeBindings() + .keySet() + .stream() + .anyMatch(key -> !firstRunResult.getSpeculativeBindings().containsKey(key)) + ) { throw new DeferredValueException( "Modified values in deferred for loop: " + - String.join(", ", eagerExecutionResult.getSpeculativeBindings().keySet()) + String.join(", ", secondRunResult.getSpeculativeBindings().keySet()) ); } - result.append(eagerExecutionResult.asTemplateString()); + result.append(secondRunResult.asTemplateString()); result.append(EagerReconstructionUtils.reconstructEnd(tagNode)); return prefix + result; } From 22b6a332acb1154b0e76d1836c70f9f02ff36f86 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 16 Dec 2022 20:37:19 -0500 Subject: [PATCH 099/637] Deferred reference modification works! Committing before removing comments and cleaning up --- .../interpret/DeferredLazyReference.java | 2 +- .../DeferredLazyReferenceSource.java | 35 +++++++ .../jinjava/interpret/DeferredValueImpl.java | 1 + .../jinjava/interpret/LazyReference.java | 6 +- .../expression/EagerExpressionStrategy.java | 97 ++++++++++++++++++ .../jinjava/lib/tag/eager/EagerPrintTag.java | 99 +++++++++++++++++++ .../jinjava/util/DeferredValueUtils.java | 36 ++++++- .../util/EagerReconstructionUtils.java | 71 +++++++++++-- ...cope-reference-modification.expected.jinja | 18 ++-- ...-higher-scope-reference-modification.jinja | 23 ++++- 10 files changed, 364 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReferenceSource.java diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReference.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReference.java index 5a1ce338c..74e903bb9 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReference.java +++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReference.java @@ -15,7 +15,7 @@ public static DeferredLazyReference instance( } @Override - public Object getOriginalValue() { + public LazyReference getOriginalValue() { return lazyReference; } } diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReferenceSource.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReferenceSource.java new file mode 100644 index 000000000..e70899464 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReferenceSource.java @@ -0,0 +1,35 @@ +package com.hubspot.jinjava.interpret; + +public class DeferredLazyReferenceSource implements DeferredValue { + private static final DeferredLazyReferenceSource INSTANCE = new DeferredLazyReferenceSource(); + + private Object originalValue; + private boolean reconstructed; + + private DeferredLazyReferenceSource() {} + + private DeferredLazyReferenceSource(Object originalValue) { + this.originalValue = originalValue; + } + + @Override + public Object getOriginalValue() { + return originalValue; + } + + public static DeferredLazyReferenceSource instance() { + return INSTANCE; + } + + public static DeferredLazyReferenceSource instance(Object originalValue) { + return new DeferredLazyReferenceSource(originalValue); + } + + public boolean isReconstructed() { + return reconstructed; + } + + public void setReconstructed(boolean reconstructed) { + this.reconstructed = reconstructed; + } +} diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredValueImpl.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueImpl.java index a2b6e04b9..9cf6dfdc1 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/DeferredValueImpl.java +++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueImpl.java @@ -13,6 +13,7 @@ private DeferredValueImpl(Object originalValue) { this.originalValue = originalValue; } + @Override public Object getOriginalValue() { return originalValue; } diff --git a/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java b/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java index 456b12997..6a40714b5 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java +++ b/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java @@ -3,7 +3,7 @@ import com.hubspot.jinjava.objects.serialization.PyishSerializable; public class LazyReference extends LazyExpression implements PyishSerializable { - private final String referenceKey; + private String referenceKey; protected LazyReference(Context referenceContext, String referenceKey) { super(() -> referenceContext.get(referenceKey), "", Memoization.ON); @@ -19,6 +19,10 @@ public String getReferenceKey() { return referenceKey; } + public void setReferenceKey(String referenceKey) { + this.referenceKey = referenceKey; + } + @Override public String toPyishString() { return getReferenceKey(); diff --git a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java index f5f1c98aa..0375546f8 100644 --- a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java @@ -1,6 +1,8 @@ package com.hubspot.jinjava.lib.expression; import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.interpret.DeferredLazyReference; +import com.hubspot.jinjava.interpret.DeferredLazyReferenceSource; import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; @@ -8,12 +10,15 @@ import com.hubspot.jinjava.lib.tag.RawTag; import com.hubspot.jinjava.lib.tag.eager.DeferredToken; import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; +import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.tree.output.RenderedOutputNode; import com.hubspot.jinjava.tree.parse.ExpressionToken; import com.hubspot.jinjava.util.EagerExpressionResolver; import com.hubspot.jinjava.util.EagerReconstructionUtils; import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.Logging; +import java.util.AbstractMap; +import java.util.Map.Entry; import java.util.Objects; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -91,6 +96,98 @@ private String eagerResolveExpression( .collect(Collectors.toSet()) ) ); + prefixToPreserveState.append( + EagerReconstructionUtils.buildSetTag( + interpreter + .getContext() + .getScope() + .entrySet() + .stream() + .filter( + entry -> + entry.getValue() instanceof DeferredLazyReferenceSource && + !((DeferredLazyReferenceSource) entry.getValue()).isReconstructed() + ) + .peek( + entry -> + ((DeferredLazyReferenceSource) entry.getValue()).setReconstructed(true) + ) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> + PyishObjectMapper.getAsPyishString( + ((DeferredLazyReferenceSource) entry.getValue()).getOriginalValue() + ) + ) + ), + // eagerExecutionResult + // .getResult() + // .getDeferredWords() + // .stream() + // .map(w -> w.split("\\.", 2)[0]) + // .map(word -> interpreter.getContext().getScope().get(word)) + // .filter(value -> value instanceof DeferredLazyReference) + // .map(value -> (DeferredLazyReference) value) + // .map(DeferredLazyReference::getOriginalValue) + // .distinct() + // .filter(lazyReference -> lazyReference.) + //// .filter(lazyReference -> !(interpreter.getContext().get(lazyReference.getReferenceKey()) instanceof DeferredValue)) + // .peek(lazyReference -> interpreter.) + // .collect( + // Collectors.toMap( + // LazyReference::getReferenceKey, + // val -> PyishObjectMapper.getAsPyishString(val.get()) + // ) + // ), + interpreter, + false + ) + ); + prefixToPreserveState.append( + EagerReconstructionUtils.buildSetTag( + eagerExecutionResult + .getResult() + .getDeferredWords() + .stream() + .map(w -> w.split("\\.", 2)[0]) + .map( + word -> + new AbstractMap.SimpleImmutableEntry<>( + word, + interpreter.getContext().get(word) + ) + ) + .filter(entry -> entry.getValue() instanceof DeferredLazyReference) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> + PyishObjectMapper.getAsPyishString( + ((DeferredLazyReference) entry.getValue()).getOriginalValue() + ) + ) + ), + interpreter, + false + ) + ); + // prefixToPreserveState.append( + // EagerReconstructionUtils.reconstructFromContextBeforeDeferring( + // eagerExecutionResult + // .getResult() + // .getDeferredWords() + // .stream() + // .map(w -> w.split("\\.", 2)[0]) + // .map(word -> interpreter.getContext().get(word)) + // .filter(value -> value instanceof DeferredLazyReference) + // .map(value -> (DeferredLazyReference) value) + // .map(DeferredLazyReference::getOriginalValue) + // .map(LazyReference::getReferenceKey) + // .collect(Collectors.toSet()), + // interpreter + // ) + // ); // There is only a preserving prefix because it couldn't be entirely evaluated. return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( prefixToPreserveState.toString() + helpers, diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java index fc17576c7..51ce91b7a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java @@ -1,14 +1,21 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.hubspot.jinjava.interpret.DeferredLazyReference; +import com.hubspot.jinjava.interpret.DeferredLazyReferenceSource; import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; +import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.LazyReference; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.tag.PrintTag; +import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.util.EagerExpressionResolver; import com.hubspot.jinjava.util.EagerReconstructionUtils; import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.LengthLimitingStringJoiner; +import java.util.AbstractMap; +import java.util.Map.Entry; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -119,6 +126,98 @@ public static String interpretExpression( .collect(Collectors.toSet()) ) ); + prefixToPreserveState.append( + EagerReconstructionUtils.buildSetTag( + interpreter + .getContext() + .getScope() + .entrySet() + .stream() + .filter( + entry -> + entry.getValue() instanceof DeferredLazyReferenceSource && + !((DeferredLazyReferenceSource) entry.getValue()).isReconstructed() + ) + .peek( + entry -> + ((DeferredLazyReferenceSource) entry.getValue()).setReconstructed(true) + ) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> + PyishObjectMapper.getAsPyishString( + ((DeferredLazyReferenceSource) entry.getValue()).getOriginalValue() + ) + ) + ), + // eagerExecutionResult + // .getResult() + // .getDeferredWords() + // .stream() + // .map(w -> w.split("\\.", 2)[0]) + // .map(word -> interpreter.getContext().getScope().get(word)) + // .filter(value -> value instanceof DeferredLazyReference) + // .map(value -> (DeferredLazyReference) value) + // .map(DeferredLazyReference::getOriginalValue) + // .distinct() + // .filter(lazyReference -> lazyReference.) + //// .filter(lazyReference -> !(interpreter.getContext().get(lazyReference.getReferenceKey()) instanceof DeferredValue)) + // .peek(lazyReference -> interpreter.) + // .collect( + // Collectors.toMap( + // LazyReference::getReferenceKey, + // val -> PyishObjectMapper.getAsPyishString(val.get()) + // ) + // ), + interpreter, + false + ) + ); + prefixToPreserveState.append( + EagerReconstructionUtils.buildSetTag( + eagerExecutionResult + .getResult() + .getDeferredWords() + .stream() + .map(w -> w.split("\\.", 2)[0]) + .map( + word -> + new AbstractMap.SimpleImmutableEntry<>( + word, + interpreter.getContext().get(word) + ) + ) + .filter(entry -> entry.getValue() instanceof DeferredLazyReference) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> + PyishObjectMapper.getAsPyishString( + ((DeferredLazyReference) entry.getValue()).getOriginalValue() + ) + ) + ), + interpreter, + false + ) + ); + // prefixToPreserveState.append( + // EagerReconstructionUtils.reconstructFromContextBeforeDeferring( + // eagerExecutionResult + // .getResult() + // .getDeferredWords() + // .stream() + // .map(w -> w.split("\\.", 2)[0]) + // .map(word -> interpreter.getContext().get(word)) + // .filter(value -> value instanceof DeferredLazyReference) + // .map(value -> (DeferredLazyReference) value) + // .map(DeferredLazyReference::getOriginalValue) + // .map(LazyReference::getReferenceKey) + // .collect(Collectors.toSet()), + // interpreter + // ) + // ); // Possible set tag in front of this one. return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( prefixToPreserveState.toString() + joiner.toString(), diff --git a/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java b/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java index 2fcbf2f7d..281153af5 100644 --- a/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java @@ -6,6 +6,7 @@ import com.hubspot.jinjava.el.ext.AbstractCallableMethod; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredLazyReference; +import com.hubspot.jinjava.interpret.DeferredLazyReferenceSource; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.SetTag; @@ -20,6 +21,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.StringJoiner; @@ -146,23 +148,53 @@ public static Set findAndMarkDeferredProperties( referentialDefers.forEach( word -> { Object wordValue = context.get(word); + if ( !(wordValue instanceof DeferredValue) && !EagerExpressionResolver.isPrimitive(wordValue) ) { + DeferredLazyReference deferredLazyReference = DeferredLazyReference.instance( + context, + word + ); Context temp = context; + Set> matchingEntries = new HashSet<>(); while (temp.getParent() != null) { temp .getScope() .entrySet() .stream() - .filter(entry -> !entry.getKey().equals(word)) + // .filter(entry -> !entry.getKey().equals(word)) .filter(entry -> entry.getValue() == wordValue) .forEach( - entry -> entry.setValue(DeferredLazyReference.instance(context, word)) + entry -> { + matchingEntries.add(entry); + deferredLazyReference + .getOriginalValue() + .setReferenceKey(entry.getKey()); + } ); + // .forEach( + // entry -> entry.setValue(DeferredLazyReference.instance(context, word)) + // ); temp = temp.getParent(); } + matchingEntries.forEach( + entry -> { + if ( + deferredLazyReference + .getOriginalValue() + .getReferenceKey() + .equals(entry.getKey()) + ) { + Object val = entry.getValue(); + context.put(entry.getKey(), DeferredLazyReferenceSource.instance(val)); + entry.setValue(DeferredLazyReferenceSource.instance(val)); + } else { + entry.setValue(deferredLazyReference); + } + } + ); } } ); diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index f81feabca..09f40355d 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -5,12 +5,14 @@ import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.Context.Library; import com.hubspot.jinjava.interpret.DeferredLazyReference; +import com.hubspot.jinjava.interpret.DeferredLazyReferenceSource; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.DisabledException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; import com.hubspot.jinjava.interpret.LazyExpression; +import com.hubspot.jinjava.interpret.LazyReference; import com.hubspot.jinjava.interpret.RevertibleObject; import com.hubspot.jinjava.lib.fn.MacroFunction; import com.hubspot.jinjava.lib.fn.eager.EagerMacroFunction; @@ -153,8 +155,26 @@ public static EagerExecutionResult executeInChildContext( result = function.apply(interpreter); speculativeBindings = eagerChildContextConfig.discardSessionBindings - ? Collections.emptyMap() + ? new HashMap<>() : interpreter.getContext().getSessionBindings(); + // speculativeBindings.putAll( + // interpreter + // .getContext() + // .getScope() + // .entrySet() + // .stream() + // .filter( + // e -> + // e.getValue() instanceof DeferredLazyReferenceSource && + // !(((DeferredLazyReferenceSource) e.getValue()).isReconstructed()) + // ) + // .collect( + // Collectors.toMap( + // Entry::getKey, + // entry -> ((DeferredLazyReferenceSource) entry.getValue()).getOriginalValue() + // ) + // ) + // ); } if (eagerChildContextConfig.createReconstructedContext) { speculativeBindings = @@ -185,6 +205,33 @@ private static Map getBasicSpeculativeBindings( Set metaContextVariables, Map speculativeBindings ) { + speculativeBindings.putAll( + interpreter + .getContext() + .getScope() + .entrySet() + .stream() + .filter( + e -> + e.getValue() instanceof DeferredLazyReferenceSource && + !(((DeferredLazyReferenceSource) e.getValue()).isReconstructed()) + ) + .peek(e -> ((DeferredLazyReferenceSource) e.getValue()).setReconstructed(true)) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> ((DeferredLazyReferenceSource) entry.getValue()).getOriginalValue() + ) + ) + ); + // speculativeBindings.putAll( + // speculativeBindings + // .values() + // .stream() + // .filter(o -> o instanceof DeferredLazyReference) + // .map(o -> ((DeferredLazyReference) o).getOriginalValue()) + // .collect(Collectors.toMap(LazyReference::getReferenceKey, Function.identity())) + // ); return speculativeBindings .entrySet() .stream() @@ -478,7 +525,10 @@ private static String reconstructBlockSetVariablesBeforeDeferring( Set deferredWords, JinjavaInterpreter interpreter ) { - Set filteredDeferredWords = deferredWords; + Set filteredDeferredWords = deferredWords + .stream() + .map(w -> w.split("\\.", 2)[0]) + .collect(Collectors.toSet()); // get base prop if (interpreter.getContext().isDeferredExecutionMode()) { Context parent = interpreter.getContext().getParent(); while (parent.isDeferredExecutionMode()) { @@ -499,7 +549,6 @@ private static String reconstructBlockSetVariablesBeforeDeferring( filteredDeferredWords .stream() - .map(w -> w.split("\\.", 2)[0]) // get base prop .filter(w -> !metaContextVariables.contains(w)) .filter(w -> interpreter.getContext().get(w) instanceof PyishBlockSetSerializable) .forEach( @@ -508,7 +557,6 @@ private static String reconstructBlockSetVariablesBeforeDeferring( ); filteredDeferredWords .stream() - .map(w -> w.split("\\.", 2)[0]) // get base prop .filter( w -> { Object value = interpreter.getContext().get(w); @@ -551,26 +599,29 @@ private static String reconstructInlineSetVariablesBeforeDeferring( Set deferredWords, JinjavaInterpreter interpreter ) { + Set filteredDeferredWords = deferredWords + .stream() + .map(w -> w.split("\\.", 2)[0]) + .collect(Collectors.toSet()); // get base prop if (interpreter.getContext().isDeferredExecutionMode()) { Context parent = interpreter.getContext().getParent(); while (parent.isDeferredExecutionMode()) { parent = parent.getParent(); } final Context finalParent = parent; - deferredWords = - deferredWords + filteredDeferredWords = + filteredDeferredWords .stream() .filter(word -> interpreter.getContext().get(word) != finalParent.get(word)) .collect(Collectors.toSet()); } - if (deferredWords.isEmpty()) { + if (filteredDeferredWords.isEmpty()) { return ""; } Set metaContextVariables = interpreter.getContext().getMetaContextVariables(); Map deferredMap = new HashMap<>(); - deferredWords + filteredDeferredWords .stream() - .map(w -> w.split("\\.", 2)[0]) // get base prop .filter( w -> interpreter.getContext().containsKey(w) && @@ -583,7 +634,7 @@ private static String reconstructInlineSetVariablesBeforeDeferring( deferredMap.put(w, PyishObjectMapper.getAsPyishString(value)); } ); - deferredWords + filteredDeferredWords .stream() .map(w -> w.split("\\.", 2)[0]) // get base prop .filter(w -> (interpreter.getContext().get(w) instanceof DeferredLazyReference)) diff --git a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja b/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja index 3933f23f6..10b399693 100644 --- a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja +++ b/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja @@ -1,10 +1,10 @@ -{% set b_list = ['a'] %}{% do b_list.append(deferred ? 'b' : '') %} -{% macro c(c_list) %}{% do c_list.append(deferred ? 'c' : '') %} -C: {{ c_list }}.{% endmacro %}{{ c(b_list) }}{% do b_list.append(deferred ? 'b' : '') %} -B: {{ b_list }}.{% set a_list = b_list %}{% do a_list.append(deferred ? 'a' : '') %} -A: {% set a_list = b_list %}{{ a_list }}. ---- -{% set a_list = ['a'] %}{% for i in [0] %}{% set b_list = a_list %}{% do b_list.append('b') %}{% for __ignored__ in [0] %}{% set c_list = b_list %}{% do c_list.append(deferred ? 'c' : '') %} -C: {{ c_list }}.{% endfor %}{% do b_list.append(deferred ? 'b' : '') %} -B: {{ b_list }}.{% endfor %}{% do a_list.append(deferred ? 'a' : '') %} +{% set b_list = ['a'] %}{% set a_list = ['a'] %}{% set b_list = a_list %}{% do b_list.append(deferred ? 'b' : '') %} +{% set b_list = a_list %}{% set b_list = a_list %}{% macro c(c_list) %}{% do c_list.append(deferred ? 'c' : '') %} +C: {{ c_list }}.{% endmacro %}{% set b_list = a_list %}{% set b_list = a_list %}{{ c(b_list) }}{% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{% do b_list.append(deferred ? 'B' : '') %} +B: {% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{{ b_list }}.{% do a_list.append(deferred ? 'a' : '') %} A: {{ a_list }}. +--- +{% set a_list = ['a'] %}{% set b_list = a_list %}{% for i in [0] %}{% set b_list = a_list %}{% set b_list = a_list %}{% do b_list.append('b') %}{% for __ignored__ in [0] %}{% set c_list = b_list %}{% do c_list.append(deferred ? 'c' : '') %} +C: {{ c_list }}.{% endfor %}{% set b_list = a_list %}{% do b_list.append(deferred ? 'b' : '') %} +B: {% set b_list = a_list %}{{ b_list }}.{% endfor %}{% do a_list.append(deferred ? 'a' : '') %} +A: {{ a_list }}. \ No newline at end of file diff --git a/src/test/resources/eager/handles-higher-scope-reference-modification.jinja b/src/test/resources/eager/handles-higher-scope-reference-modification.jinja index 28a7615c3..f8b78d84f 100644 --- a/src/test/resources/eager/handles-higher-scope-reference-modification.jinja +++ b/src/test/resources/eager/handles-higher-scope-reference-modification.jinja @@ -1,5 +1,26 @@ {% set a_list = [] %} {%- do a_list.append('a') %} +{%- for i in range(1) %} +{%- set b_list = a_list %} +{%- do b_list.append(deferred) %} +{% endfor %} +{{ a_list }} +#} +--- +{% for k in range(1) %} +{% set a_list = [] %} +{% for i in range(1) %} +{% if deferred %} +{% set b_list = a_list %} +{% do b_list.append(1) %} +{% endif %} +{% endfor %} +{{ a_list }} +{% endfor %} + + +#}{% set a_list = [] %} +{%- do a_list.append('a') %} {%- macro c(c_list) %} {%- do c_list.append(deferred ? 'c' : '') %} C: {{ c_list }}. @@ -7,7 +28,7 @@ C: {{ c_list }}. {%- macro b(b_list) %} {%- do b_list.append(deferred ? 'b' : '') %} {{ c(b_list) }} -{%- do b_list.append(deferred ? 'b' : '') %} +{%- do b_list.append(deferred ? 'B' : '') %} B: {{ b_list }}. {%- endmacro %} {{ b(a_list) }} From 817ed3c57013b31abb3b19fd08ccc91f39302789 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 16 Dec 2022 20:58:02 -0500 Subject: [PATCH 100/637] Only use deferred reference logic if there are distinct keys pointing to the same object --- .../jinjava/util/DeferredValueUtils.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java b/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java index 281153af5..23416fd20 100644 --- a/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java @@ -179,22 +179,24 @@ public static Set findAndMarkDeferredProperties( // ); temp = temp.getParent(); } - matchingEntries.forEach( - entry -> { - if ( - deferredLazyReference - .getOriginalValue() - .getReferenceKey() - .equals(entry.getKey()) - ) { - Object val = entry.getValue(); - context.put(entry.getKey(), DeferredLazyReferenceSource.instance(val)); - entry.setValue(DeferredLazyReferenceSource.instance(val)); - } else { - entry.setValue(deferredLazyReference); + if (matchingEntries.size() > 1) { // at least one duplicate + matchingEntries.forEach( + entry -> { + if ( + deferredLazyReference + .getOriginalValue() + .getReferenceKey() + .equals(entry.getKey()) + ) { + Object val = entry.getValue(); + context.put(entry.getKey(), DeferredLazyReferenceSource.instance(val)); + entry.setValue(DeferredLazyReferenceSource.instance(val)); + } else { + entry.setValue(deferredLazyReference); + } } - } - ); + ); + } } } ); From 72e4bebd759dfb76e2729882d22e69b0e0ea5b05 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 19 Dec 2022 10:11:38 -0500 Subject: [PATCH 101/637] Begin refactoring to "reconstructDeferredReferences" method --- .../expression/EagerExpressionStrategy.java | 88 +---------------- .../jinjava/lib/tag/eager/EagerPrintTag.java | 95 +------------------ .../util/EagerReconstructionUtils.java | 87 ++++++++++++----- 3 files changed, 63 insertions(+), 207 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java index 0375546f8..50a1f0ca4 100644 --- a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java @@ -97,97 +97,11 @@ private String eagerResolveExpression( ) ); prefixToPreserveState.append( - EagerReconstructionUtils.buildSetTag( - interpreter - .getContext() - .getScope() - .entrySet() - .stream() - .filter( - entry -> - entry.getValue() instanceof DeferredLazyReferenceSource && - !((DeferredLazyReferenceSource) entry.getValue()).isReconstructed() - ) - .peek( - entry -> - ((DeferredLazyReferenceSource) entry.getValue()).setReconstructed(true) - ) - .collect( - Collectors.toMap( - Entry::getKey, - entry -> - PyishObjectMapper.getAsPyishString( - ((DeferredLazyReferenceSource) entry.getValue()).getOriginalValue() - ) - ) - ), - // eagerExecutionResult - // .getResult() - // .getDeferredWords() - // .stream() - // .map(w -> w.split("\\.", 2)[0]) - // .map(word -> interpreter.getContext().getScope().get(word)) - // .filter(value -> value instanceof DeferredLazyReference) - // .map(value -> (DeferredLazyReference) value) - // .map(DeferredLazyReference::getOriginalValue) - // .distinct() - // .filter(lazyReference -> lazyReference.) - //// .filter(lazyReference -> !(interpreter.getContext().get(lazyReference.getReferenceKey()) instanceof DeferredValue)) - // .peek(lazyReference -> interpreter.) - // .collect( - // Collectors.toMap( - // LazyReference::getReferenceKey, - // val -> PyishObjectMapper.getAsPyishString(val.get()) - // ) - // ), + EagerReconstructionUtils.reconstructDeferredReferences( interpreter, - false - ) - ); - prefixToPreserveState.append( - EagerReconstructionUtils.buildSetTag( eagerExecutionResult - .getResult() - .getDeferredWords() - .stream() - .map(w -> w.split("\\.", 2)[0]) - .map( - word -> - new AbstractMap.SimpleImmutableEntry<>( - word, - interpreter.getContext().get(word) - ) - ) - .filter(entry -> entry.getValue() instanceof DeferredLazyReference) - .collect( - Collectors.toMap( - Entry::getKey, - entry -> - PyishObjectMapper.getAsPyishString( - ((DeferredLazyReference) entry.getValue()).getOriginalValue() - ) - ) - ), - interpreter, - false ) ); - // prefixToPreserveState.append( - // EagerReconstructionUtils.reconstructFromContextBeforeDeferring( - // eagerExecutionResult - // .getResult() - // .getDeferredWords() - // .stream() - // .map(w -> w.split("\\.", 2)[0]) - // .map(word -> interpreter.getContext().get(word)) - // .filter(value -> value instanceof DeferredLazyReference) - // .map(value -> (DeferredLazyReference) value) - // .map(DeferredLazyReference::getOriginalValue) - // .map(LazyReference::getReferenceKey) - // .collect(Collectors.toSet()), - // interpreter - // ) - // ); // There is only a preserving prefix because it couldn't be entirely evaluated. return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( prefixToPreserveState.toString() + helpers, diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java index 51ce91b7a..3b558914d 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java @@ -1,21 +1,14 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.hubspot.jinjava.interpret.DeferredLazyReference; -import com.hubspot.jinjava.interpret.DeferredLazyReferenceSource; import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; -import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.LazyReference; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.tag.PrintTag; -import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.util.EagerExpressionResolver; import com.hubspot.jinjava.util.EagerReconstructionUtils; import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.LengthLimitingStringJoiner; -import java.util.AbstractMap; -import java.util.Map.Entry; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -127,97 +120,11 @@ public static String interpretExpression( ) ); prefixToPreserveState.append( - EagerReconstructionUtils.buildSetTag( - interpreter - .getContext() - .getScope() - .entrySet() - .stream() - .filter( - entry -> - entry.getValue() instanceof DeferredLazyReferenceSource && - !((DeferredLazyReferenceSource) entry.getValue()).isReconstructed() - ) - .peek( - entry -> - ((DeferredLazyReferenceSource) entry.getValue()).setReconstructed(true) - ) - .collect( - Collectors.toMap( - Entry::getKey, - entry -> - PyishObjectMapper.getAsPyishString( - ((DeferredLazyReferenceSource) entry.getValue()).getOriginalValue() - ) - ) - ), - // eagerExecutionResult - // .getResult() - // .getDeferredWords() - // .stream() - // .map(w -> w.split("\\.", 2)[0]) - // .map(word -> interpreter.getContext().getScope().get(word)) - // .filter(value -> value instanceof DeferredLazyReference) - // .map(value -> (DeferredLazyReference) value) - // .map(DeferredLazyReference::getOriginalValue) - // .distinct() - // .filter(lazyReference -> lazyReference.) - //// .filter(lazyReference -> !(interpreter.getContext().get(lazyReference.getReferenceKey()) instanceof DeferredValue)) - // .peek(lazyReference -> interpreter.) - // .collect( - // Collectors.toMap( - // LazyReference::getReferenceKey, - // val -> PyishObjectMapper.getAsPyishString(val.get()) - // ) - // ), + EagerReconstructionUtils.reconstructDeferredReferences( interpreter, - false - ) - ); - prefixToPreserveState.append( - EagerReconstructionUtils.buildSetTag( eagerExecutionResult - .getResult() - .getDeferredWords() - .stream() - .map(w -> w.split("\\.", 2)[0]) - .map( - word -> - new AbstractMap.SimpleImmutableEntry<>( - word, - interpreter.getContext().get(word) - ) - ) - .filter(entry -> entry.getValue() instanceof DeferredLazyReference) - .collect( - Collectors.toMap( - Entry::getKey, - entry -> - PyishObjectMapper.getAsPyishString( - ((DeferredLazyReference) entry.getValue()).getOriginalValue() - ) - ) - ), - interpreter, - false ) ); - // prefixToPreserveState.append( - // EagerReconstructionUtils.reconstructFromContextBeforeDeferring( - // eagerExecutionResult - // .getResult() - // .getDeferredWords() - // .stream() - // .map(w -> w.split("\\.", 2)[0]) - // .map(word -> interpreter.getContext().get(word)) - // .filter(value -> value instanceof DeferredLazyReference) - // .map(value -> (DeferredLazyReference) value) - // .map(DeferredLazyReference::getOriginalValue) - // .map(LazyReference::getReferenceKey) - // .collect(Collectors.toSet()), - // interpreter - // ) - // ); // Possible set tag in front of this one. return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( prefixToPreserveState.toString() + joiner.toString(), diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 09f40355d..b32d72494 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -157,24 +157,6 @@ public static EagerExecutionResult executeInChildContext( eagerChildContextConfig.discardSessionBindings ? new HashMap<>() : interpreter.getContext().getSessionBindings(); - // speculativeBindings.putAll( - // interpreter - // .getContext() - // .getScope() - // .entrySet() - // .stream() - // .filter( - // e -> - // e.getValue() instanceof DeferredLazyReferenceSource && - // !(((DeferredLazyReferenceSource) e.getValue()).isReconstructed()) - // ) - // .collect( - // Collectors.toMap( - // Entry::getKey, - // entry -> ((DeferredLazyReferenceSource) entry.getValue()).getOriginalValue() - // ) - // ) - // ); } if (eagerChildContextConfig.createReconstructedContext) { speculativeBindings = @@ -224,14 +206,6 @@ private static Map getBasicSpeculativeBindings( ) ) ); - // speculativeBindings.putAll( - // speculativeBindings - // .values() - // .stream() - // .filter(o -> o instanceof DeferredLazyReference) - // .map(o -> ((DeferredLazyReference) o).getOriginalValue()) - // .collect(Collectors.toMap(LazyReference::getReferenceKey, Function.identity())) - // ); return speculativeBindings .entrySet() .stream() @@ -908,6 +882,67 @@ public static Boolean isDeferredExecutionMode() { .orElse(false); } + public static String reconstructDeferredReferences( + JinjavaInterpreter interpreter, + EagerExecutionResult eagerExecutionResult + ) { + String extraStuff = + buildSetTag( + interpreter + .getContext() + .getScope() + .entrySet() + .stream() + .filter( + entry -> + entry.getValue() instanceof DeferredLazyReferenceSource && + !((DeferredLazyReferenceSource) entry.getValue()).isReconstructed() + ) + .peek( + entry -> + ((DeferredLazyReferenceSource) entry.getValue()).setReconstructed(true) + ) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> + PyishObjectMapper.getAsPyishString( + ((DeferredLazyReferenceSource) entry.getValue()).getOriginalValue() + ) + ) + ), + interpreter, + false + ) + + buildSetTag( + eagerExecutionResult + .getResult() + .getDeferredWords() + .stream() + .map(w -> w.split("\\.", 2)[0]) + .map( + word -> + new AbstractMap.SimpleImmutableEntry<>( + word, + interpreter.getContext().get(word) + ) + ) + .filter(entry -> entry.getValue() instanceof DeferredLazyReference) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> + PyishObjectMapper.getAsPyishString( + ((DeferredLazyReference) entry.getValue()).getOriginalValue() + ) + ) + ), + interpreter, + false + ); + return extraStuff; + } + public static class EagerChildContextConfig { private final boolean takeNewValue; From dd623c2e05ebbebe8138a681b6cfe0dfdfbfd92c Mon Sep 17 00:00:00 2001 From: Julia Uy Date: Mon, 19 Dec 2022 09:50:11 -0600 Subject: [PATCH 102/637] Update variable data types for calculation --- .../hubspot/jinjava/lib/filter/FileSizeFormatFilter.java | 8 ++++---- .../jinjava/lib/filter/FileSizeFormatFilterTest.java | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilter.java index 4e3eaf1c9..b5a89efef 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilter.java @@ -53,19 +53,19 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) } String[] sizes = binary ? BINARY_SIZES : DECIMAL_SIZES; - int unit = 1; + long unit = 1; String prefix = ""; for (int i = 0; i < sizes.length; i++) { - unit = (int) Math.pow(base, i + 2); + unit = (long) Math.pow(base, i + 2); prefix = sizes[i]; if (bytes < unit) { - return String.format("%.1f %s", (base * bytes / unit), prefix); + return String.format("%.1f %s", (double)(base * bytes / unit), prefix); } } - return String.format("%.1f %s", (base * bytes / unit), prefix); + return String.format("%.1f %s", (double)(base * bytes / unit), prefix); } private static final String[] BINARY_SIZES = { diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java index 1e9079641..fecbff82e 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java @@ -29,5 +29,9 @@ public void testFileSizeFormatFilter() { jinjava.render("{{3531836|filesizeformat(true)}}", new HashMap()) ) .isEqualTo("3.4 MiB"); + assertThat( + jinjava.render("{{1000000000|filesizeformat}}", new HashMap()) + ) + .isEqualTo("1.0 GB"); } } From 50b95cdbd0e06e8cc693ca6af08cbc1642367bdd Mon Sep 17 00:00:00 2001 From: Julia Uy Date: Mon, 19 Dec 2022 10:54:38 -0600 Subject: [PATCH 103/637] Run prettier on updated filesizeformat filter --- .../hubspot/jinjava/lib/filter/FileSizeFormatFilter.java | 4 ++-- .../jinjava/lib/filter/FileSizeFormatFilterTest.java | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilter.java index b5a89efef..80df3efe8 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilter.java @@ -61,11 +61,11 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) prefix = sizes[i]; if (bytes < unit) { - return String.format("%.1f %s", (double)(base * bytes / unit), prefix); + return String.format("%.1f %s", (double) (base * bytes / unit), prefix); } } - return String.format("%.1f %s", (double)(base * bytes / unit), prefix); + return String.format("%.1f %s", (double) (base * bytes / unit), prefix); } private static final String[] BINARY_SIZES = { diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java index fecbff82e..e2b3d5dbe 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java @@ -30,8 +30,13 @@ public void testFileSizeFormatFilter() { ) .isEqualTo("3.4 MiB"); assertThat( - jinjava.render("{{1000000000|filesizeformat}}", new HashMap()) - ) + jinjava.render("{{1000000000|filesizeformat}}", new HashMap()) + ) .isEqualTo("1.0 GB"); } + + @Test + public void testFileSizeFormat() { + jinjava.render("{{3309735582|filesizeformat}}", new HashMap()); + } } From 86ffdc68955a24daa581ba869defa6fd3292a6d5 Mon Sep 17 00:00:00 2001 From: Julia Uy Date: Mon, 19 Dec 2022 10:56:01 -0600 Subject: [PATCH 104/637] Remove extra test --- .../hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java index e2b3d5dbe..247149b67 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java @@ -34,9 +34,4 @@ public void testFileSizeFormatFilter() { ) .isEqualTo("1.0 GB"); } - - @Test - public void testFileSizeFormat() { - jinjava.render("{{3309735582|filesizeformat}}", new HashMap()); - } } From 9a912af026e807a3019b298b1700a9f78f97697d Mon Sep 17 00:00:00 2001 From: Libo Song Date: Mon, 19 Dec 2022 16:09:47 -0500 Subject: [PATCH 105/637] Call the entrySet() only once before eager render child scope. --- .../jinjava/util/EagerReconstructionUtils.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 54a21c0d3..0f724c3e7 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -97,10 +97,11 @@ public static EagerExecutionResult executeInChildContext( final Map initiallyResolvedHashes; final Map initiallyResolvedAsStrings; if (eagerChildContextConfig.checkForContextChanges) { + Set> entrySet = interpreter.getContext().entrySet(); initiallyResolvedHashes = - getInitiallyResolvedHashes(interpreter, metaContextVariables); + getInitiallyResolvedHashes(entrySet, metaContextVariables); initiallyResolvedAsStrings = - getInitiallyResolvedAsStrings(interpreter, initiallyResolvedHashes); + getInitiallyResolvedAsStrings(interpreter, entrySet, initiallyResolvedHashes); } else { initiallyResolvedHashes = Collections.emptyMap(); initiallyResolvedAsStrings = Collections.emptyMap(); @@ -186,6 +187,7 @@ private static Map getChangedValues( private static Map getInitiallyResolvedAsStrings( JinjavaInterpreter interpreter, + Set> entrySet, Map initiallyResolvedHashes ) { Map initiallyResolvedAsStrings = new HashMap<>(); @@ -205,9 +207,7 @@ private static Map getInitiallyResolvedAsStrings( ); } else { entryStream = - interpreter - .getContext() - .entrySet() + entrySet .stream() .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) .filter( @@ -258,10 +258,9 @@ private static void putInitiallyResolvedAsString( } private static Map getInitiallyResolvedHashes( - JinjavaInterpreter interpreter, + Set> entrySet, Set metaContextVariables ) { - Set> entrySet = interpreter.getContext().entrySet(); Map resolved = new HashMap<>(); for (Entry entry : entrySet) { if (metaContextVariables.contains(entry.getKey())) { From fc6b9ebc991d70489a2abc9f3f48cbc95fd044ff Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 20 Dec 2022 16:28:31 -0500 Subject: [PATCH 106/637] Update tests, including fixing scoping in import tags --- .../jinjava/lib/tag/eager/EagerImportTag.java | 117 ++++++++++++++---- .../util/EagerReconstructionUtils.java | 21 +++- .../java/com/hubspot/jinjava/EagerTest.java | 22 +++- .../jinjava/ExpectedTemplateInterpreter.java | 9 ++ .../lib/tag/eager/EagerImportTagTest.java | 10 +- .../util/EagerReconstructionUtilsTest.java | 29 +++-- ...meta-context-var-overriding.expected.jinja | 6 +- ...s-within-deferred-set-block.expected.jinja | 2 +- .../eager/defers-macro-in-for.expected.jinja | 2 +- ...e-append-in-deferred-if-tag.expected.jinja | 6 +- ...able-reference-modification.expected.jinja | 8 +- ...ndles-import-in-deferred-if.expected.jinja | 17 ++- .../eager/handles-import-in-deferred-if.jinja | 12 +- ...es-import-with-macros-in-deferred-if.jinja | 8 ++ ...andles-same-name-import-var.expected.jinja | 4 +- ...ariables-in-deferred-caller.expected.jinja | 2 +- .../eager/reverts-simple.expected.jinja | 4 +- 17 files changed, 203 insertions(+), 76 deletions(-) create mode 100644 src/test/resources/eager/handles-import-with-macros-in-deferred-if.jinja diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java index c78ba29a3..447bdb149 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java @@ -9,7 +9,6 @@ import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.fn.MacroFunction; -import com.hubspot.jinjava.lib.fn.eager.EagerMacroFunction; import com.hubspot.jinjava.lib.tag.ImportTag; import com.hubspot.jinjava.lib.tag.SetTag; import com.hubspot.jinjava.loader.RelativePathResolver; @@ -17,9 +16,7 @@ import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.tree.Node; import com.hubspot.jinjava.tree.parse.TagToken; -import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -122,32 +119,24 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter } else if (!Strings.isNullOrEmpty(currentImportAlias)) { // Since some values got deferred, output a DoTag that will load the currentImportAlias on the context. finalOutput = - ( - newPathSetter + - getSetTagForDeferredChildBindings( - interpreter, - currentImportAlias, - childBindings - ) + - EagerReconstructionUtils.buildSetTag( - ImmutableMap.of(currentImportAlias, "{}"), - interpreter, - true - ) + - output + - getDoTagToPreserve(interpreter, currentImportAlias) + - initialPathSetter + getFinalOutputWithAlias( + interpreter, + currentImportAlias, + initialPathSetter, + newPathSetter, + output, + childBindings ); } else { finalOutput = - newPathSetter + - getSetTagForDeferredChildBindings( + getFinalOutputWithoutAlias( interpreter, currentImportAlias, + initialPathSetter, + newPathSetter, + output, childBindings - ) + - output + - initialPathSetter; + ); } return EagerReconstructionUtils.buildBlockSetTag( SetTag.IGNORED_VARIABLE_NAME, @@ -168,6 +157,56 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter } } + private String getFinalOutputWithoutAlias( + JinjavaInterpreter interpreter, + String currentImportAlias, + String initialPathSetter, + String newPathSetter, + String output, + Map childBindings + ) { + return ( + newPathSetter + + getSetTagForDeferredChildBindings(interpreter, currentImportAlias, childBindings) + + output + + initialPathSetter + ); + } + + private String getFinalOutputWithAlias( + JinjavaInterpreter interpreter, + String currentImportAlias, + String initialPathSetter, + String newPathSetter, + String output, + Map childBindings + ) { + return ( + newPathSetter + + getSetTagForDeferredChildBindings(interpreter, currentImportAlias, childBindings) + + EagerReconstructionUtils.buildSetTag( + ImmutableMap.of(currentImportAlias, "{}"), + interpreter, + true + ) + + wrapInChildScopeIfNecessary(interpreter, output, currentImportAlias) + + initialPathSetter + ); + } + + private static String wrapInChildScopeIfNecessary( + JinjavaInterpreter interpreter, + String output, + String currentImportAlias + ) { + String combined = output + getDoTagToPreserve(interpreter, currentImportAlias); + // So that any set variables other than the alias won't exist outside the child's scope + if (interpreter.getContext().isDeferredExecutionMode()) { + return EagerReconstructionUtils.wrapInChildScope(combined, interpreter); + } + return combined; + } + private String getSetTagForDeferredChildBindings( JinjavaInterpreter interpreter, String currentImportAlias, @@ -323,7 +362,12 @@ private static void constructFullAliasPathMap( throw new InterpretException("Encountered a problem with import alias maps"); } } - currentMap.put(allAliases[allAliases.length - 1], new PyMap(new HashMap<>())); + currentMap.put( + allAliases[allAliases.length - 1], + child.getContext().isDeferredExecutionMode() + ? DeferredValue.instance(new PyMap(new HashMap<>())) + : new PyMap(new HashMap<>()) + ); } @SuppressWarnings("unchecked") @@ -352,7 +396,15 @@ private static Map getMapForCurrentContextAlias( return newMap; } else { Map newMap = new PyMap(new HashMap<>()); - child.getContext().getParent().put(currentImportAlias, newMap); + child + .getContext() + .getParent() + .put( + currentImportAlias, + child.getContext().isDeferredExecutionMode() + ? DeferredValue.instance(newMap) + : newMap + ); return newMap; } } @@ -392,6 +444,21 @@ public static void integrateChild( parent.getContext().putAll(childBindingsWithoutImportResourcePath); } } else { + if ( + child.getContext().isDeferredExecutionMode() && + child + .getContext() + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) + .collect(Collectors.toSet()) + .contains(currentImportAlias) + ) { + // since a child scope will be used, the import alias would not be properly reconstructed + throw new DeferredValueException( + "Same-named variable as import alias: " + currentImportAlias + ); + } childBindings.putAll(child.getContext().getGlobalMacros()); Map mapForCurrentContextAlias = getMapForCurrentContextAlias( currentImportAlias, diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index b32d72494..da99ed2cd 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -12,7 +12,6 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; import com.hubspot.jinjava.interpret.LazyExpression; -import com.hubspot.jinjava.interpret.LazyReference; import com.hubspot.jinjava.interpret.RevertibleObject; import com.hubspot.jinjava.lib.fn.MacroFunction; import com.hubspot.jinjava.lib.fn.eager.EagerMacroFunction; @@ -512,7 +511,15 @@ private static String reconstructBlockSetVariablesBeforeDeferring( filteredDeferredWords = deferredWords .stream() - .filter(word -> interpreter.getContext().get(word) != finalParent.get(word)) + .filter( + word -> { + Object parentValue = finalParent.get(word); + return ( + !(parentValue instanceof DeferredValue) && + interpreter.getContext().get(word) != finalParent.get(word) + ); + } + ) .collect(Collectors.toSet()); } if (filteredDeferredWords.isEmpty()) { @@ -586,7 +593,15 @@ private static String reconstructInlineSetVariablesBeforeDeferring( filteredDeferredWords = filteredDeferredWords .stream() - .filter(word -> interpreter.getContext().get(word) != finalParent.get(word)) + .filter( + word -> { + Object parentValue = finalParent.get(word); + return ( + !(parentValue instanceof DeferredValue) && + interpreter.getContext().get(word) != finalParent.get(word) + ); + } + ) .collect(Collectors.toSet()); } if (filteredDeferredWords.isEmpty()) { diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 4b788a29c..7bf321a2e 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -937,7 +937,7 @@ public void itHandlesDeferringLoopVariable() { @Test public void itDefersChangesWithinDeferredSetBlock() { - expectedTemplateInterpreter.assertExpectedOutput( + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( "defers-changes-within-deferred-set-block" ); } @@ -953,6 +953,16 @@ public void itDefersChangesWithinDeferredSetBlockSecondPass() { ); } + @Test + public void itHandlesImportWithMacrosInDeferredIf() { + String template = expectedTemplateInterpreter.getFixtureTemplate( + "handles-import-with-macros-in-deferred-if" + ); + JinjavaInterpreter.getCurrent().render(template); + assertThat(JinjavaInterpreter.getCurrent().getContext().getDeferredNodes()) + .isNotEmpty(); + } + @Test public void itHandlesImportInDeferredIf() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( @@ -993,9 +1003,13 @@ public void itHandlesDoubleImportModificationSecondPass() { @Test public void itHandlesSameNameImportVar() { - expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "handles-same-name-import-var" + String template = expectedTemplateInterpreter.getFixtureTemplate( + "handles-import-with-macros-in-deferred-if" ); + JinjavaInterpreter.getCurrent().render(template); + // No longer allows importing a file that uses the same alias as a variable declared in the import file + assertThat(JinjavaInterpreter.getCurrent().getContext().getDeferredNodes()) + .isNotEmpty(); } @Test @@ -1177,7 +1191,7 @@ public void itReconstructsBlockSetVariablesInForLoop() { @Test public void itReconstructsNullVariablesInDeferredCaller() { - expectedTemplateInterpreter.assertExpectedOutput( + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( "reconstructs-null-variables-in-deferred-caller" ); } diff --git a/src/test/java/com/hubspot/jinjava/ExpectedTemplateInterpreter.java b/src/test/java/com/hubspot/jinjava/ExpectedTemplateInterpreter.java index 88e631fd7..83daa4e1b 100644 --- a/src/test/java/com/hubspot/jinjava/ExpectedTemplateInterpreter.java +++ b/src/test/java/com/hubspot/jinjava/ExpectedTemplateInterpreter.java @@ -27,6 +27,9 @@ public ExpectedTemplateInterpreter( public String assertExpectedOutput(String name) { String template = getFixtureTemplate(name); String output = JinjavaInterpreter.getCurrent().render(template); + assertThat(JinjavaInterpreter.getCurrent().getContext().getDeferredNodes()) + .as("Ensure no deferred nodes were created") + .isEmpty(); assertThat(output.trim()).isEqualTo(expected(name).trim()); assertThat(JinjavaInterpreter.getCurrent().render(output).trim()) .isEqualTo(expected(name).trim()); @@ -36,6 +39,9 @@ public String assertExpectedOutput(String name) { public String assertExpectedOutputNonIdempotent(String name) { String template = getFixtureTemplate(name); String output = JinjavaInterpreter.getCurrent().render(template); + assertThat(JinjavaInterpreter.getCurrent().getContext().getDeferredNodes()) + .as("Ensure no deferred nodes were created") + .isEmpty(); assertThat(output.trim()).isEqualTo(expected(name).trim()); return output; } @@ -61,6 +67,9 @@ public String assertExpectedNonEagerOutput(String name) { preserveInterpreter.getContext().putAll(interpreter.getContext()); String template = getFixtureTemplate(name); String output = JinjavaInterpreter.getCurrent().render(template); + assertThat(JinjavaInterpreter.getCurrent().getContext().getDeferredNodes()) + .as("Ensure no deferred nodes were created") + .isEmpty(); assertThat(output.trim()).isEqualTo(expected(name).trim()); return output; } finally { 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 31321dd19..96bd42b81 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 @@ -462,11 +462,11 @@ public void itHandlesQuadLayerInDeferredIf() { ); assertThat(result) .isEqualTo( - "{% if deferred %}{% set __ignored__ %}{% set current_path = 'import-tree-b.jinja' %}{% set a,foo_b = {'foo_a': 'a', 'import_resource_path': 'import-tree-a.jinja', 'something': 'somn'} ,null %}{% set b = {} %}{% set __ignored__ %}{% set current_path = 'import-tree-a.jinja' %}{% set a = {} %}{% set something = 'somn' %}{% do a.update({'something': something}) %}\n" + + "{% if deferred %}{% set __ignored__ %}{% set current_path = 'import-tree-b.jinja' %}{% set a,foo_b = {'foo_a': 'a', 'import_resource_path': 'import-tree-a.jinja', 'something': 'somn'} ,null %}{% set b = {} %}{% for __ignored__ in [0] %}{% set __ignored__ %}{% set current_path = 'import-tree-a.jinja' %}{% set a = {} %}{% for __ignored__ in [0] %}{% set something = 'somn' %}{% do a.update({'something': something}) %}\n" + "{% set foo_a = 'a' %}{% do a.update({'foo_a': foo_a}) %}\n" + - "{% do a.update({'foo_a': 'a','import_resource_path': 'import-tree-a.jinja','something': 'somn'}) %}{% set current_path = 'import-tree-b.jinja' %}{% endset %}\n" + + "{% do a.update({'foo_a': 'a','import_resource_path': 'import-tree-a.jinja','something': 'somn'}) %}{% endfor %}{% set current_path = 'import-tree-b.jinja' %}{% endset %}\n" + "{% set foo_b = 'b' + a.foo_a %}{% do b.update({'foo_b': foo_b}) %}\n" + - "{% do b.update({'a': a,'foo_b': foo_b,'import_resource_path': 'import-tree-b.jinja'}) %}{% set current_path = '' %}{% endset %}{% endif %}" + "{% do b.update({'a': a,'foo_b': foo_b,'import_resource_path': 'import-tree-b.jinja'}) %}{% endfor %}{% set current_path = '' %}{% endset %}{% endif %}" ); removeDeferredContextKeys(); @@ -707,8 +707,8 @@ public void itDoesNotSilentlyOverrideVariable() { .isEqualTo( "a" + "{% set vars = {'foo': 'a', 'import_resource_path': 'var-a.jinja'} %}{% if deferred %}" + - "{% set __ignored__ %}{% set current_path = 'var-b.jinja' %}{% set vars = {} %}{% set foo = 'b' %}{% do vars.update({'foo': foo}) %}\n" + - "{% do vars.update({'foo': 'b','import_resource_path': 'var-b.jinja'}) %}{% set current_path = '' %}{% endset %}" + + "{% set __ignored__ %}{% set current_path = 'var-b.jinja' %}{% set vars = {} %}{% for __ignored__ in [0] %}{% set foo = 'b' %}{% do vars.update({'foo': foo}) %}\n" + + "{% do vars.update({'foo': 'b','import_resource_path': 'var-b.jinja'}) %}{% endfor %}{% set current_path = '' %}{% endset %}" + "{% endif %}" + "{{ vars.foo }}" ); diff --git a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java index e79e3b564..82b852e1a 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java @@ -11,6 +11,7 @@ import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.lib.fn.MacroFunction; import com.hubspot.jinjava.lib.tag.eager.DeferredToken; @@ -79,6 +80,7 @@ public void itExecutesInChildContextAndTakesNewValue() { .withTakeNewValue(true) .withForceDeferredExecutionMode(true) .withCheckForContextChanges(true) + .withCreateReconstructedContext(true) .build() ); @@ -108,6 +110,7 @@ public void itExecutesInChildContextAndDefersNewValue() { .newBuilder() .withForceDeferredExecutionMode(true) .withCheckForContextChanges(true) + .withCreateReconstructedContext(true) .build() ); assertThat(result.getResult().toString()).isEqualTo("function return"); @@ -163,12 +166,14 @@ public void itDoesntReconstructVariablesInDeferredExecutionMode() { Set deferredWords = new HashSet<>(); deferredWords.add("foo.append"); context.put("foo", new PyList(new ArrayList<>())); - context.setDeferredExecutionMode(true); - String result = EagerReconstructionUtils.reconstructFromContextBeforeDeferring( - deferredWords, - interpreter - ); - assertThat(result).isEqualTo(""); + try (InterpreterScopeClosable c = interpreter.enterScope()) { + interpreter.getContext().setDeferredExecutionMode(true); + String result = EagerReconstructionUtils.reconstructFromContextBeforeDeferring( + deferredWords, + interpreter + ); + assertThat(result).isEqualTo(""); + } } @Test @@ -373,7 +378,11 @@ public void itDiscardsSessionBindings() { return EagerExpressionResult.fromString(""); }, interpreter, - EagerChildContextConfig.newBuilder().withDiscardSessionBindings(false).build() + EagerChildContextConfig + .newBuilder() + .withDiscardSessionBindings(false) + .withCreateReconstructedContext(true) + .build() ); EagerExecutionResult withoutSessionBindings = EagerReconstructionUtils.executeInChildContext( eagerInterpreter -> { @@ -381,7 +390,11 @@ public void itDiscardsSessionBindings() { return EagerExpressionResult.fromString(""); }, interpreter, - EagerChildContextConfig.newBuilder().withDiscardSessionBindings(true).build() + EagerChildContextConfig + .newBuilder() + .withDiscardSessionBindings(true) + .withCreateReconstructedContext(true) + .build() ); assertThat(withSessionBindings.getSpeculativeBindings()) .containsEntry("foo", "foobar"); diff --git a/src/test/resources/eager/allows-meta-context-var-overriding.expected.jinja b/src/test/resources/eager/allows-meta-context-var-overriding.expected.jinja index 4fa876537..6052f7712 100644 --- a/src/test/resources/eager/allows-meta-context-var-overriding.expected.jinja +++ b/src/test/resources/eager/allows-meta-context-var-overriding.expected.jinja @@ -1,9 +1,9 @@ 0 META -{% for meta in deferred %} +{% set meta = 'META' %}{% for meta in deferred %} {{ meta }}{% endfor %} {{ meta }} {% set meta = [] %}{% if deferred %} -{% set meta = [1] %} +{% do meta.append(1) %} {% endif %} -{{ meta }} \ No newline at end of file +{{ meta }} diff --git a/src/test/resources/eager/defers-changes-within-deferred-set-block.expected.jinja b/src/test/resources/eager/defers-changes-within-deferred-set-block.expected.jinja index 8f42953dd..af01185bb 100644 --- a/src/test/resources/eager/defers-changes-within-deferred-set-block.expected.jinja +++ b/src/test/resources/eager/defers-changes-within-deferred-set-block.expected.jinja @@ -1,6 +1,6 @@ 1 {% set bar,foo = [1],'1' %}{% if deferred %} -{% set foo %}2{% set bar = [1, 2] %}{% endset %} +{% set bar = [1] %}{% set foo %}2{% do bar.append(2) %}{% endset %} {% endif %} Bar: {{ bar }} Foo: {{ foo }} diff --git a/src/test/resources/eager/defers-macro-in-for.expected.jinja b/src/test/resources/eager/defers-macro-in-for.expected.jinja index 16d63d7bc..93d3b72ac 100644 --- a/src/test/resources/eager/defers-macro-in-for.expected.jinja +++ b/src/test/resources/eager/defers-macro-in-for.expected.jinja @@ -1,3 +1,3 @@ -{% set my_list = [] %}{% macro macro_append(num) %}{% do my_list.append(num) %}{{ my_list }}{% endmacro %}{% for item in filter:split.filter(macro_append(deferred), ____int3rpr3t3r____, ',', 2) %} +{% set my_list = [] %}{% set my_list = [] %}{% macro macro_append(num) %}{% do my_list.append(num) %}{{ my_list }}{% endmacro %}{% for item in filter:split.filter(macro_append(deferred), ____int3rpr3t3r____, ',', 2) %} {{ item }} {% endfor %} diff --git a/src/test/resources/eager/doesnt-double-append-in-deferred-if-tag.expected.jinja b/src/test/resources/eager/doesnt-double-append-in-deferred-if-tag.expected.jinja index bc50799a0..3e9054c04 100644 --- a/src/test/resources/eager/doesnt-double-append-in-deferred-if-tag.expected.jinja +++ b/src/test/resources/eager/doesnt-double-append-in-deferred-if-tag.expected.jinja @@ -1,8 +1,8 @@ {% set foo = [0, 1] %}{% if deferred == true %} [0, 1] -{% set foo = [0, 1, 2] %} +{% do foo.append(2) %} {% else %} [0, 1] -{% set foo = [0, 1, 3] %} +{% do foo.append(3) %} {% endif %} -{{ foo }} +{{ foo }} \ No newline at end of file diff --git a/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja b/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja index 6ba812599..b99253750 100644 --- a/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja +++ b/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja @@ -1,9 +1,9 @@ -{% set some_list = [] %}{% set the_list = some_list %}{% if deferred %} -{% do some_list.append(deferred) %} +{% set the_list = [] %}{% if deferred %} +{% set the_list = [] %}{% set some_list = [] %}{% set the_list = [] %}{% set some_list = the_list %}{% do some_list.append(deferred) %} {% endif %} {{ the_list }} -{% set foo = [1] %}{% do foo.append(deferred) %} +{% set foo = [1] %}{% set foo = [1] %}{% do foo.append(deferred) %} {% do foo.append(2) %} -{% set bar = foo %}{{ foo ~ 'and' ~ bar }} +{% set bar = foo %}{% set bar = foo %}{{ foo ~ 'and' ~ bar }} diff --git a/src/test/resources/eager/handles-import-in-deferred-if.expected.jinja b/src/test/resources/eager/handles-import-in-deferred-if.expected.jinja index 1ead133c6..a2ed7392e 100644 --- a/src/test/resources/eager/handles-import-in-deferred-if.expected.jinja +++ b/src/test/resources/eager/handles-import-in-deferred-if.expected.jinja @@ -1,9 +1,8 @@ -{% if deferred %}{% set __ignored__ %}{% set current_path = 'macro-and-set.jinja' %}{% set simple = {} %} -{% set bar = 'person19' %}{% do simple.update({'bar': bar}) %} -Hello person -{% do simple.update({'bar': 'person19','import_resource_path': 'macro-and-set.jinja'}) %}{% set current_path = '' %}{% endset %}{% else %}{% set __ignored__ %}{% set current_path = 'macro-and-set.jinja' %}{% set simple = {} %} -{% set bar = 'person19' %}{% do simple.update({'bar': bar}) %} -Hello person -{% do simple.update({'bar': 'person19','import_resource_path': 'macro-and-set.jinja'}) %}{% set current_path = '' %}{% endset %}{% endif %} -simple.foo: {{ simple.foo() }} -simple.bar: {{ simple.bar }} +{% set primary_line_height = 100 %}{% if deferred %} +{% set __ignored__ %}{% set current_path = '../settag/set-val.jinja' %}{% set simple = {} %}{% for __ignored__ in [0] %}{% set primary_line_height = 42 %}{% do simple.update({'primary_line_height': primary_line_height}) %}{% do simple.update({'primary_line_height': 42,'import_resource_path': '../settag/set-val.jinja'}) %}{% endfor %}{% set current_path = '' %}{% endset %} +{% else %} +{% set __ignored__ %}{% set current_path = '../settag/set-val.jinja' %}{% set primary_line_height = 42 %}{% set current_path = '' %}{% endset %} +{% endif %} +simple.primary_line_height (deferred): {{ simple.primary_line_height }} +primary_line_height (deferred): {{ primary_line_height }} +secondary_line_height: 200 diff --git a/src/test/resources/eager/handles-import-in-deferred-if.jinja b/src/test/resources/eager/handles-import-in-deferred-if.jinja index 4bbe1af62..8ed6f36be 100644 --- a/src/test/resources/eager/handles-import-in-deferred-if.jinja +++ b/src/test/resources/eager/handles-import-in-deferred-if.jinja @@ -1,8 +1,10 @@ -{% set myname = 'person' %} +{% set primary_line_height = 100 %} +{% set secondary_line_height = 200 %} {% if deferred %} -{%- import "macro-and-set.jinja" as simple -%} +{% import "../settag/set-val.jinja" as simple %} {% else %} -{%- import "macro-and-set.jinja" as simple -%} +{% import "../settag/set-val.jinja" %} {% endif %} -simple.foo: {{ simple.foo() }} -simple.bar: {{ simple.bar }} +simple.primary_line_height (deferred): {{ simple.primary_line_height }} +primary_line_height (deferred): {{ primary_line_height }} +secondary_line_height: {{ secondary_line_height }} diff --git a/src/test/resources/eager/handles-import-with-macros-in-deferred-if.jinja b/src/test/resources/eager/handles-import-with-macros-in-deferred-if.jinja new file mode 100644 index 000000000..4bbe1af62 --- /dev/null +++ b/src/test/resources/eager/handles-import-with-macros-in-deferred-if.jinja @@ -0,0 +1,8 @@ +{% set myname = 'person' %} +{% if deferred %} +{%- import "macro-and-set.jinja" as simple -%} +{% else %} +{%- import "macro-and-set.jinja" as simple -%} +{% endif %} +simple.foo: {{ simple.foo() }} +simple.bar: {{ simple.bar }} diff --git a/src/test/resources/eager/handles-same-name-import-var.expected.jinja b/src/test/resources/eager/handles-same-name-import-var.expected.jinja index 2c9fe92e3..2149e6499 100644 --- a/src/test/resources/eager/handles-same-name-import-var.expected.jinja +++ b/src/test/resources/eager/handles-same-name-import-var.expected.jinja @@ -1,7 +1,7 @@ {% if deferred %} {% set __ignored__ %}{% set current_path = '../settag/set-var-and-deferred.jinja' %}{% set path,value = null,null %}{% set my_var = {} %}{% set my_var = {} %}{% if deferred %} -{% set __ignored__ %}{% set path = '../settag/set-var-and-deferred.jinja' %}{% do my_var.update({'path': path}) %}{% set value = null %}{% do my_var.update({'value': value}) %}{% set my_var = {} %}{% set my_var = {'foo': 'bar'} %}{% set my_var = {'my_var': {'foo': 'bar'} } %} -{% set value = deferred %}{% do my_var.update({'value': value}) %}{% do my_var.update({'value': value}) %} +{% set path,my_var = '',{} %}{% set __ignored__ %}{% set path = '../settag/set-var-and-deferred.jinja' %}{% set my_var = {} %}{% do my_var.update({'path': path}) %}{% set value = null %}{% do my_var.update({'value': value}) %}{% set my_var = {} %}{% set my_var = {'foo': 'bar'} %}{% set my_var = {'my_var': {'foo': 'bar'} } %} +{% set value = deferred %}{% set my_var = {'my_var': {'foo': 'bar'} } %}{% do my_var.update({'value': value}) %}{% do my_var.update({'value': value}) %} {% do my_var.update({'import_resource_path': '../settag/set-var-and-deferred.jinja', 'value': value}) %}{% set path = '' %}{% do my_var.update({'path': path}) %}{% endset %}{% do my_var.update({'__ignored__': __ignored__}) %} {{ my_var }} {% endif %} diff --git a/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller.expected.jinja b/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller.expected.jinja index 3301885ed..ee7445dbf 100644 --- a/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller.expected.jinja +++ b/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller.expected.jinja @@ -1,7 +1,7 @@ {% if deferred %} {% macro foo(var) %} {% set second_list = [] %} -{% do second_list.append(var) %} +{% set second_list = [] %}{% do second_list.append(var) %} {{ caller() }} {% endmacro %}{% call foo(deferred) %} [] diff --git a/src/test/resources/eager/reverts-simple.expected.jinja b/src/test/resources/eager/reverts-simple.expected.jinja index 946e1452d..fb1635add 100644 --- a/src/test/resources/eager/reverts-simple.expected.jinja +++ b/src/test/resources/eager/reverts-simple.expected.jinja @@ -1,9 +1,9 @@ {% for __ignored__ in [0] %} {% set my_list = [0, 1] %}{% if deferred %} - {% set my_list = [0, 1, 2] %} + {% do my_list.append(2) %} {% endif %} {% do my_list.append(3) %} {% do my_list.append(4) %} {{ my_list }} -{% endfor %} \ No newline at end of file +{% endfor %} From 66a18bec13e8ba92f9c47f24c8afde3370f18df7 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 20 Dec 2022 17:00:56 -0500 Subject: [PATCH 107/637] Add additional reference modification test --- .../expression/EagerExpressionStrategy.java | 5 ----- .../java/com/hubspot/jinjava/EagerTest.java | 7 +++++++ ...-higher-scope-reference-modification.jinja | 21 ------------------- ...ication-when-source-is-lost.expected.jinja | 14 +++++++++++++ ...nce-modification-when-source-is-lost.jinja | 18 ++++++++++++++++ 5 files changed, 39 insertions(+), 26 deletions(-) create mode 100644 src/test/resources/eager/handles-reference-modification-when-source-is-lost.expected.jinja create mode 100644 src/test/resources/eager/handles-reference-modification-when-source-is-lost.jinja diff --git a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java index 50a1f0ca4..01923feb3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java @@ -1,8 +1,6 @@ package com.hubspot.jinjava.lib.expression; import com.hubspot.jinjava.JinjavaConfig; -import com.hubspot.jinjava.interpret.DeferredLazyReference; -import com.hubspot.jinjava.interpret.DeferredLazyReferenceSource; import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; @@ -10,15 +8,12 @@ import com.hubspot.jinjava.lib.tag.RawTag; import com.hubspot.jinjava.lib.tag.eager.DeferredToken; import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; -import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.tree.output.RenderedOutputNode; import com.hubspot.jinjava.tree.parse.ExpressionToken; import com.hubspot.jinjava.util.EagerExpressionResolver; import com.hubspot.jinjava.util.EagerReconstructionUtils; import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.Logging; -import java.util.AbstractMap; -import java.util.Map.Entry; import java.util.Objects; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 7bf321a2e..4e8866b40 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1138,6 +1138,13 @@ public void itHandlesHigherScopeReferenceModificationSecondPass() { ); } + @Test + public void itHandlesReferenceModificationWhenSourceIsLost() { + expectedTemplateInterpreter.assertExpectedOutput( + "handles-reference-modification-when-source-is-lost" + ); + } + @Test public void itDoesNotReferentialDeferForSetVars() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( diff --git a/src/test/resources/eager/handles-higher-scope-reference-modification.jinja b/src/test/resources/eager/handles-higher-scope-reference-modification.jinja index f8b78d84f..7a3141908 100644 --- a/src/test/resources/eager/handles-higher-scope-reference-modification.jinja +++ b/src/test/resources/eager/handles-higher-scope-reference-modification.jinja @@ -1,26 +1,5 @@ {% set a_list = [] %} {%- do a_list.append('a') %} -{%- for i in range(1) %} -{%- set b_list = a_list %} -{%- do b_list.append(deferred) %} -{% endfor %} -{{ a_list }} -#} ---- -{% for k in range(1) %} -{% set a_list = [] %} -{% for i in range(1) %} -{% if deferred %} -{% set b_list = a_list %} -{% do b_list.append(1) %} -{% endif %} -{% endfor %} -{{ a_list }} -{% endfor %} - - -#}{% set a_list = [] %} -{%- do a_list.append('a') %} {%- macro c(c_list) %} {%- do c_list.append(deferred ? 'c' : '') %} C: {{ c_list }}. diff --git a/src/test/resources/eager/handles-reference-modification-when-source-is-lost.expected.jinja b/src/test/resources/eager/handles-reference-modification-when-source-is-lost.expected.jinja new file mode 100644 index 000000000..5d7fc15ac --- /dev/null +++ b/src/test/resources/eager/handles-reference-modification-when-source-is-lost.expected.jinja @@ -0,0 +1,14 @@ +{% set a_list = ['a'] %}{% for i in [0] %}{% set b_list = a_list %}{% do b_list.append(deferred) %} +{% endfor %} +{{ a_list }} +--- +{% for __ignored__ in [0] %} + +{% set a_list = [] %}{% for i in [0] %} +{% if deferred %} +{% set b_list = a_list %} +{% do b_list.append(1) %} +{% endif %} +{% endfor %} +{{ a_list }} +{% endfor %} diff --git a/src/test/resources/eager/handles-reference-modification-when-source-is-lost.jinja b/src/test/resources/eager/handles-reference-modification-when-source-is-lost.jinja new file mode 100644 index 000000000..53197b6d1 --- /dev/null +++ b/src/test/resources/eager/handles-reference-modification-when-source-is-lost.jinja @@ -0,0 +1,18 @@ +{% set a_list = [] %} +{%- do a_list.append('a') %} +{%- for i in range(1) %} +{%- set b_list = a_list %} +{%- do b_list.append(deferred) %} +{% endfor %} +{{ a_list }} +--- +{% for k in range(1) %} +{% set a_list = [] %} +{% for i in range(1) %} +{% if deferred %} +{% set b_list = a_list %} +{% do b_list.append(1) %} +{% endif %} +{% endfor %} +{{ a_list }} +{% endfor %} From e79b14ff7a2f41eab8eb826a6550ffc23cd99c0b Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 20 Dec 2022 17:04:24 -0500 Subject: [PATCH 108/637] Refactor createReconstructedContext into checkForContextChanges --- .../expression/EagerExpressionStrategy.java | 1 - .../jinjava/lib/tag/eager/EagerCallTag.java | 2 -- .../jinjava/lib/tag/eager/EagerCycleTag.java | 6 +----- .../jinjava/lib/tag/eager/EagerForTag.java | 12 ++--------- .../jinjava/lib/tag/eager/EagerIfTag.java | 2 -- .../tag/eager/EagerInlineSetTagStrategy.java | 6 +----- .../jinjava/lib/tag/eager/EagerPrintTag.java | 6 +----- .../lib/tag/eager/EagerStateChangingTag.java | 1 - .../lib/tag/eager/EagerTagDecorator.java | 1 - .../jinjava/util/DeferredValueUtils.java | 4 ---- .../util/EagerReconstructionUtils.java | 21 ++++--------------- .../lib/tag/eager/EagerForTagTest.java | 3 --- .../util/EagerReconstructionUtilsTest.java | 6 ++---- ...rence-modification.expected.expected.jinja | 8 +++---- ...cope-reference-modification.expected.jinja | 8 +++---- ...-higher-scope-reference-modification.jinja | 6 +++--- 16 files changed, 22 insertions(+), 71 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java index 01923feb3..98b94dc5d 100644 --- a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java @@ -44,7 +44,6 @@ private String eagerResolveExpression( .withPartialMacroEvaluation( interpreter.getConfig().isNestedInterpretationEnabled() ) - .withCheckForContextChanges(interpreter.getContext().isDeferredExecutionMode()) .build() ); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java index 242db372a..36839b1cc 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java @@ -62,7 +62,6 @@ public String eagerInterpret( .withPartialMacroEvaluation( interpreter.getConfig().isNestedInterpretationEnabled() ) - .withCheckForContextChanges(interpreter.getContext().isDeferredExecutionMode()) .build() ); StringBuilder prefixToPreserveState = new StringBuilder(); @@ -140,7 +139,6 @@ public String eagerInterpret( interpreter, EagerChildContextConfig .newBuilder() - .withCheckForContextChanges(true) .withForceDeferredExecutionMode(true) .build() ) 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 171960329..bf4769fc7 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 @@ -48,11 +48,7 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter eagerInterpreter -> EagerExpressionResolver.resolveExpression(expression, interpreter), interpreter, - EagerChildContextConfig - .newBuilder() - .withTakeNewValue(true) - .withCheckForContextChanges(interpreter.getContext().isDeferredExecutionMode()) - .build() + EagerChildContextConfig.newBuilder().withTakeNewValue(true).build() ); StringBuilder prefixToPreserveState = new StringBuilder(); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 0373ab985..a1c3e4725 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -45,11 +45,7 @@ public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { return expressionResult; }, interpreter, - EagerChildContextConfig - .newBuilder() - .withCheckForContextChanges(true) - .withCreateReconstructedContext(true) - .build() + EagerChildContextConfig.newBuilder().withCheckForContextChanges(true).build() ); if ( result.getResult().getResolutionState() == ResolutionState.NONE || @@ -164,11 +160,7 @@ private EagerExecutionResult runLoopOnce( ); }, interpreter, - EagerChildContextConfig - .newBuilder() - .withForceDeferredExecutionMode(true) - .withCheckForContextChanges(true) - .build() + EagerChildContextConfig.newBuilder().withForceDeferredExecutionMode(true).build() ); } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java index b4421bb91..7ab6eb082 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java @@ -64,7 +64,6 @@ public String eagerInterpret( EagerChildContextConfig .newBuilder() .withForceDeferredExecutionMode(true) - .withCheckForContextChanges(true) .build() ) .asTemplateString() @@ -118,7 +117,6 @@ public String eagerRenderBranches( EagerChildContextConfig .newBuilder() .withForceDeferredExecutionMode(true) - .withCheckForContextChanges(true) .build() ); sb.append(result.getResult()); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java index 1db49b82c..ec956b0c4 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java @@ -35,11 +35,7 @@ public EagerExecutionResult getEagerExecutionResult( eagerInterpreter -> EagerExpressionResolver.resolveExpression('[' + expression + ']', interpreter), interpreter, - EagerChildContextConfig - .newBuilder() - .withTakeNewValue(true) - .withCheckForContextChanges(interpreter.getContext().isDeferredExecutionMode()) - .build() + EagerChildContextConfig.newBuilder().withTakeNewValue(true).build() ); } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java index 3b558914d..5a0488219 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java @@ -56,11 +56,7 @@ public static String interpretExpression( EagerExecutionResult eagerExecutionResult = EagerReconstructionUtils.executeInChildContext( eagerInterpreter -> EagerExpressionResolver.resolveExpression(expr, interpreter), interpreter, - EagerChildContextConfig - .newBuilder() - .withTakeNewValue(true) - .withCheckForContextChanges(interpreter.getContext().isDeferredExecutionMode()) - .build() + EagerChildContextConfig.newBuilder().withTakeNewValue(true).build() ); StringBuilder prefixToPreserveState = new StringBuilder(); if (interpreter.getContext().isDeferredExecutionMode()) { diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java index 0862a2bd8..cfd113237 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java @@ -44,7 +44,6 @@ public String eagerInterpret( interpreter, EagerChildContextConfig .newBuilder() - .withCheckForContextChanges(true) .withForceDeferredExecutionMode(true) .build() ) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java index 18516e527..f5c8e67cd 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java @@ -114,7 +114,6 @@ public String eagerInterpret( EagerChildContextConfig .newBuilder() .withForceDeferredExecutionMode(true) - .withCheckForContextChanges(true) .build() ) .asTemplateString() diff --git a/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java b/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java index 23416fd20..66d4bdc4f 100644 --- a/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java @@ -164,7 +164,6 @@ public static Set findAndMarkDeferredProperties( .getScope() .entrySet() .stream() - // .filter(entry -> !entry.getKey().equals(word)) .filter(entry -> entry.getValue() == wordValue) .forEach( entry -> { @@ -174,9 +173,6 @@ public static Set findAndMarkDeferredProperties( .setReferenceKey(entry.getKey()); } ); - // .forEach( - // entry -> entry.setValue(DeferredLazyReference.instance(context, word)) - // ); temp = temp.getParent(); } if (matchingEntries.size() > 1) { // at least one duplicate diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index da99ed2cd..67e6d26c8 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -82,7 +82,6 @@ public static EagerExecutionResult executeInChildContext( EagerChildContextConfig .newBuilder() .withTakeNewValue(takeNewValue) - .withCheckForContextChanges(checkForContextChanges) .withForceDeferredExecutionMode(checkForContextChanges) .withPartialMacroEvaluation(partialMacroEvaluation) .build() @@ -98,7 +97,7 @@ public static EagerExecutionResult executeInChildContext( Set metaContextVariables = interpreter.getContext().getMetaContextVariables(); final Map initiallyResolvedHashes; final Map initiallyResolvedAsStrings; - if (eagerChildContextConfig.createReconstructedContext) { + if (eagerChildContextConfig.checkForContextChanges) { initiallyResolvedHashes = interpreter .getContext() @@ -157,7 +156,7 @@ public static EagerExecutionResult executeInChildContext( ? new HashMap<>() : interpreter.getContext().getSessionBindings(); } - if (eagerChildContextConfig.createReconstructedContext) { + if (eagerChildContextConfig.checkForContextChanges) { speculativeBindings = getAllSpeculativeBindings( interpreter, @@ -401,7 +400,7 @@ private static Object getObjectOrHashCode( if (o instanceof LazyExpression) { o = ((LazyExpression) o).get(); } - if (eagerChildContextConfig.createReconstructedContext) { + if (eagerChildContextConfig.checkForContextChanges) { if (o instanceof PyList && !((PyList) o).toList().contains(o)) { return o.hashCode(); } @@ -483,7 +482,6 @@ private static String reconstructMacroFunctionsBeforeDeferring( EagerChildContextConfig .newBuilder() .withForceDeferredExecutionMode(true) - .withCheckForContextChanges(true) .build() ) ) @@ -963,9 +961,8 @@ public static class EagerChildContextConfig { private final boolean discardSessionBindings; private final boolean partialMacroEvaluation; - private final boolean checkForContextChanges; - private final boolean createReconstructedContext; + private final boolean checkForContextChanges; private final boolean forceDeferredExecutionMode; private EagerChildContextConfig( @@ -973,14 +970,12 @@ private EagerChildContextConfig( boolean discardSessionBindings, boolean partialMacroEvaluation, boolean checkForContextChanges, - boolean createReconstructedContext, boolean forceDeferredExecutionMode ) { this.takeNewValue = takeNewValue; this.discardSessionBindings = discardSessionBindings; this.partialMacroEvaluation = partialMacroEvaluation; this.checkForContextChanges = checkForContextChanges; - this.createReconstructedContext = createReconstructedContext; this.forceDeferredExecutionMode = forceDeferredExecutionMode; } @@ -994,8 +989,6 @@ public static class Builder { private boolean discardSessionBindings; private boolean partialMacroEvaluation; private boolean checkForContextChanges; - - private boolean createReconstructedContext; private boolean forceDeferredExecutionMode; private Builder() {} @@ -1020,11 +1013,6 @@ public Builder withCheckForContextChanges(boolean checkForContextChanges) { return this; } - public Builder withCreateReconstructedContext(boolean createReconstructedContext) { - this.createReconstructedContext = createReconstructedContext; - return this; - } - public Builder withForceDeferredExecutionMode(boolean forceDeferredExecutionMode) { this.forceDeferredExecutionMode = forceDeferredExecutionMode; return this; @@ -1036,7 +1024,6 @@ public EagerChildContextConfig build() { discardSessionBindings, partialMacroEvaluation, checkForContextChanges, - createReconstructedContext, forceDeferredExecutionMode ); } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java index bba145982..a82d9e435 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java @@ -1,7 +1,6 @@ package com.hubspot.jinjava.lib.tag.eager; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.in; import com.google.common.collect.ImmutableList; import com.hubspot.jinjava.ExpectedNodeInterpreter; @@ -9,14 +8,12 @@ import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; import com.hubspot.jinjava.lib.tag.ForTagTest; import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.tree.parse.TagToken; import java.util.Optional; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; public class EagerForTagTest extends ForTagTest { diff --git a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java index 82b852e1a..172cdc68c 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java @@ -80,7 +80,6 @@ public void itExecutesInChildContextAndTakesNewValue() { .withTakeNewValue(true) .withForceDeferredExecutionMode(true) .withCheckForContextChanges(true) - .withCreateReconstructedContext(true) .build() ); @@ -110,7 +109,6 @@ public void itExecutesInChildContextAndDefersNewValue() { .newBuilder() .withForceDeferredExecutionMode(true) .withCheckForContextChanges(true) - .withCreateReconstructedContext(true) .build() ); assertThat(result.getResult().toString()).isEqualTo("function return"); @@ -381,7 +379,7 @@ public void itDiscardsSessionBindings() { EagerChildContextConfig .newBuilder() .withDiscardSessionBindings(false) - .withCreateReconstructedContext(true) + .withCheckForContextChanges(true) .build() ); EagerExecutionResult withoutSessionBindings = EagerReconstructionUtils.executeInChildContext( @@ -393,7 +391,7 @@ public void itDiscardsSessionBindings() { EagerChildContextConfig .newBuilder() .withDiscardSessionBindings(true) - .withCreateReconstructedContext(true) + .withCheckForContextChanges(true) .build() ); assertThat(withSessionBindings.getSpeculativeBindings()) diff --git a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.expected.jinja b/src/test/resources/eager/handles-higher-scope-reference-modification.expected.expected.jinja index 98f6f7bc2..b32d28d29 100644 --- a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.expected.jinja +++ b/src/test/resources/eager/handles-higher-scope-reference-modification.expected.expected.jinja @@ -1,8 +1,8 @@ C: ['a', 'b', 'c']. -B: ['a', 'b', 'c', 'b']. -A: ['a', 'b', 'c', 'b', 'a']. +B: ['a', 'b', 'c', 'B']. +A: ['a', 'b', 'c', 'B', 'A']. --- C: ['a', 'b', 'c']. -B: ['a', 'b', 'c', 'b']. -A: ['a', 'b', 'c', 'b', 'a']. +B: ['a', 'b', 'c', 'B']. +A: ['a', 'b', 'c', 'B', 'A']. diff --git a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja b/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja index 10b399693..cdfa41f49 100644 --- a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja +++ b/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja @@ -1,10 +1,10 @@ {% set b_list = ['a'] %}{% set a_list = ['a'] %}{% set b_list = a_list %}{% do b_list.append(deferred ? 'b' : '') %} {% set b_list = a_list %}{% set b_list = a_list %}{% macro c(c_list) %}{% do c_list.append(deferred ? 'c' : '') %} C: {{ c_list }}.{% endmacro %}{% set b_list = a_list %}{% set b_list = a_list %}{{ c(b_list) }}{% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{% do b_list.append(deferred ? 'B' : '') %} -B: {% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{{ b_list }}.{% do a_list.append(deferred ? 'a' : '') %} +B: {% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{{ b_list }}.{% do a_list.append(deferred ? 'A' : '') %} A: {{ a_list }}. --- {% set a_list = ['a'] %}{% set b_list = a_list %}{% for i in [0] %}{% set b_list = a_list %}{% set b_list = a_list %}{% do b_list.append('b') %}{% for __ignored__ in [0] %}{% set c_list = b_list %}{% do c_list.append(deferred ? 'c' : '') %} -C: {{ c_list }}.{% endfor %}{% set b_list = a_list %}{% do b_list.append(deferred ? 'b' : '') %} -B: {% set b_list = a_list %}{{ b_list }}.{% endfor %}{% do a_list.append(deferred ? 'a' : '') %} -A: {{ a_list }}. \ No newline at end of file +C: {{ c_list }}.{% endfor %}{% set b_list = a_list %}{% do b_list.append(deferred ? 'B' : '') %} +B: {% set b_list = a_list %}{{ b_list }}.{% endfor %}{% do a_list.append(deferred ? 'A' : '') %} +A: {{ a_list }}. diff --git a/src/test/resources/eager/handles-higher-scope-reference-modification.jinja b/src/test/resources/eager/handles-higher-scope-reference-modification.jinja index 7a3141908..aab73f5d1 100644 --- a/src/test/resources/eager/handles-higher-scope-reference-modification.jinja +++ b/src/test/resources/eager/handles-higher-scope-reference-modification.jinja @@ -11,7 +11,7 @@ C: {{ c_list }}. B: {{ b_list }}. {%- endmacro %} {{ b(a_list) }} -{%- do a_list.append(deferred ? 'a' : '') %} +{%- do a_list.append(deferred ? 'A' : '') %} A: {{ a_list }}. --- {% set a_list = [] %} @@ -24,8 +24,8 @@ A: {{ a_list }}. {%- do c_list.append(deferred ? 'c' : '') %} C: {{ c_list }}. {%- endfor %} -{%- do b_list.append(deferred ? 'b' : '') %} +{%- do b_list.append(deferred ? 'B' : '') %} B: {{ b_list }}. {%- endfor %} -{%- do a_list.append(deferred ? 'a' : '') %} +{%- do a_list.append(deferred ? 'A' : '') %} A: {{ a_list }}. From 97e8de20913d8ea537f41a5ad237c6685b23efd3 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 21 Dec 2022 11:35:05 -0500 Subject: [PATCH 109/637] Remove redundant parameter from getObjectOrHashCode --- .../util/EagerReconstructionUtils.java | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 67e6d26c8..1b9358f92 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -98,10 +98,9 @@ public static EagerExecutionResult executeInChildContext( final Map initiallyResolvedHashes; final Map initiallyResolvedAsStrings; if (eagerChildContextConfig.checkForContextChanges) { + Set> entrySet = interpreter.getContext().entrySet(); initiallyResolvedHashes = - interpreter - .getContext() - .entrySet() + entrySet .stream() .filter(e -> !metaContextVariables.contains(e.getKey())) .filter( @@ -111,7 +110,7 @@ public static EagerExecutionResult executeInChildContext( .collect( Collectors.toMap( Entry::getKey, - entry -> getObjectOrHashCode(entry.getValue(), eagerChildContextConfig) + entry -> getObjectOrHashCode(entry.getValue()) ) ); initiallyResolvedAsStrings = new HashMap<>(); @@ -285,7 +284,7 @@ private static Map getAllSpeculativeBindings( e -> !initiallyResolvedHashes .get(e.getKey()) - .equals(getObjectOrHashCode(e.getValue(), eagerChildContextConfig)) + .equals(getObjectOrHashCode(e.getValue())) ) .collect( Collectors.toMap( @@ -364,12 +363,7 @@ private static Object getOriginalValue( e.getValue() instanceof DeferredValue && initiallyResolvedHashes .get(e.getKey()) - .equals( - getObjectOrHashCode( - ((DeferredValue) e.getValue()).getOriginalValue(), - eagerChildContextConfig - ) - ) + .equals(getObjectOrHashCode(((DeferredValue) e.getValue()).getOriginalValue())) ) { return ((DeferredValue) e.getValue()).getOriginalValue(); } @@ -393,20 +387,15 @@ private static Object getOriginalValue( ); } - private static Object getObjectOrHashCode( - Object o, - EagerChildContextConfig eagerChildContextConfig - ) { + private static Object getObjectOrHashCode(Object o) { if (o instanceof LazyExpression) { o = ((LazyExpression) o).get(); } - if (eagerChildContextConfig.checkForContextChanges) { - if (o instanceof PyList && !((PyList) o).toList().contains(o)) { - return o.hashCode(); - } - if (o instanceof PyMap && !((PyMap) o).toMap().containsValue(o)) { - return o.hashCode() + ((PyMap) o).keySet().hashCode(); - } + if (o instanceof PyList && !((PyList) o).toList().contains(o)) { + return o.hashCode(); + } + if (o instanceof PyMap && !((PyMap) o).toMap().containsValue(o)) { + return o.hashCode() + ((PyMap) o).keySet().hashCode(); } return o; } From 7d52488e4a82be46245166ba92600eeb0902e4d7 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 21 Dec 2022 11:53:30 -0500 Subject: [PATCH 110/637] Simplify executeInChildContext with some refactoring --- .../util/EagerReconstructionUtils.java | 74 ++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index f82605d21..daf615399 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -93,37 +93,23 @@ public static EagerExecutionResult executeInChildContext( JinjavaInterpreter interpreter, EagerChildContextConfig eagerChildContextConfig ) { - EagerExpressionResult result; - Set metaContextVariables = interpreter.getContext().getMetaContextVariables(); - final Map initiallyResolvedHashes; - final Map initiallyResolvedAsStrings; - if (eagerChildContextConfig.checkForContextChanges) { - Set> entrySet = interpreter.getContext().entrySet(); - initiallyResolvedHashes = - getInitiallyResolvedHashes(entrySet, metaContextVariables); - initiallyResolvedAsStrings = - getInitiallyResolvedAsStrings(interpreter, entrySet, initiallyResolvedHashes); - } else { - initiallyResolvedHashes = Collections.emptyMap(); - initiallyResolvedAsStrings = Collections.emptyMap(); - } - - // Don't create new call stacks to prevent hitting max recursion with this silent new scope - Map speculativeBindings; - try (InterpreterScopeClosable c = interpreter.enterNonStackingScope()) { - if (eagerChildContextConfig.forceDeferredExecutionMode) { - interpreter.getContext().setDeferredExecutionMode(true); - } - interpreter - .getContext() - .setPartialMacroEvaluation(eagerChildContextConfig.partialMacroEvaluation); - result = function.apply(interpreter); - speculativeBindings = - eagerChildContextConfig.discardSessionBindings - ? new HashMap<>() - : interpreter.getContext().getSessionBindings(); - } + final Set metaContextVariables = interpreter + .getContext() + .getMetaContextVariables(); + final EagerExecutionResult initialResult; + final Map speculativeBindings; if (eagerChildContextConfig.checkForContextChanges) { + final Set> entrySet = interpreter.getContext().entrySet(); + final Map initiallyResolvedHashes = getInitiallyResolvedHashes( + entrySet, + metaContextVariables + ); + final Map initiallyResolvedAsStrings = getInitiallyResolvedAsStrings( + interpreter, + entrySet, + initiallyResolvedHashes + ); + initialResult = applyFunction(function, interpreter, eagerChildContextConfig); speculativeBindings = getAllSpeculativeBindings( interpreter, @@ -131,19 +117,41 @@ public static EagerExecutionResult executeInChildContext( metaContextVariables, initiallyResolvedHashes, initiallyResolvedAsStrings, - speculativeBindings + initialResult.getSpeculativeBindings() ); } else { + initialResult = applyFunction(function, interpreter, eagerChildContextConfig); speculativeBindings = getBasicSpeculativeBindings( interpreter, eagerChildContextConfig, metaContextVariables, - speculativeBindings + initialResult.getSpeculativeBindings() ); } + return new EagerExecutionResult(initialResult.getResult(), speculativeBindings); + } - return new EagerExecutionResult(result, speculativeBindings); + private static EagerExecutionResult applyFunction( + Function function, + JinjavaInterpreter interpreter, + EagerChildContextConfig eagerChildContextConfig + ) { + // Don't create new call stacks to prevent hitting max recursion with this silent new scope + try (InterpreterScopeClosable c = interpreter.enterNonStackingScope()) { + if (eagerChildContextConfig.forceDeferredExecutionMode) { + interpreter.getContext().setDeferredExecutionMode(true); + } + interpreter + .getContext() + .setPartialMacroEvaluation(eagerChildContextConfig.partialMacroEvaluation); + return new EagerExecutionResult( + function.apply(interpreter), + eagerChildContextConfig.discardSessionBindings + ? new HashMap<>() + : interpreter.getContext().getSessionBindings() + ); + } } private static Map getInitiallyResolvedAsStrings( From 860e92f40a21a46332d6b5f7503c9ae380599bb3 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 21 Dec 2022 14:19:37 -0500 Subject: [PATCH 111/637] Refactor executeInChildContext related logic to separate class --- .../expression/EagerExpressionStrategy.java | 8 +- .../tag/eager/EagerBlockSetTagStrategy.java | 9 +- .../jinjava/lib/tag/eager/EagerCallTag.java | 14 +- .../jinjava/lib/tag/eager/EagerCycleTag.java | 9 +- .../jinjava/lib/tag/eager/EagerForTag.java | 22 +- .../jinjava/lib/tag/eager/EagerIfTag.java | 14 +- .../tag/eager/EagerInlineSetTagStrategy.java | 9 +- .../jinjava/lib/tag/eager/EagerPrintTag.java | 9 +- .../lib/tag/eager/EagerStateChangingTag.java | 8 +- .../lib/tag/eager/EagerTagDecorator.java | 8 +- .../jinjava/util/EagerContextWatcher.java | 472 ++++++++++++++++++ .../util/EagerReconstructionUtils.java | 421 +--------------- .../util/EagerReconstructionUtilsTest.java | 25 +- 13 files changed, 558 insertions(+), 470 deletions(-) create mode 100644 src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java diff --git a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java index 98b94dc5d..bdd6e10e3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java @@ -10,9 +10,9 @@ import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; import com.hubspot.jinjava.tree.output.RenderedOutputNode; import com.hubspot.jinjava.tree.parse.ExpressionToken; +import com.hubspot.jinjava.util.EagerContextWatcher; import com.hubspot.jinjava.util.EagerExpressionResolver; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.Logging; import java.util.Objects; import java.util.stream.Collectors; @@ -34,12 +34,12 @@ private String eagerResolveExpression( JinjavaInterpreter interpreter ) { interpreter.getContext().checkNumberOfDeferredTokens(); - EagerExecutionResult eagerExecutionResult = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult eagerExecutionResult = EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResolver.resolveExpression(master.getExpr(), interpreter), interpreter, - EagerChildContextConfig - .newBuilder() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() .withTakeNewValue(true) .withPartialMacroEvaluation( interpreter.getConfig().isNestedInterpretationEnabled() diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java index f04771b12..3c480a55b 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java @@ -7,10 +7,10 @@ import com.hubspot.jinjava.tree.Node; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; +import com.hubspot.jinjava.util.EagerContextWatcher; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult.ResolutionState; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.LengthLimitingStringJoiner; import java.util.Collections; import java.util.Optional; @@ -31,7 +31,7 @@ protected EagerExecutionResult getEagerExecutionResult( String expression, JinjavaInterpreter interpreter ) { - EagerExecutionResult result = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult result = EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromSupplier( () -> { @@ -44,7 +44,10 @@ protected EagerExecutionResult getEagerExecutionResult( eagerInterpreter ), interpreter, - EagerChildContextConfig.newBuilder().withTakeNewValue(true).build() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() + .withTakeNewValue(true) + .build() ); if (result.getResult().getResolutionState() == ResolutionState.NONE) { throw new DeferredValueException(result.getResult().toString()); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java index 36839b1cc..6449c7180 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java @@ -12,10 +12,10 @@ import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.ExpressionToken; import com.hubspot.jinjava.tree.parse.TagToken; +import com.hubspot.jinjava.util.EagerContextWatcher; import com.hubspot.jinjava.util.EagerExpressionResolver; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.LengthLimitingStringJoiner; import java.util.LinkedHashMap; import java.util.stream.Collectors; @@ -49,15 +49,15 @@ public String eagerInterpret( interpreter.getPosition() ); interpreter.getContext().addGlobalMacro(caller); - EagerExecutionResult eagerExecutionResult = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult eagerExecutionResult = EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResolver.resolveExpression( tagNode.getHelpers().trim(), interpreter ), interpreter, - EagerChildContextConfig - .newBuilder() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() .withTakeNewValue(true) .withPartialMacroEvaluation( interpreter.getConfig().isNestedInterpretationEnabled() @@ -130,15 +130,15 @@ public String eagerInterpret( interpreter.getContext().setDynamicVariableResolver(s -> DeferredValue.instance()); if (!tagNode.getChildren().isEmpty()) { result.append( - EagerReconstructionUtils + EagerContextWatcher .executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromString( renderChildren(tagNode, eagerInterpreter) ), interpreter, - EagerChildContextConfig - .newBuilder() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() .withForceDeferredExecutionMode(true) .build() ) 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 bf4769fc7..7cff8aab3 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 @@ -6,9 +6,9 @@ import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.tag.CycleTag; import com.hubspot.jinjava.tree.parse.TagToken; +import com.hubspot.jinjava.util.EagerContextWatcher; import com.hubspot.jinjava.util.EagerExpressionResolver; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.HelperStringTokenizer; import com.hubspot.jinjava.util.WhitespaceUtils; import java.util.ArrayList; @@ -44,11 +44,14 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter helper.add(sb.toString()); } String expression = '[' + helper.get(0) + ']'; - EagerExecutionResult eagerExecutionResult = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult eagerExecutionResult = EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResolver.resolveExpression(expression, interpreter), interpreter, - EagerChildContextConfig.newBuilder().withTakeNewValue(true).build() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() + .withTakeNewValue(true) + .build() ); StringBuilder prefixToPreserveState = new StringBuilder(); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index a1c3e4725..6dd0a9dd2 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -9,11 +9,11 @@ import com.hubspot.jinjava.lib.tag.ForTag; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; +import com.hubspot.jinjava.util.EagerContextWatcher; import com.hubspot.jinjava.util.EagerExpressionResolver; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult.ResolutionState; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.LengthLimitingStringBuilder; import com.hubspot.jinjava.util.LengthLimitingStringJoiner; import java.util.HashSet; @@ -35,7 +35,7 @@ public EagerForTag(ForTag forTag) { @Override public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { Set addedTokens = new HashSet<>(); - EagerExecutionResult result = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult result = EagerContextWatcher.executeInChildContext( eagerInterpreter -> { EagerExpressionResult expressionResult = EagerExpressionResult.fromSupplier( () -> getTag().interpretUnchecked(tagNode, eagerInterpreter), @@ -45,7 +45,10 @@ public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { return expressionResult; }, interpreter, - EagerChildContextConfig.newBuilder().withCheckForContextChanges(true).build() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() + .withCheckForContextChanges(!interpreter.getContext().isDeferredExecutionMode()) + .build() ); if ( result.getResult().getResolutionState() == ResolutionState.NONE || @@ -80,7 +83,7 @@ public String eagerInterpret( ) { if ( e instanceof DeferredValueException && - e.getMessage().startsWith(EagerReconstructionUtils.CANNOT_RECONSTRUCT_MESSAGE) + e.getMessage().startsWith(EagerContextWatcher.CANNOT_RECONSTRUCT_MESSAGE) ) { throw e; } @@ -100,7 +103,7 @@ public String eagerInterpret( // separate getEagerImage from renderChildren because the token gets evaluated once // while the children are evaluated 0...n times. result.append( - EagerReconstructionUtils + EagerContextWatcher .executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromString( @@ -115,7 +118,7 @@ public String eagerInterpret( ) ), interpreter, - EagerChildContextConfig.newBuilder().build() + EagerContextWatcher.EagerChildContextConfig.newBuilder().build() ) .asTemplateString() ); @@ -150,7 +153,7 @@ private EagerExecutionResult runLoopOnce( TagNode tagNode, JinjavaInterpreter interpreter ) { - return EagerReconstructionUtils.executeInChildContext( + return EagerContextWatcher.executeInChildContext( eagerInterpreter -> { if (!(eagerInterpreter.getContext().get("loop") instanceof DeferredValue)) { eagerInterpreter.getContext().put("loop", DeferredValue.instance()); @@ -160,7 +163,10 @@ private EagerExecutionResult runLoopOnce( ); }, interpreter, - EagerChildContextConfig.newBuilder().withForceDeferredExecutionMode(true).build() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() + .withForceDeferredExecutionMode(true) + .build() ); } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java index 7ab6eb082..d620abd9f 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java @@ -11,9 +11,9 @@ import com.hubspot.jinjava.tree.Node; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.NoteToken; +import com.hubspot.jinjava.util.EagerContextWatcher; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.LengthLimitingStringBuilder; import java.util.HashSet; import java.util.Set; @@ -54,15 +54,15 @@ public String eagerInterpret( ); result.append( - EagerReconstructionUtils + EagerContextWatcher .executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromString( eagerRenderBranches(tagNode, eagerInterpreter, e) ), interpreter, - EagerChildContextConfig - .newBuilder() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() .withForceDeferredExecutionMode(true) .build() ) @@ -108,14 +108,14 @@ public String eagerRenderBranches( int branchEnd = findNextElseToken(tagNode, branchStart); if (!definitelyDrop) { int finalBranchStart = branchStart; - EagerExecutionResult result = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult result = EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromString( evaluateBranch(tagNode, finalBranchStart, branchEnd, interpreter) ), interpreter, - EagerChildContextConfig - .newBuilder() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() .withForceDeferredExecutionMode(true) .build() ); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java index ec956b0c4..fc8b5b7da 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java @@ -6,9 +6,9 @@ import com.hubspot.jinjava.lib.tag.SetTag; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; +import com.hubspot.jinjava.util.EagerContextWatcher; import com.hubspot.jinjava.util.EagerExpressionResolver; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.LengthLimitingStringJoiner; import com.hubspot.jinjava.util.WhitespaceUtils; import java.util.Arrays; @@ -31,11 +31,14 @@ public EagerExecutionResult getEagerExecutionResult( String expression, JinjavaInterpreter interpreter ) { - return EagerReconstructionUtils.executeInChildContext( + return EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResolver.resolveExpression('[' + expression + ']', interpreter), interpreter, - EagerChildContextConfig.newBuilder().withTakeNewValue(true).build() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() + .withTakeNewValue(true) + .build() ); } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java index 5a0488219..f8b010da4 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java @@ -5,9 +5,9 @@ import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.tag.PrintTag; import com.hubspot.jinjava.tree.parse.TagToken; +import com.hubspot.jinjava.util.EagerContextWatcher; import com.hubspot.jinjava.util.EagerExpressionResolver; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.LengthLimitingStringJoiner; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -53,10 +53,13 @@ public static String interpretExpression( JinjavaInterpreter interpreter, boolean includeExpressionResult ) { - EagerExecutionResult eagerExecutionResult = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult eagerExecutionResult = EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResolver.resolveExpression(expr, interpreter), interpreter, - EagerChildContextConfig.newBuilder().withTakeNewValue(true).build() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() + .withTakeNewValue(true) + .build() ); StringBuilder prefixToPreserveState = new StringBuilder(); if (interpreter.getContext().isDeferredExecutionMode()) { diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java index cfd113237..0dd86c3ab 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java @@ -6,9 +6,9 @@ import com.hubspot.jinjava.lib.tag.Tag; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; +import com.hubspot.jinjava.util.EagerContextWatcher; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import org.apache.commons.lang3.StringUtils; public class EagerStateChangingTag extends EagerTagDecorator { @@ -37,13 +37,13 @@ public String eagerInterpret( if (!tagNode.getChildren().isEmpty()) { result.append( - EagerReconstructionUtils + EagerContextWatcher .executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromString(renderChildren(tagNode, eagerInterpreter)), interpreter, - EagerChildContextConfig - .newBuilder() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() .withForceDeferredExecutionMode(true) .build() ) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java index f5c8e67cd..92455b72a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java @@ -12,10 +12,10 @@ import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.tree.parse.Token; +import com.hubspot.jinjava.util.EagerContextWatcher; import com.hubspot.jinjava.util.EagerExpressionResolver; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.LengthLimitingStringBuilder; import com.hubspot.jinjava.util.LengthLimitingStringJoiner; import java.util.stream.Collectors; @@ -95,7 +95,7 @@ public String eagerInterpret( interpreter.getConfig().getMaxOutputSize() ); result.append( - EagerReconstructionUtils + EagerContextWatcher .executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromString( @@ -111,8 +111,8 @@ public String eagerInterpret( renderChildren(tagNode, eagerInterpreter) ), interpreter, - EagerChildContextConfig - .newBuilder() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() .withForceDeferredExecutionMode(true) .build() ) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java new file mode 100644 index 000000000..c69ef21ba --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java @@ -0,0 +1,472 @@ +package com.hubspot.jinjava.util; + +import com.hubspot.jinjava.interpret.DeferredLazyReferenceSource; +import com.hubspot.jinjava.interpret.DeferredValue; +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; +import com.hubspot.jinjava.interpret.LazyExpression; +import com.hubspot.jinjava.interpret.RevertibleObject; +import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; +import com.hubspot.jinjava.objects.collections.PyList; +import com.hubspot.jinjava.objects.collections.PyMap; +import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; +import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class EagerContextWatcher { + public static final String CANNOT_RECONSTRUCT_MESSAGE = "Cannot reconstruct value"; + + /** + * Execute the specified functions within a protected context. + * Additionally, if the execution causes existing values on the context to become + * deferred, then their previous values will wrapped in a set + * tag that gets prepended to the returned result. + * The function is run in deferredExecutionMode=true, where the context needs to + * be protected from having values updated or set, + * such as when evaluating both the positive and negative nodes in an if statement. + * @param function Function to run within a "protected" child context + * @param interpreter JinjavaInterpreter to create a child from. + * @param eagerChildContextConfig Configuration for evaluation as defined in {@link EagerChildContextConfig} + * @return An EagerExecutionResult where: + * result is the string result of function. + * prefixToPreserveState is either blank or a set tag + * that preserves the state within the output for a second rendering pass. + */ + public static EagerExecutionResult executeInChildContext( + Function function, + JinjavaInterpreter interpreter, + EagerChildContextConfig eagerChildContextConfig + ) { + final Set metaContextVariables = interpreter + .getContext() + .getMetaContextVariables(); + final EagerExecutionResult initialResult; + final Map speculativeBindings; + if (eagerChildContextConfig.checkForContextChanges) { + final Set> entrySet = interpreter.getContext().entrySet(); + final Map initiallyResolvedHashes = getInitiallyResolvedHashes( + entrySet, + metaContextVariables + ); + final Map initiallyResolvedAsStrings = getInitiallyResolvedAsStrings( + interpreter, + entrySet, + initiallyResolvedHashes + ); + initialResult = applyFunction(function, interpreter, eagerChildContextConfig); + speculativeBindings = + getAllSpeculativeBindings( + interpreter, + eagerChildContextConfig, + metaContextVariables, + initiallyResolvedHashes, + initiallyResolvedAsStrings, + initialResult.getSpeculativeBindings() + ); + } else { + initialResult = applyFunction(function, interpreter, eagerChildContextConfig); + speculativeBindings = + getBasicSpeculativeBindings( + interpreter, + eagerChildContextConfig, + metaContextVariables, + initialResult.getSpeculativeBindings() + ); + } + return new EagerExecutionResult(initialResult.getResult(), speculativeBindings); + } + + private static EagerExecutionResult applyFunction( + Function function, + JinjavaInterpreter interpreter, + EagerChildContextConfig eagerChildContextConfig + ) { + // Don't create new call stacks to prevent hitting max recursion with this silent new scope + try (InterpreterScopeClosable c = interpreter.enterNonStackingScope()) { + if (eagerChildContextConfig.forceDeferredExecutionMode) { + interpreter.getContext().setDeferredExecutionMode(true); + } + interpreter + .getContext() + .setPartialMacroEvaluation(eagerChildContextConfig.partialMacroEvaluation); + return new EagerExecutionResult( + function.apply(interpreter), + eagerChildContextConfig.discardSessionBindings + ? new HashMap<>() + : interpreter.getContext().getSessionBindings() + ); + } + } + + private static Map getInitiallyResolvedAsStrings( + JinjavaInterpreter interpreter, + Set> entrySet, + Map initiallyResolvedHashes + ) { + Map initiallyResolvedAsStrings = new HashMap<>(); + // This creates a stringified snapshot of the context + // so it can be disabled via the config because it may cause performance issues. + Stream> entryStream = + ( + interpreter.getConfig().getExecutionMode().useEagerContextReverting() + ? entrySet + : interpreter.getContext().getCombinedScope().entrySet() + ).stream() + .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) + .filter( + entry -> EagerExpressionResolver.isResolvableObject(entry.getValue(), 4, 400) // TODO make this configurable + ); + entryStream.forEach( + entry -> + cacheRevertibleObject( + interpreter, + initiallyResolvedHashes, + initiallyResolvedAsStrings, + entry + ) + ); + return initiallyResolvedAsStrings; + } + + private static Map getInitiallyResolvedHashes( + Set> entrySet, + Set metaContextVariables + ) { + return entrySet + .stream() + .filter(entry -> !metaContextVariables.contains(entry.getKey())) + .filter( + entry -> !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null + ) + .collect( + Collectors.toMap(Entry::getKey, entry -> getObjectOrHashCode(entry.getValue())) + ); + } + + private static Map getBasicSpeculativeBindings( + JinjavaInterpreter interpreter, + EagerChildContextConfig eagerChildContextConfig, + Set metaContextVariables, + Map speculativeBindings + ) { + speculativeBindings.putAll( + interpreter + .getContext() + .getScope() + .entrySet() + .stream() + .filter( + entry -> + entry.getValue() instanceof DeferredLazyReferenceSource && + !(((DeferredLazyReferenceSource) entry.getValue()).isReconstructed()) + ) + .peek( + entry -> ((DeferredLazyReferenceSource) entry.getValue()).setReconstructed(true) + ) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> ((DeferredLazyReferenceSource) entry.getValue()).getOriginalValue() + ) + ) + ); + return speculativeBindings + .entrySet() + .stream() + .filter(entry -> !metaContextVariables.contains(entry.getKey())) + .filter(entry -> !"loop".equals(entry.getKey())) + .map( + entry -> { + if ( + eagerChildContextConfig.takeNewValue && + !(entry.getValue() instanceof DeferredValue) && + entry.getValue() != null + ) { + return entry; + } + Object contextValue = interpreter.getContext().get(entry.getKey()); + if ( + contextValue instanceof DeferredValue && + ((DeferredValue) contextValue).getOriginalValue() != null + ) { + if ( + !eagerChildContextConfig.takeNewValue && + !EagerExpressionResolver.isResolvableObject( + ((DeferredValue) contextValue).getOriginalValue() + ) + ) { + throw new DeferredValueException( + String.format("%s: %s", CANNOT_RECONSTRUCT_MESSAGE, entry.getKey()) + ); + } + return new AbstractMap.SimpleImmutableEntry<>( + entry.getKey(), + ((DeferredValue) contextValue).getOriginalValue() + ); + } + return null; + } + ) + .filter(Objects::nonNull) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> + entry.getValue() instanceof DeferredValue + ? ((DeferredValue) entry.getValue()).getOriginalValue() + : entry.getValue() + ) + ); + } + + private static Map getAllSpeculativeBindings( + JinjavaInterpreter interpreter, + EagerChildContextConfig eagerChildContextConfig, + Set metaContextVariables, + Map initiallyResolvedHashes, + Map initiallyResolvedAsStrings, + Map speculativeBindings + ) { + speculativeBindings = + speculativeBindings + .entrySet() + .stream() + .filter( + entry -> + entry.getValue() != null && + !entry.getValue().equals(interpreter.getContext().get(entry.getKey())) + ) + .filter( + entry -> + !(interpreter.getContext().get(entry.getKey()) instanceof DeferredValue) + ) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + speculativeBindings.putAll( + interpreter + .getContext() + .entrySet() + .stream() + .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) + .filter( + entry -> + !initiallyResolvedHashes + .get(entry.getKey()) + .equals(getObjectOrHashCode(entry.getValue())) + ) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> + getOriginalValue( + interpreter, + eagerChildContextConfig, + initiallyResolvedHashes, + initiallyResolvedAsStrings, + entry + ) + ) + ) + ); + + speculativeBindings = + speculativeBindings + .entrySet() + .stream() + .filter(entry -> !metaContextVariables.contains(entry.getKey())) + .filter( + entry -> + !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null + ) // these are already set recursively + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + return speculativeBindings; + } + + private static void cacheRevertibleObject( + JinjavaInterpreter interpreter, + Map initiallyResolvedHashes, + Map initiallyResolvedAsStrings, + Entry entry + ) { + RevertibleObject revertibleObject = interpreter + .getRevertibleObjects() + .get(entry.getKey()); + Object hashCode = initiallyResolvedHashes.get(entry.getKey()); + try { + if (revertibleObject == null || !hashCode.equals(revertibleObject.getHashCode())) { + revertibleObject = + new RevertibleObject( + hashCode, + PyishObjectMapper.getAsPyishStringOrThrow(entry.getValue()) + ); + interpreter.getRevertibleObjects().put(entry.getKey(), revertibleObject); + } + revertibleObject + .getPyishString() + .ifPresent( + pyishString -> initiallyResolvedAsStrings.put(entry.getKey(), pyishString) + ); + } catch (Exception e) { + interpreter + .getRevertibleObjects() + .put(entry.getKey(), new RevertibleObject(hashCode)); + } + } + + private static Object getOriginalValue( + JinjavaInterpreter interpreter, + EagerChildContextConfig eagerChildContextConfig, + Map initiallyResolvedHashes, + Map initiallyResolvedAsStrings, + Entry e + ) { + if (eagerChildContextConfig.takeNewValue) { + if (e.getValue() instanceof DeferredValue) { + return ((DeferredValue) e.getValue()).getOriginalValue(); + } + return e.getValue(); + } + + if ( + e.getValue() instanceof DeferredValue && + initiallyResolvedHashes + .get(e.getKey()) + .equals(getObjectOrHashCode(((DeferredValue) e.getValue()).getOriginalValue())) + ) { + return ((DeferredValue) e.getValue()).getOriginalValue(); + } + + // This is necessary if a state-changing function, such as .update() + // or .append() is run against a variable in the context. + // It will revert the effects when takeNewValue is false. + if (initiallyResolvedAsStrings.containsKey(e.getKey())) { + // convert to new list or map + try { + return interpreter.resolveELExpression( + initiallyResolvedAsStrings.get(e.getKey()), + interpreter.getLineNumber() + ); + } catch (DeferredValueException ignored) {} + } + + // Previous value could not be mapped to a string + throw new DeferredValueException( + String.format("%s: %s", CANNOT_RECONSTRUCT_MESSAGE, e.getKey()) + ); + } + + private static Object getObjectOrHashCode(Object o) { + if (o instanceof LazyExpression) { + o = ((LazyExpression) o).get(); + } + if (o instanceof PyList && !((PyList) o).toList().contains(o)) { + return o.hashCode(); + } + if (o instanceof PyMap && !((PyMap) o).toMap().containsValue(o)) { + return o.hashCode() + ((PyMap) o).keySet().hashCode(); + } + return o; + } + + public static class EagerChildContextConfig { + private final boolean takeNewValue; + + private final boolean discardSessionBindings; + private final boolean partialMacroEvaluation; + + private final boolean checkForContextChanges; + private final boolean forceDeferredExecutionMode; + + private EagerChildContextConfig( + boolean takeNewValue, + boolean discardSessionBindings, + boolean partialMacroEvaluation, + boolean checkForContextChanges, + boolean forceDeferredExecutionMode + ) { + this.takeNewValue = takeNewValue; + this.discardSessionBindings = discardSessionBindings; + this.partialMacroEvaluation = partialMacroEvaluation; + this.checkForContextChanges = checkForContextChanges; + this.forceDeferredExecutionMode = forceDeferredExecutionMode; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + private boolean takeNewValue; + + private boolean discardSessionBindings; + private boolean partialMacroEvaluation; + private boolean checkForContextChanges; + private boolean forceDeferredExecutionMode; + + private Builder() {} + + /** + * @param takeNewValue If a value is updated (not replaced) either take the new value or + * take the previous value and put it into the + * EagerExecutionResult.prefixToPreserveState. + */ + public Builder withTakeNewValue(boolean takeNewValue) { + this.takeNewValue = takeNewValue; + return this; + } + + /** + * @param discardSessionBindings Discard the session bindings from the child context + * created while executing the provided function. + */ + public Builder withDiscardSessionBindings(boolean discardSessionBindings) { + this.discardSessionBindings = discardSessionBindings; + return this; + } + + /** + * @param partialMacroEvaluation Allow macro functions to be partially evaluated rather than + * needing an explicit result during this render. + */ + public Builder withPartialMacroEvaluation(boolean partialMacroEvaluation) { + this.partialMacroEvaluation = partialMacroEvaluation; + return this; + } + + /** + * @param checkForContextChanges Hash and serialize values on the context to determine if changes + * have been made to any values on the context. + */ + public Builder withCheckForContextChanges(boolean checkForContextChanges) { + this.checkForContextChanges = checkForContextChanges; + return this; + } + + /** + * @param forceDeferredExecutionMode Start the evaluation of the specified function in deferred execution mode. + */ + public Builder withForceDeferredExecutionMode(boolean forceDeferredExecutionMode) { + this.forceDeferredExecutionMode = forceDeferredExecutionMode; + return this; + } + + public EagerChildContextConfig build() { + return new EagerChildContextConfig( + takeNewValue, + discardSessionBindings, + partialMacroEvaluation, + checkForContextChanges, + forceDeferredExecutionMode + ); + } + } + } +} diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index daf615399..150bc3b3c 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -7,12 +7,8 @@ import com.hubspot.jinjava.interpret.DeferredLazyReference; import com.hubspot.jinjava.interpret.DeferredLazyReferenceSource; import com.hubspot.jinjava.interpret.DeferredValue; -import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.DisabledException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; -import com.hubspot.jinjava.interpret.LazyExpression; -import com.hubspot.jinjava.interpret.RevertibleObject; import com.hubspot.jinjava.lib.fn.MacroFunction; import com.hubspot.jinjava.lib.fn.eager.EagerMacroFunction; import com.hubspot.jinjava.lib.tag.AutoEscapeTag; @@ -23,12 +19,11 @@ import com.hubspot.jinjava.lib.tag.eager.DeferredToken; import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; import com.hubspot.jinjava.mode.EagerExecutionMode; -import com.hubspot.jinjava.objects.collections.PyList; -import com.hubspot.jinjava.objects.collections.PyMap; import com.hubspot.jinjava.objects.serialization.PyishBlockSetSerializable; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; +import com.hubspot.jinjava.util.EagerContextWatcher.EagerChildContextConfig; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import java.util.AbstractMap; import java.util.Collections; @@ -44,9 +39,9 @@ import java.util.stream.Stream; public class EagerReconstructionUtils { - public static final String CANNOT_RECONSTRUCT_MESSAGE = "Cannot reconstruct value"; /** + * @deprecated Use {@link EagerContextWatcher#executeInChildContext(Function, JinjavaInterpreter, EagerChildContextConfig)} * Execute the specified functions within a protected context. * Additionally, if the execution causes existing values on the context to become * deferred, then their previous values will wrapped in a set @@ -69,6 +64,7 @@ public class EagerReconstructionUtils { * prefixToPreserveState is either blank or a set tag * that preserves the state within the output for a second rendering pass. */ + @Deprecated public static EagerExecutionResult executeInChildContext( Function function, JinjavaInterpreter interpreter, @@ -93,336 +89,13 @@ public static EagerExecutionResult executeInChildContext( JinjavaInterpreter interpreter, EagerChildContextConfig eagerChildContextConfig ) { - final Set metaContextVariables = interpreter - .getContext() - .getMetaContextVariables(); - final EagerExecutionResult initialResult; - final Map speculativeBindings; - if (eagerChildContextConfig.checkForContextChanges) { - final Set> entrySet = interpreter.getContext().entrySet(); - final Map initiallyResolvedHashes = getInitiallyResolvedHashes( - entrySet, - metaContextVariables - ); - final Map initiallyResolvedAsStrings = getInitiallyResolvedAsStrings( - interpreter, - entrySet, - initiallyResolvedHashes - ); - initialResult = applyFunction(function, interpreter, eagerChildContextConfig); - speculativeBindings = - getAllSpeculativeBindings( - interpreter, - eagerChildContextConfig, - metaContextVariables, - initiallyResolvedHashes, - initiallyResolvedAsStrings, - initialResult.getSpeculativeBindings() - ); - } else { - initialResult = applyFunction(function, interpreter, eagerChildContextConfig); - speculativeBindings = - getBasicSpeculativeBindings( - interpreter, - eagerChildContextConfig, - metaContextVariables, - initialResult.getSpeculativeBindings() - ); - } - return new EagerExecutionResult(initialResult.getResult(), speculativeBindings); - } - - private static EagerExecutionResult applyFunction( - Function function, - JinjavaInterpreter interpreter, - EagerChildContextConfig eagerChildContextConfig - ) { - // Don't create new call stacks to prevent hitting max recursion with this silent new scope - try (InterpreterScopeClosable c = interpreter.enterNonStackingScope()) { - if (eagerChildContextConfig.forceDeferredExecutionMode) { - interpreter.getContext().setDeferredExecutionMode(true); - } - interpreter - .getContext() - .setPartialMacroEvaluation(eagerChildContextConfig.partialMacroEvaluation); - return new EagerExecutionResult( - function.apply(interpreter), - eagerChildContextConfig.discardSessionBindings - ? new HashMap<>() - : interpreter.getContext().getSessionBindings() - ); - } - } - - private static Map getInitiallyResolvedAsStrings( - JinjavaInterpreter interpreter, - Set> entrySet, - Map initiallyResolvedHashes - ) { - Map initiallyResolvedAsStrings = new HashMap<>(); - // This creates a stringified snapshot of the context - // so it can be disabled via the config because it may cause performance issues. - Stream> entryStream = - ( - interpreter.getConfig().getExecutionMode().useEagerContextReverting() - ? entrySet - : interpreter.getContext().getCombinedScope().entrySet() - ).stream() - .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) - .filter( - entry -> EagerExpressionResolver.isResolvableObject(entry.getValue(), 4, 400) // TODO make this configurable - ); - entryStream.forEach( - entry -> - cacheRevertibleObject( - interpreter, - initiallyResolvedHashes, - initiallyResolvedAsStrings, - entry - ) - ); - return initiallyResolvedAsStrings; - } - - private static Map getInitiallyResolvedHashes( - Set> entrySet, - Set metaContextVariables - ) { - return entrySet - .stream() - .filter(entry -> !metaContextVariables.contains(entry.getKey())) - .filter( - entry -> !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null - ) - .collect( - Collectors.toMap(Entry::getKey, entry -> getObjectOrHashCode(entry.getValue())) - ); - } - - private static Map getBasicSpeculativeBindings( - JinjavaInterpreter interpreter, - EagerChildContextConfig eagerChildContextConfig, - Set metaContextVariables, - Map speculativeBindings - ) { - speculativeBindings.putAll( - interpreter - .getContext() - .getScope() - .entrySet() - .stream() - .filter( - entry -> - entry.getValue() instanceof DeferredLazyReferenceSource && - !(((DeferredLazyReferenceSource) entry.getValue()).isReconstructed()) - ) - .peek( - entry -> ((DeferredLazyReferenceSource) entry.getValue()).setReconstructed(true) - ) - .collect( - Collectors.toMap( - Entry::getKey, - entry -> ((DeferredLazyReferenceSource) entry.getValue()).getOriginalValue() - ) - ) - ); - return speculativeBindings - .entrySet() - .stream() - .filter(entry -> !metaContextVariables.contains(entry.getKey())) - .filter(entry -> !"loop".equals(entry.getKey())) - .map( - entry -> { - if ( - eagerChildContextConfig.takeNewValue && - !(entry.getValue() instanceof DeferredValue) && - entry.getValue() != null - ) { - return entry; - } - Object contextValue = interpreter.getContext().get(entry.getKey()); - if ( - contextValue instanceof DeferredValue && - ((DeferredValue) contextValue).getOriginalValue() != null - ) { - if ( - !eagerChildContextConfig.takeNewValue && - !EagerExpressionResolver.isResolvableObject( - ((DeferredValue) contextValue).getOriginalValue() - ) - ) { - throw new DeferredValueException( - String.format("%s: %s", CANNOT_RECONSTRUCT_MESSAGE, entry.getKey()) - ); - } - return new AbstractMap.SimpleImmutableEntry<>( - entry.getKey(), - ((DeferredValue) contextValue).getOriginalValue() - ); - } - return null; - } - ) - .filter(Objects::nonNull) - .collect( - Collectors.toMap( - Entry::getKey, - entry -> - entry.getValue() instanceof DeferredValue - ? ((DeferredValue) entry.getValue()).getOriginalValue() - : entry.getValue() - ) - ); - } - - private static Map getAllSpeculativeBindings( - JinjavaInterpreter interpreter, - EagerChildContextConfig eagerChildContextConfig, - Set metaContextVariables, - Map initiallyResolvedHashes, - Map initiallyResolvedAsStrings, - Map speculativeBindings - ) { - speculativeBindings = - speculativeBindings - .entrySet() - .stream() - .filter( - entry -> - entry.getValue() != null && - !entry.getValue().equals(interpreter.getContext().get(entry.getKey())) - ) - .filter( - entry -> - !(interpreter.getContext().get(entry.getKey()) instanceof DeferredValue) - ) - .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); - speculativeBindings.putAll( - interpreter - .getContext() - .entrySet() - .stream() - .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) - .filter( - entry -> - !initiallyResolvedHashes - .get(entry.getKey()) - .equals(getObjectOrHashCode(entry.getValue())) - ) - .collect( - Collectors.toMap( - Entry::getKey, - entry -> - getOriginalValue( - interpreter, - eagerChildContextConfig, - initiallyResolvedHashes, - initiallyResolvedAsStrings, - entry - ) - ) - ) - ); - - speculativeBindings = - speculativeBindings - .entrySet() - .stream() - .filter(entry -> !metaContextVariables.contains(entry.getKey())) - .filter( - entry -> - !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null - ) // these are already set recursively - .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); - return speculativeBindings; - } - - private static void cacheRevertibleObject( - JinjavaInterpreter interpreter, - Map initiallyResolvedHashes, - Map initiallyResolvedAsStrings, - Entry entry - ) { - RevertibleObject revertibleObject = interpreter - .getRevertibleObjects() - .get(entry.getKey()); - Object hashCode = initiallyResolvedHashes.get(entry.getKey()); - try { - if (revertibleObject == null || !hashCode.equals(revertibleObject.getHashCode())) { - revertibleObject = - new RevertibleObject( - hashCode, - PyishObjectMapper.getAsPyishStringOrThrow(entry.getValue()) - ); - interpreter.getRevertibleObjects().put(entry.getKey(), revertibleObject); - } - revertibleObject - .getPyishString() - .ifPresent( - pyishString -> initiallyResolvedAsStrings.put(entry.getKey(), pyishString) - ); - } catch (Exception e) { - interpreter - .getRevertibleObjects() - .put(entry.getKey(), new RevertibleObject(hashCode)); - } - } - - private static Object getOriginalValue( - JinjavaInterpreter interpreter, - EagerChildContextConfig eagerChildContextConfig, - Map initiallyResolvedHashes, - Map initiallyResolvedAsStrings, - Entry e - ) { - if (eagerChildContextConfig.takeNewValue) { - if (e.getValue() instanceof DeferredValue) { - return ((DeferredValue) e.getValue()).getOriginalValue(); - } - return e.getValue(); - } - - if ( - e.getValue() instanceof DeferredValue && - initiallyResolvedHashes - .get(e.getKey()) - .equals(getObjectOrHashCode(((DeferredValue) e.getValue()).getOriginalValue())) - ) { - return ((DeferredValue) e.getValue()).getOriginalValue(); - } - - // This is necessary if a state-changing function, such as .update() - // or .append() is run against a variable in the context. - // It will revert the effects when takeNewValue is false. - if (initiallyResolvedAsStrings.containsKey(e.getKey())) { - // convert to new list or map - try { - return interpreter.resolveELExpression( - initiallyResolvedAsStrings.get(e.getKey()), - interpreter.getLineNumber() - ); - } catch (DeferredValueException ignored) {} - } - - // Previous value could not be mapped to a string - throw new DeferredValueException( - String.format("%s: %s", CANNOT_RECONSTRUCT_MESSAGE, e.getKey()) + return EagerContextWatcher.executeInChildContext( + function, + interpreter, + eagerChildContextConfig ); } - private static Object getObjectOrHashCode(Object o) { - if (o instanceof LazyExpression) { - o = ((LazyExpression) o).get(); - } - if (o instanceof PyList && !((PyList) o).toList().contains(o)) { - return o.hashCode(); - } - if (o instanceof PyMap && !((PyMap) o).toMap().containsValue(o)) { - return o.hashCode() + ((PyMap) o).keySet().hashCode(); - } - return o; - } - /** * Reconstruct the macro functions and variables from the context before they * get deferred. @@ -484,15 +157,15 @@ private static String reconstructMacroFunctionsBeforeDeferring( .peek(entry -> entry.getValue().setDeferred(true)) .map( entry -> - executeInChildContext( + EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromString( new EagerMacroFunction(entry.getKey(), entry.getValue(), interpreter) .reconstructImage() ), interpreter, - EagerChildContextConfig - .newBuilder() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() .withForceDeferredExecutionMode(true) .build() ) @@ -967,78 +640,4 @@ public static String reconstructDeferredReferences( ); return extraStuff; } - - public static class EagerChildContextConfig { - private final boolean takeNewValue; - - private final boolean discardSessionBindings; - private final boolean partialMacroEvaluation; - - private final boolean checkForContextChanges; - private final boolean forceDeferredExecutionMode; - - private EagerChildContextConfig( - boolean takeNewValue, - boolean discardSessionBindings, - boolean partialMacroEvaluation, - boolean checkForContextChanges, - boolean forceDeferredExecutionMode - ) { - this.takeNewValue = takeNewValue; - this.discardSessionBindings = discardSessionBindings; - this.partialMacroEvaluation = partialMacroEvaluation; - this.checkForContextChanges = checkForContextChanges; - this.forceDeferredExecutionMode = forceDeferredExecutionMode; - } - - public static Builder newBuilder() { - return new Builder(); - } - - public static class Builder { - private boolean takeNewValue; - - private boolean discardSessionBindings; - private boolean partialMacroEvaluation; - private boolean checkForContextChanges; - private boolean forceDeferredExecutionMode; - - private Builder() {} - - public Builder withTakeNewValue(boolean takeNewValue) { - this.takeNewValue = takeNewValue; - return this; - } - - public Builder withDiscardSessionBindings(boolean discardSessionBindings) { - this.discardSessionBindings = discardSessionBindings; - return this; - } - - public Builder withPartialMacroEvaluation(boolean partialMacroEvaluation) { - this.partialMacroEvaluation = partialMacroEvaluation; - return this; - } - - public Builder withCheckForContextChanges(boolean checkForContextChanges) { - this.checkForContextChanges = checkForContextChanges; - return this; - } - - public Builder withForceDeferredExecutionMode(boolean forceDeferredExecutionMode) { - this.forceDeferredExecutionMode = forceDeferredExecutionMode; - return this; - } - - public EagerChildContextConfig build() { - return new EagerChildContextConfig( - takeNewValue, - discardSessionBindings, - partialMacroEvaluation, - checkForContextChanges, - forceDeferredExecutionMode - ); - } - } - } } diff --git a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java index 172cdc68c..f380d8d3e 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java @@ -25,7 +25,6 @@ import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.DefaultTokenScannerSymbols; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -67,7 +66,7 @@ public void teardown() { @Test public void itExecutesInChildContextAndTakesNewValue() { context.put("foo", new PyList(new ArrayList<>())); - EagerExecutionResult result = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult result = EagerContextWatcher.executeInChildContext( ( interpreter1 -> { ((List) interpreter1.getContext().get("foo")).add(1); @@ -75,8 +74,8 @@ public void itExecutesInChildContextAndTakesNewValue() { } ), interpreter, - EagerChildContextConfig - .newBuilder() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() .withTakeNewValue(true) .withForceDeferredExecutionMode(true) .withCheckForContextChanges(true) @@ -94,7 +93,7 @@ public void itExecutesInChildContextAndTakesNewValue() { @Test public void itExecutesInChildContextAndDefersNewValue() { context.put("foo", new ArrayList()); - EagerExecutionResult result = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult result = EagerContextWatcher.executeInChildContext( ( interpreter1 -> { context.put( @@ -105,8 +104,8 @@ public void itExecutesInChildContextAndDefersNewValue() { } ), interpreter, - EagerChildContextConfig - .newBuilder() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() .withForceDeferredExecutionMode(true) .withCheckForContextChanges(true) .build() @@ -370,26 +369,26 @@ public void itRemovesOtherMetaContextVariables() { @Test public void itDiscardsSessionBindings() { interpreter.getContext().put("foo", "bar"); - EagerExecutionResult withSessionBindings = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult withSessionBindings = EagerContextWatcher.executeInChildContext( eagerInterpreter -> { interpreter.getContext().put("foo", "foobar"); return EagerExpressionResult.fromString(""); }, interpreter, - EagerChildContextConfig - .newBuilder() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() .withDiscardSessionBindings(false) .withCheckForContextChanges(true) .build() ); - EagerExecutionResult withoutSessionBindings = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult withoutSessionBindings = EagerContextWatcher.executeInChildContext( eagerInterpreter -> { interpreter.getContext().put("foo", "foobar"); return EagerExpressionResult.fromString(""); }, interpreter, - EagerChildContextConfig - .newBuilder() + EagerContextWatcher + .EagerChildContextConfig.newBuilder() .withDiscardSessionBindings(true) .withCheckForContextChanges(true) .build() From cf08d54153b89228aa40197b7d8cec6b09cdf13f Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 21 Dec 2022 14:24:34 -0500 Subject: [PATCH 112/637] Remove unused test file --- .../hubspot/jinjava/util/EagerReconstructionUtils.java | 6 +++--- src/test/java/com/hubspot/jinjava/EagerTest.java | 2 +- .../hubspot/jinjava/lib/tag/eager/EagerForTagTest.java | 1 - .../eager/handles-same-name-import-var.expected.jinja | 10 ---------- 4 files changed, 4 insertions(+), 15 deletions(-) delete mode 100644 src/test/resources/eager/handles-same-name-import-var.expected.jinja diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 150bc3b3c..f12b1c823 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -584,7 +584,7 @@ public static String reconstructDeferredReferences( JinjavaInterpreter interpreter, EagerExecutionResult eagerExecutionResult ) { - String extraStuff = + return ( buildSetTag( interpreter .getContext() @@ -637,7 +637,7 @@ public static String reconstructDeferredReferences( ), interpreter, false - ); - return extraStuff; + ) + ); } } diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 4e8866b40..b4baa9c2a 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1004,7 +1004,7 @@ public void itHandlesDoubleImportModificationSecondPass() { @Test public void itHandlesSameNameImportVar() { String template = expectedTemplateInterpreter.getFixtureTemplate( - "handles-import-with-macros-in-deferred-if" + "handles-same-name-import-var" ); JinjavaInterpreter.getCurrent().render(template); // No longer allows importing a file that uses the same alias as a variable declared in the import file diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java index a82d9e435..956591f5c 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java @@ -217,7 +217,6 @@ public void itDefersLoopVariable() { } @Test - // @Ignore // Not needed since append is disallowed in deferred execution mode public void itDoesNotSwallowDeferredValueException() { interpreter.getContext().registerTag(new EagerDoTag()); interpreter.getContext().registerTag(new EagerIfTag()); diff --git a/src/test/resources/eager/handles-same-name-import-var.expected.jinja b/src/test/resources/eager/handles-same-name-import-var.expected.jinja deleted file mode 100644 index 2149e6499..000000000 --- a/src/test/resources/eager/handles-same-name-import-var.expected.jinja +++ /dev/null @@ -1,10 +0,0 @@ -{% if deferred %} -{% set __ignored__ %}{% set current_path = '../settag/set-var-and-deferred.jinja' %}{% set path,value = null,null %}{% set my_var = {} %}{% set my_var = {} %}{% if deferred %} -{% set path,my_var = '',{} %}{% set __ignored__ %}{% set path = '../settag/set-var-and-deferred.jinja' %}{% set my_var = {} %}{% do my_var.update({'path': path}) %}{% set value = null %}{% do my_var.update({'value': value}) %}{% set my_var = {} %}{% set my_var = {'foo': 'bar'} %}{% set my_var = {'my_var': {'foo': 'bar'} } %} -{% set value = deferred %}{% set my_var = {'my_var': {'foo': 'bar'} } %}{% do my_var.update({'value': value}) %}{% do my_var.update({'value': value}) %} -{% do my_var.update({'import_resource_path': '../settag/set-var-and-deferred.jinja', 'value': value}) %}{% set path = '' %}{% do my_var.update({'path': path}) %}{% endset %}{% do my_var.update({'__ignored__': __ignored__}) %} -{{ my_var }} -{% endif %} -{% do my_var.update({'path': path,'import_resource_path': '../settag/set-var-and-deferred.jinja','value': value}) %}{% set current_path = '' %}{% endset %} -{{ my_var }} -{% endif %} From ccf710b50b16b3cadbcadbf5b24f92b59c8f5533 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 21 Dec 2022 16:32:48 -0500 Subject: [PATCH 113/637] Replace usages of handleDeferredToken with method that also reconstructs deferred references --- .../expression/EagerExpressionStrategy.java | 11 ++---- .../tag/eager/EagerBlockSetTagStrategy.java | 12 ++---- .../jinjava/lib/tag/eager/EagerCallTag.java | 11 +++--- .../jinjava/lib/tag/eager/EagerCycleTag.java | 12 +++--- .../jinjava/lib/tag/eager/EagerForTag.java | 13 ++++--- .../jinjava/lib/tag/eager/EagerIfTag.java | 10 +++-- .../jinjava/lib/tag/eager/EagerImportTag.java | 12 +++--- .../tag/eager/EagerInlineSetTagStrategy.java | 13 ++----- .../jinjava/lib/tag/eager/EagerPrintTag.java | 11 ++---- .../lib/tag/eager/EagerTagDecorator.java | 16 ++++---- .../util/EagerReconstructionUtils.java | 37 +++++++++++++------ ...cope-reference-modification.expected.jinja | 2 +- 12 files changed, 80 insertions(+), 80 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java index bdd6e10e3..377118fe3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java @@ -69,9 +69,9 @@ private String eagerResolveExpression( eagerExecutionResult.getResult().toString(), interpreter ); - interpreter - .getContext() - .handleDeferredToken( + prefixToPreserveState.append( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, new DeferredToken( new ExpressionToken( helpers, @@ -89,11 +89,6 @@ private String eagerResolveExpression( ) .collect(Collectors.toSet()) ) - ); - prefixToPreserveState.append( - EagerReconstructionUtils.reconstructDeferredReferences( - interpreter, - eagerExecutionResult ) ); // There is only a preserving prefix because it couldn't be entirely evaluated. diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java index 3c480a55b..25337c4f2 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java @@ -112,15 +112,11 @@ protected Triple getPrefixTokenAndSuffix( .add(tagNode.getTag().getName()) .add(variables[0]) .add(tagNode.getSymbols().getExpressionEndWithTag()); - String prefixToPreserveState = getPrefixToPreserveState( - eagerExecutionResult, - variables, - interpreter - ); - interpreter - .getContext() - .handleDeferredToken( + String prefixToPreserveState = + getPrefixToPreserveState(eagerExecutionResult, variables, interpreter) + + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, new DeferredToken( new TagToken( joiner.toString(), diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java index 6449c7180..6a6b3eef6 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCallTag.java @@ -103,9 +103,9 @@ public String eagerInterpret( .add(tagNode.getTag().getName()) .add(eagerExecutionResult.getResult().toString().trim()) .add(tagNode.getSymbols().getExpressionEndWithTag()); - interpreter - .getContext() - .handleDeferredToken( + prefixToPreserveState.append( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, new DeferredToken( new TagToken( joiner.toString(), @@ -123,10 +123,9 @@ public String eagerInterpret( ) .collect(Collectors.toSet()) ) - ); - StringBuilder result = new StringBuilder( - prefixToPreserveState.toString() + joiner.toString() + ) ); + StringBuilder result = new StringBuilder(prefixToPreserveState + joiner.toString()); interpreter.getContext().setDynamicVariableResolver(s -> DeferredValue.instance()); if (!tagNode.getChildren().isEmpty()) { result.append( 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 7cff8aab3..893514b50 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 @@ -180,10 +180,10 @@ private String interpretPrintingCycle( ) { if (interpreter.getContext().isDeferredExecutionMode()) { String reconstructedTag = reconstructCycleTag(resolvedExpression, tagToken); - - interpreter - .getContext() - .handleDeferredToken( + return ( + reconstructedTag + + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, new DeferredToken( new TagToken( reconstructedTag, @@ -193,8 +193,8 @@ private String interpretPrintingCycle( ), deferredWords ) - ); - return reconstructedTag; + ) + ); } Integer forindex = (Integer) interpreter.retraceVariable( CycleTag.LOOP_INDEX, diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 6dd0a9dd2..aeff3304e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -194,18 +194,20 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter .add("in") .add(eagerExpressionResult.toString()) .add(tagToken.getSymbols().getExpressionEndWithTag()); + StringBuilder prefixToPreserveState = new StringBuilder(); String newlyDeferredFunctionImages = EagerReconstructionUtils.reconstructFromContextBeforeDeferring( eagerExpressionResult.getDeferredWords(), interpreter ); + prefixToPreserveState.append(newlyDeferredFunctionImages); EagerReconstructionUtils.removeMetaContextVariables( loopVars.stream(), interpreter.getContext() ); - interpreter - .getContext() - .handleDeferredToken( + prefixToPreserveState.append( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, new DeferredToken( new TagToken( joiner.toString(), @@ -223,7 +225,8 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter .collect(Collectors.toSet()), new HashSet<>(loopVars) ) - ); - return (newlyDeferredFunctionImages + joiner.toString()); + ) + ); + return (prefixToPreserveState + joiner.toString()); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java index d620abd9f..25d1e2484 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java @@ -94,6 +94,7 @@ public String eagerRenderBranches( // We know this has to start as false otherwise IfTag would have chosen // the first branch. boolean definitelyExecuted = false; + StringBuilder prefixToPreserveState = new StringBuilder(); StringBuilder sb = new StringBuilder(); sb.append( getEagerImage( @@ -159,9 +160,9 @@ public String eagerRenderBranches( .filter(key -> !(interpreter.getContext().get(key) instanceof DeferredValue)) .collect(Collectors.toSet()); if (!bindingsToDefer.isEmpty()) { - interpreter - .getContext() - .handleDeferredToken( + prefixToPreserveState.append( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, new DeferredToken( new NoteToken( "", @@ -171,7 +172,8 @@ public String eagerRenderBranches( ), bindingsToDefer ) - ); + ) + ); } return sb.toString(); } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java index 447bdb149..520ba3bfe 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java @@ -55,16 +55,18 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter if (currentImportAlias.isEmpty()) { throw e; } - interpreter - .getContext() - .handleDeferredToken( + return ( + initialPathSetter + + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, new DeferredToken( tagToken, Collections.singleton(helper.get(0)), Collections.singleton(currentImportAlias) ) - ); - return (initialPathSetter + tagToken.getImage()); + ) + + tagToken.getImage() + ); } if (!maybeTemplateFile.isPresent()) { return ""; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java index fc8b5b7da..9b786a84d 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java @@ -83,15 +83,10 @@ public Triple getPrefixTokenAndSuffix( .add("=") .add(deferredResult) .add(tagNode.getSymbols().getExpressionEndWithTag()); - String prefixToPreserveState = getPrefixToPreserveState( - eagerExecutionResult, - variables, - interpreter - ); - - interpreter - .getContext() - .handleDeferredToken( + String prefixToPreserveState = + getPrefixToPreserveState(eagerExecutionResult, variables, interpreter) + + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, new DeferredToken( new TagToken( joiner.toString(), diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java index f8b010da4..f1a0ccc21 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerPrintTag.java @@ -97,9 +97,9 @@ public static String interpretExpression( .add(tagToken.getTagName()) .add(eagerExecutionResult.getResult().toString().trim()) .add(tagToken.getSymbols().getExpressionEndWithTag()); - interpreter - .getContext() - .handleDeferredToken( + prefixToPreserveState.append( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, new DeferredToken( new TagToken( joiner.toString(), @@ -117,11 +117,6 @@ public static String interpretExpression( ) .collect(Collectors.toSet()) ) - ); - prefixToPreserveState.append( - EagerReconstructionUtils.reconstructDeferredReferences( - interpreter, - eagerExecutionResult ) ); // Possible set tag in front of this one. diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java index 92455b72a..a1c1f8cf6 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecorator.java @@ -213,14 +213,14 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter joiner.add(resolvedString); } joiner.add(tagToken.getSymbols().getExpressionEndWithTag()); - String reconstructedFromContext = EagerReconstructionUtils.reconstructFromContextBeforeDeferring( - eagerExpressionResult.getDeferredWords(), - interpreter - ); - interpreter - .getContext() - .handleDeferredToken( + String prefixToPreserveState = + EagerReconstructionUtils.reconstructFromContextBeforeDeferring( + eagerExpressionResult.getDeferredWords(), + interpreter + ) + + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, new DeferredToken( new TagToken( joiner.toString(), @@ -239,6 +239,6 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter ) ); - return (reconstructedFromContext + joiner.toString()); + return (prefixToPreserveState + joiner.toString()); } } diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index f12b1c823..b364e61ba 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -376,9 +376,9 @@ public static String buildSetTag( String image = result.toString(); // Don't defer if we're sticking with the new value if (registerDeferredToken) { - interpreter - .getContext() - .handleDeferredToken( + return ( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, new DeferredToken( new TagToken( image, @@ -390,7 +390,9 @@ public static String buildSetTag( Collections.emptySet(), deferredValuesToSet.keySet() ) - ); + ) + + image + ); } return image; } @@ -437,9 +439,9 @@ public static String buildBlockSetTag( .add(interpreter.getConfig().getTokenScannerSymbols().getExpressionEndWithTag()); String image = blockSetTokenBuilder + value + endTokenBuilder; if (registerDeferredToken) { - interpreter - .getContext() - .handleDeferredToken( + return ( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, new DeferredToken( new TagToken( blockSetTokenBuilder.toString(), @@ -450,7 +452,9 @@ public static String buildBlockSetTag( Collections.emptySet(), Collections.singleton(name) ) - ); + ) + + image + ); } return image; } @@ -580,9 +584,20 @@ public static Boolean isDeferredExecutionMode() { .orElse(false); } + public static String handleDeferredTokenAndReconstructReferences( + JinjavaInterpreter interpreter, + DeferredToken deferredToken + ) { + interpreter.getContext().handleDeferredToken(deferredToken); + return reconstructDeferredReferences( + interpreter, + deferredToken.getUsedDeferredWords() + ); + } + public static String reconstructDeferredReferences( JinjavaInterpreter interpreter, - EagerExecutionResult eagerExecutionResult + Set usedDeferredWords ) { return ( buildSetTag( @@ -613,9 +628,7 @@ public static String reconstructDeferredReferences( false ) + buildSetTag( - eagerExecutionResult - .getResult() - .getDeferredWords() + usedDeferredWords .stream() .map(w -> w.split("\\.", 2)[0]) .map( diff --git a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja b/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja index cdfa41f49..95a282556 100644 --- a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja +++ b/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja @@ -4,7 +4,7 @@ C: {{ c_list }}.{% endmacro %}{% set b_list = a_list %}{% set b_list = a_list %} B: {% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{{ b_list }}.{% do a_list.append(deferred ? 'A' : '') %} A: {{ a_list }}. --- -{% set a_list = ['a'] %}{% set b_list = a_list %}{% for i in [0] %}{% set b_list = a_list %}{% set b_list = a_list %}{% do b_list.append('b') %}{% for __ignored__ in [0] %}{% set c_list = b_list %}{% do c_list.append(deferred ? 'c' : '') %} +{% set a_list = ['a'] %}{% set b_list = a_list %}{% for i in [0] %}{% set b_list = a_list %}{% set b_list = a_list %}{% do b_list.append('b') %}{% for __ignored__ in [0] %}{% set b_list = a_list %}{% set c_list = b_list %}{% do c_list.append(deferred ? 'c' : '') %} C: {{ c_list }}.{% endfor %}{% set b_list = a_list %}{% do b_list.append(deferred ? 'B' : '') %} B: {% set b_list = a_list %}{{ b_list }}.{% endfor %}{% do a_list.append(deferred ? 'A' : '') %} A: {{ a_list }}. From 561123cba8040192d9f45ec2c79bd03c260dd6ab Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 21 Dec 2022 19:13:21 -0500 Subject: [PATCH 114/637] Add special DeferredValueException rather than checking message prefix --- .../interpret/CannotReconstructValueException.java | 9 +++++++++ .../com/hubspot/jinjava/lib/tag/eager/EagerForTag.java | 6 ++---- .../com/hubspot/jinjava/util/EagerContextWatcher.java | 10 +++------- 3 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/hubspot/jinjava/interpret/CannotReconstructValueException.java diff --git a/src/main/java/com/hubspot/jinjava/interpret/CannotReconstructValueException.java b/src/main/java/com/hubspot/jinjava/interpret/CannotReconstructValueException.java new file mode 100644 index 000000000..c47643309 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/interpret/CannotReconstructValueException.java @@ -0,0 +1,9 @@ +package com.hubspot.jinjava.interpret; + +public class CannotReconstructValueException extends DeferredValueException { + public static final String CANNOT_RECONSTRUCT_MESSAGE = "Cannot reconstruct value"; + + public CannotReconstructValueException(String key) { + super(String.format("%s: %s", CANNOT_RECONSTRUCT_MESSAGE, key)); + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index aeff3304e..13291b73a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.hubspot.jinjava.interpret.CannotReconstructValueException; import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; import com.hubspot.jinjava.interpret.DeferredValue; @@ -81,10 +82,7 @@ public String eagerInterpret( JinjavaInterpreter interpreter, InterpretException e ) { - if ( - e instanceof DeferredValueException && - e.getMessage().startsWith(EagerContextWatcher.CANNOT_RECONSTRUCT_MESSAGE) - ) { + if (e instanceof CannotReconstructValueException) { throw e; } LengthLimitingStringBuilder result = new LengthLimitingStringBuilder( diff --git a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java index c69ef21ba..db0e77ad8 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.util; +import com.hubspot.jinjava.interpret.CannotReconstructValueException; import com.hubspot.jinjava.interpret.DeferredLazyReferenceSource; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.DeferredValueException; @@ -23,7 +24,6 @@ import java.util.stream.Stream; public class EagerContextWatcher { - public static final String CANNOT_RECONSTRUCT_MESSAGE = "Cannot reconstruct value"; /** * Execute the specified functions within a protected context. @@ -204,9 +204,7 @@ private static Map getBasicSpeculativeBindings( ((DeferredValue) contextValue).getOriginalValue() ) ) { - throw new DeferredValueException( - String.format("%s: %s", CANNOT_RECONSTRUCT_MESSAGE, entry.getKey()) - ); + throw new CannotReconstructValueException(entry.getKey()); } return new AbstractMap.SimpleImmutableEntry<>( entry.getKey(), @@ -358,9 +356,7 @@ private static Object getOriginalValue( } // Previous value could not be mapped to a string - throw new DeferredValueException( - String.format("%s: %s", CANNOT_RECONSTRUCT_MESSAGE, e.getKey()) - ); + throw new CannotReconstructValueException(e.getKey()); } private static Object getObjectOrHashCode(Object o) { From aba069a27f27d2782a8ca1010e78cda97eb29818 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 29 Dec 2022 11:47:14 -0500 Subject: [PATCH 115/637] Use initiallyResolvedHashes to call interpreter.getContext().entrySet() fewer times --- .../com/hubspot/jinjava/util/EagerContextWatcher.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java index db0e77ad8..765a0bb5b 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java @@ -249,11 +249,13 @@ private static Map getAllSpeculativeBindings( ) .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); speculativeBindings.putAll( - interpreter - .getContext() - .entrySet() + initiallyResolvedHashes + .keySet() .stream() - .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) + .map( + key -> + new AbstractMap.SimpleImmutableEntry<>(key, interpreter.getContext().get(key)) + ) .filter( entry -> !initiallyResolvedHashes From dbeb34af5709d2ae316cd953a39da5eb8ae1acb7 Mon Sep 17 00:00:00 2001 From: harikrishna553 Date: Sat, 31 Dec 2022 21:20:21 +0530 Subject: [PATCH 116/637] Correct java doc for BetweenTimesFilter --- .../java/com/hubspot/jinjava/lib/filter/BetweenTimesFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/BetweenTimesFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/BetweenTimesFilter.java index d5e826385..52d5fd290 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/BetweenTimesFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/BetweenTimesFilter.java @@ -33,7 +33,7 @@ ), @JinjavaParam(value = "unit", desc = "Which temporal unit to use", required = true) }, - snippets = { @JinjavaSnippet(code = "{% begin|between_times(end, 'hours') %}") } + snippets = { @JinjavaSnippet(code = "{{ begin|between_times(end, 'hours') }}") } ) public class BetweenTimesFilter extends BaseDateFilter { From 3043856c4c1696f6e9052ca631ac95f4a58ece27 Mon Sep 17 00:00:00 2001 From: harikrishna553 Date: Mon, 2 Jan 2023 19:10:15 +0530 Subject: [PATCH 117/637] Fix sort dictionaly by value logic --- .../jinjava/lib/filter/DictSortFilter.java | 2 +- .../lib/filter/DictSortFilterTest.java | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/hubspot/jinjava/lib/filter/DictSortFilterTest.java diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/DictSortFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/DictSortFilter.java index db36ea1bf..5f1f24f3a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/DictSortFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/DictSortFilter.java @@ -58,7 +58,7 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) boolean sortByKey = true; if (args.length > 1) { - sortByKey = "value".equalsIgnoreCase(args[1]); + sortByKey = !"value".equalsIgnoreCase(args[1]); } @SuppressWarnings("unchecked") diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/DictSortFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/DictSortFilterTest.java new file mode 100644 index 000000000..f29498f4d --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DictSortFilterTest.java @@ -0,0 +1,55 @@ +package com.hubspot.jinjava.lib.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.hubspot.jinjava.BaseJinjavaTest; + +public class DictSortFilterTest extends BaseJinjavaTest { + private static Map context; + + @BeforeClass + public static void initTemplate() { + + context = new HashMap<>(); + + Map countryCapitals = new HashMap<>(); + countryCapitals.put("Bhutan", "Thimpu"); + countryCapitals.put("Australia", "Canberra"); + countryCapitals.put("none", "none"); + countryCapitals.put("India", "New Delhi"); + countryCapitals.put("France", "Paris"); + + context.put("countryCapitals", countryCapitals); + + } + + @Test + public void sortByKeyCaseInsensitive() { + String template = "{% for key, value in countryCapitals|dictsort %}" + " {{key}},{{value}}" + " {% endfor %}"; + + String expected = " Australia,Canberra Bhutan,Thimpu France,Paris India,New Delhi none,none "; + + String actual = jinjava.render(template, context); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void sortByValueAndCaseInsensitive() { + String template = "{% for key, value in countryCapitals|dictsort(false,'value') %}" + " {{key}},{{value}}" + + " {% endfor %}"; + + String expected = " Australia,Canberra India,New Delhi none,none France,Paris Bhutan,Thimpu "; + + String actual = jinjava.render(template, context); + + assertThat(actual).isEqualTo(expected); + } + +} From ba5ff178b9275da21e39ca95761533a38be8f3ff Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 3 Jan 2023 15:52:56 -0500 Subject: [PATCH 118/637] Add null check to handle null lazy expressions --- .../util/EagerReconstructionUtils.java | 4 +++- .../util/EagerReconstructionUtilsTest.java | 23 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 0f724c3e7..f449d593f 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -167,7 +167,9 @@ private static Map getChangedValues( if (newValue == null) { continue; } - if (entry.getValue().equals(getObjectOrHashCode(newValue))) { + if ( + entry.getValue() != null && entry.getValue().equals(getObjectOrHashCode(newValue)) + ) { continue; } // New value different, changes. diff --git a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java index e79e3b564..d31479bdb 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java @@ -2,7 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyList; +import static org.mockito.Mockito.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -11,6 +15,7 @@ import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.LazyExpression; import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.lib.fn.MacroFunction; import com.hubspot.jinjava.lib.tag.eager.DeferredToken; @@ -388,6 +393,22 @@ public void itDiscardsSessionBindings() { assertThat(withoutSessionBindings.getSpeculativeBindings()).doesNotContainKey("foo"); } + @Test + public void itDoesNotBreakOnNullLazyExpressions() { + interpreter.getContext().put("foo", LazyExpression.of(() -> null, "")); + EagerReconstructionUtils.executeInChildContext( + eagerInterpreter -> + EagerExpressionResult.fromString(interpreter.render("{% set foo = 'bar' %}")), + interpreter, + EagerChildContextConfig + .newBuilder() + .withDiscardSessionBindings(false) + .withCheckForContextChanges(true) + .withTakeNewValue(true) + .build() + ); + } + private static MacroFunction getMockMacroFunction(String image) { MacroFunction mockMacroFunction = mock(MacroFunction.class); when(mockMacroFunction.getName()).thenReturn("foo"); From 9e8a99ffb27b480186e16cdb49bed4b1e428bae1 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 4 Jan 2023 14:09:53 -0500 Subject: [PATCH 119/637] Update new logic to handle null LazyExpressions --- .../jinjava/util/EagerContextWatcher.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java index 765a0bb5b..a528d3be9 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java @@ -141,15 +141,17 @@ private static Map getInitiallyResolvedHashes( Set> entrySet, Set metaContextVariables ) { - return entrySet + Map mapOfHashes = new HashMap<>(); + entrySet .stream() .filter(entry -> !metaContextVariables.contains(entry.getKey())) .filter( entry -> !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null ) - .collect( - Collectors.toMap(Entry::getKey, entry -> getObjectOrHashCode(entry.getValue())) - ); + .forEach( + entry -> mapOfHashes.put(entry.getKey(), getObjectOrHashCode(entry.getValue())) + ); // Avoid NPE when getObjectOrHashCode(entry.getValue()) is null) + return mapOfHashes; } private static Map getBasicSpeculativeBindings( @@ -258,9 +260,10 @@ private static Map getAllSpeculativeBindings( ) .filter( entry -> - !initiallyResolvedHashes - .get(entry.getKey()) - .equals(getObjectOrHashCode(entry.getValue())) + !Objects.equals( + initiallyResolvedHashes.get(entry.getKey()), + getObjectOrHashCode(entry.getValue()) + ) ) .collect( Collectors.toMap( From 597657441b32858f3b654ca83b1a23c3decfe999 Mon Sep 17 00:00:00 2001 From: harikrishna553 Date: Thu, 5 Jan 2023 15:02:46 +0530 Subject: [PATCH 120/637] Correct java doc for lower filter --- src/main/java/com/hubspot/jinjava/lib/filter/LowerFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/LowerFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/LowerFilter.java index d1b0babb9..b694bd8ad 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/LowerFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/LowerFilter.java @@ -23,7 +23,7 @@ @JinjavaDoc( value = "Convert a value to lowercase", input = @JinjavaParam(value = "s", desc = "String to make lowercase", required = true), - snippets = { @JinjavaSnippet(code = "{{ \"Text to MAKE Lowercase\"|lowercase }}") } + snippets = { @JinjavaSnippet(code = "{{ \"Text to MAKE Lowercase\"|lower }}") } ) public class LowerFilter implements Filter { From 0ee6bf442e8169e741d96315410e9ca57141fbf7 Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Thu, 5 Jan 2023 14:19:40 -0500 Subject: [PATCH 121/637] Unwrap wiresafe values --- pom.xml | 10 +++++++++ .../jinjava/el/ExpressionResolver.java | 5 +++++ .../el/JinjavaInterpreterResolver.java | 22 +++++++++++++++++++ .../jinjava/util/EagerContextWatcher.java | 6 +++++ 4 files changed, 43 insertions(+) diff --git a/pom.xml b/pom.xml index e6e1e6d4b..38f7b3dbb 100644 --- a/pom.xml +++ b/pom.xml @@ -146,6 +146,16 @@ ch.obermuhlner big-math + + com.hubspot.immutables + hubspot-style + 1.3-SNAPSHOT + + + com.hubspot.immutables + immutables-exceptions + 1.3-SNAPSHOT + ch.qos.logback diff --git a/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java b/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java index 3ddd44b7d..bb957f55e 100644 --- a/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java @@ -3,6 +3,7 @@ import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage; import com.google.common.collect.ImmutableMap; +import com.hubspot.immutables.utils.WireSafeEnum; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.el.ext.NamedParameter; import com.hubspot.jinjava.interpret.CollectionTooBigException; @@ -115,6 +116,10 @@ private Object resolveExpression(String expression, boolean addToResolvedExpress result = ((LazyExpression) result).get(); } + if (result instanceof WireSafeEnum) { + result = ((WireSafeEnum) result).asEnum().orElse(null); + } + validateResult(result); return result; diff --git a/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java b/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java index 285043d28..912ddd359 100644 --- a/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java @@ -3,6 +3,7 @@ import static com.hubspot.jinjava.util.Logging.ENGINE_LOG; import com.google.common.collect.ImmutableMap; +import com.hubspot.immutables.utils.WireSafeEnum; import com.hubspot.jinjava.el.ext.AbstractCallableMethod; import com.hubspot.jinjava.el.ext.DeferredParsingException; import com.hubspot.jinjava.el.ext.ExtendedParser; @@ -219,6 +220,13 @@ private Object getValue( } } + if (base instanceof WireSafeEnum) { + base = ((WireSafeEnum) base).asEnum().orElse(null); + if (base == null) { + return null; + } + } + // java doesn't natively support negative array indices, so the // super class getValue returns null for them. To make negative // indices work as they do in python, detect them here and convert @@ -256,6 +264,13 @@ private Object getValue( } } + if (value instanceof WireSafeEnum) { + value = ((WireSafeEnum) value).asEnum().orElse(null); + if (value == null) { + return null; + } + } + if (value instanceof DeferredValue) { if (interpreter.getConfig().getExecutionMode().useEagerParser()) { throw new DeferredParsingException(this, propertyName); @@ -316,6 +331,13 @@ Object wrap(Object value) { } } + if (value instanceof WireSafeEnum) { + value = ((WireSafeEnum) value).asEnum().orElse(null); + if (value == null) { + return null; + } + } + if (value instanceof PyWrapper) { return value; } diff --git a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java index a528d3be9..c27197e41 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.util; +import com.hubspot.immutables.utils.WireSafeEnum; import com.hubspot.jinjava.interpret.CannotReconstructValueException; import com.hubspot.jinjava.interpret.DeferredLazyReferenceSource; import com.hubspot.jinjava.interpret.DeferredValue; @@ -368,6 +369,11 @@ private static Object getObjectOrHashCode(Object o) { if (o instanceof LazyExpression) { o = ((LazyExpression) o).get(); } + + if (o instanceof WireSafeEnum) { + o = ((WireSafeEnum) o).asEnum().orElse(null); + } + if (o instanceof PyList && !((PyList) o).toList().contains(o)) { return o.hashCode(); } From 75795a1711fce2468637d3acef25f09623eea56f Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Thu, 5 Jan 2023 14:47:55 -0500 Subject: [PATCH 122/637] use config instead --- pom.xml | 10 --- .../com/hubspot/jinjava/JinjavaConfig.java | 16 +++++ .../jinjava/el/ExpressionResolver.java | 13 +--- .../el/JinjavaInterpreterResolver.java | 68 +++---------------- .../jinjava/el/JinjavaObjectUnwrapper.java | 25 +++++++ .../hubspot/jinjava/el/ObjectUnwrapper.java | 5 ++ .../jinjava/util/EagerContextWatcher.java | 5 -- 7 files changed, 60 insertions(+), 82 deletions(-) create mode 100644 src/main/java/com/hubspot/jinjava/el/JinjavaObjectUnwrapper.java create mode 100644 src/main/java/com/hubspot/jinjava/el/ObjectUnwrapper.java diff --git a/pom.xml b/pom.xml index 38f7b3dbb..e6e1e6d4b 100644 --- a/pom.xml +++ b/pom.xml @@ -146,16 +146,6 @@ ch.obermuhlner big-math - - com.hubspot.immutables - hubspot-style - 1.3-SNAPSHOT - - - com.hubspot.immutables - immutables-exceptions - 1.3-SNAPSHOT - ch.qos.logback diff --git a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java index ad527458c..ea0abea84 100644 --- a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java +++ b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.hubspot.jinjava.el.JinjavaInterpreterResolver; +import com.hubspot.jinjava.el.JinjavaObjectUnwrapper; +import com.hubspot.jinjava.el.ObjectUnwrapper; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.Context.Library; import com.hubspot.jinjava.interpret.InterpreterFactory; @@ -69,6 +71,8 @@ public class JinjavaConfig { private final boolean enablePreciseDivideFilter; private final ObjectMapper objectMapper; + private final ObjectUnwrapper objectUnwrapper; + public static Builder newBuilder() { return new Builder(); } @@ -123,6 +127,7 @@ private JinjavaConfig(Builder builder) { legacyOverrides = builder.legacyOverrides; enablePreciseDivideFilter = builder.enablePreciseDivideFilter; objectMapper = builder.objectMapper; + objectUnwrapper = builder.objectUnwrapper; } public Charset getCharset() { @@ -221,6 +226,10 @@ public ObjectMapper getObjectMapper() { return objectMapper; } + public ObjectUnwrapper getObjectUnwrapper() { + return objectUnwrapper; + } + /** * @deprecated Replaced by {@link LegacyOverrides#isIterateOverMapKeys()} */ @@ -272,6 +281,8 @@ public static class Builder { private boolean enablePreciseDivideFilter = false; private ObjectMapper objectMapper = new ObjectMapper(); + private ObjectUnwrapper objectUnwrapper = new JinjavaObjectUnwrapper(); + private Builder() {} public Builder withCharset(Charset charset) { @@ -427,6 +438,11 @@ public Builder withObjectMapper(ObjectMapper objectMapper) { return this; } + public Builder withObjectUnwrapper(ObjectUnwrapper objectUnwrapper) { + this.objectUnwrapper = objectUnwrapper; + return this; + } + public JinjavaConfig build() { return new JinjavaConfig(this); } diff --git a/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java b/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java index bb957f55e..b60ab9efd 100644 --- a/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java @@ -3,7 +3,6 @@ import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage; import com.google.common.collect.ImmutableMap; -import com.hubspot.immutables.utils.WireSafeEnum; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.el.ext.NamedParameter; import com.hubspot.jinjava.interpret.CollectionTooBigException; @@ -14,7 +13,6 @@ import com.hubspot.jinjava.interpret.InvalidArgumentException; import com.hubspot.jinjava.interpret.InvalidInputException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.LazyExpression; import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; @@ -41,6 +39,7 @@ public class ExpressionResolver { private final ExpressionFactory expressionFactory; private final JinjavaInterpreterResolver resolver; private final JinjavaELContext elContext; + private final ObjectUnwrapper objectUnwrapper; private static final String EXPRESSION_START_TOKEN = "#{"; private static final String EXPRESSION_END_TOKEN = "}"; @@ -57,6 +56,7 @@ public ExpressionResolver(JinjavaInterpreter interpreter, Jinjava jinjava) { for (ELFunctionDefinition fn : jinjava.getGlobalContext().getAllFunctions()) { this.elContext.setFunction(fn.getNamespace(), fn.getLocalName(), fn.getMethod()); } + objectUnwrapper = interpreter.getConfig().getObjectUnwrapper(); } /** @@ -111,14 +111,7 @@ private Object resolveExpression(String expression, boolean addToResolvedExpress ); } - // resolve the LazyExpression supplier automatically - if (result instanceof LazyExpression) { - result = ((LazyExpression) result).get(); - } - - if (result instanceof WireSafeEnum) { - result = ((WireSafeEnum) result).asEnum().orElse(null); - } + result = objectUnwrapper.unwrapObject(result); validateResult(result); diff --git a/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java b/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java index 912ddd359..3c81ca19a 100644 --- a/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java @@ -3,7 +3,6 @@ import static com.hubspot.jinjava.util.Logging.ENGINE_LOG; import com.google.common.collect.ImmutableMap; -import com.hubspot.immutables.utils.WireSafeEnum; import com.hubspot.jinjava.el.ext.AbstractCallableMethod; import com.hubspot.jinjava.el.ext.DeferredParsingException; import com.hubspot.jinjava.el.ext.ExtendedParser; @@ -14,7 +13,6 @@ import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.DisabledException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.LazyExpression; import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; @@ -41,7 +39,6 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.Optional; import javax.el.ArrayELResolver; import javax.el.CompositeELResolver; import javax.el.ELContext; @@ -75,10 +72,12 @@ public class JinjavaInterpreterResolver extends SimpleResolver { }; private final JinjavaInterpreter interpreter; + private final ObjectUnwrapper objectUnwrapper; public JinjavaInterpreterResolver(JinjavaInterpreter interpreter) { super(interpreter.getConfig().getElResolver()); this.interpreter = interpreter; + this.objectUnwrapper = interpreter.getConfig().getObjectUnwrapper(); } @Override @@ -204,27 +203,9 @@ private Object getValue( } else { // Get property of base object. try { - if (base instanceof Optional) { - Optional optBase = (Optional) base; - if (!optBase.isPresent()) { - return null; - } - - base = optBase.get(); - } - - if (base instanceof LazyExpression) { - base = ((LazyExpression) base).get(); - if (base == null) { - return null; - } - } - - if (base instanceof WireSafeEnum) { - base = ((WireSafeEnum) base).asEnum().orElse(null); - if (base == null) { - return null; - } + base = objectUnwrapper.unwrapObject(base); + if (base == null) { + return null; } // java doesn't natively support negative array indices, so the @@ -248,27 +229,9 @@ private Object getValue( value = super.getValue(context, base, propertyName); - if (value instanceof Optional) { - Optional optValue = (Optional) value; - if (!optValue.isPresent()) { - return null; - } - - value = optValue.get(); - } - - if (value instanceof LazyExpression) { - value = ((LazyExpression) value).get(); - if (value == null) { - return null; - } - } - - if (value instanceof WireSafeEnum) { - value = ((WireSafeEnum) value).asEnum().orElse(null); - if (value == null) { - return null; - } + value = objectUnwrapper.unwrapObject(value); + if (value == null) { + return null; } if (value instanceof DeferredValue) { @@ -324,18 +287,9 @@ Object wrap(Object value) { return value; } - if (value instanceof LazyExpression) { - value = ((LazyExpression) value).get(); - if (value == null) { - return null; - } - } - - if (value instanceof WireSafeEnum) { - value = ((WireSafeEnum) value).asEnum().orElse(null); - if (value == null) { - return null; - } + value = objectUnwrapper.unwrapObject(value); + if (value == null) { + return null; } if (value instanceof PyWrapper) { diff --git a/src/main/java/com/hubspot/jinjava/el/JinjavaObjectUnwrapper.java b/src/main/java/com/hubspot/jinjava/el/JinjavaObjectUnwrapper.java new file mode 100644 index 000000000..338dadeb7 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/JinjavaObjectUnwrapper.java @@ -0,0 +1,25 @@ +package com.hubspot.jinjava.el; + +import com.hubspot.jinjava.interpret.LazyExpression; +import java.util.Optional; + +public class JinjavaObjectUnwrapper implements ObjectUnwrapper { + + @Override + public Object unwrapObject(Object o) { + if (o instanceof LazyExpression) { + o = ((LazyExpression) o).get(); + } + + if (o instanceof Optional) { + Optional optValue = (Optional) o; + if (!optValue.isPresent()) { + return null; + } + + o = optValue.get(); + } + + return o; + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/ObjectUnwrapper.java b/src/main/java/com/hubspot/jinjava/el/ObjectUnwrapper.java new file mode 100644 index 000000000..2d04ad6ec --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ObjectUnwrapper.java @@ -0,0 +1,5 @@ +package com.hubspot.jinjava.el; + +public interface ObjectUnwrapper { + Object unwrapObject(Object o); +} diff --git a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java index c27197e41..b3e7c02b5 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java @@ -1,6 +1,5 @@ package com.hubspot.jinjava.util; -import com.hubspot.immutables.utils.WireSafeEnum; import com.hubspot.jinjava.interpret.CannotReconstructValueException; import com.hubspot.jinjava.interpret.DeferredLazyReferenceSource; import com.hubspot.jinjava.interpret.DeferredValue; @@ -370,10 +369,6 @@ private static Object getObjectOrHashCode(Object o) { o = ((LazyExpression) o).get(); } - if (o instanceof WireSafeEnum) { - o = ((WireSafeEnum) o).asEnum().orElse(null); - } - if (o instanceof PyList && !((PyList) o).toList().contains(o)) { return o.hashCode(); } From ad87f01411265ca52349f8a9fe22cd1f6bb6a822 Mon Sep 17 00:00:00 2001 From: Anthony Pizzurro Date: Fri, 6 Jan 2023 17:09:09 -0500 Subject: [PATCH 123/637] update textmate snippets --- src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java | 6 ++---- src/main/java/com/hubspot/jinjava/lib/tag/ExtendsTag.java | 2 ++ src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java | 2 +- src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java | 6 ++---- src/main/java/com/hubspot/jinjava/lib/tag/IncludeTag.java | 2 ++ src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java | 6 ++---- src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java | 6 ++---- 7 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java index de780d180..3a8050e62 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java @@ -15,10 +15,7 @@ **********************************************************************/ package com.hubspot.jinjava.lib.tag; -import com.hubspot.jinjava.doc.annotations.JinjavaDoc; -import com.hubspot.jinjava.doc.annotations.JinjavaHasCodeBody; -import com.hubspot.jinjava.doc.annotations.JinjavaParam; -import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.doc.annotations.*; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.tree.TagNode; @@ -50,6 +47,7 @@ } ) @JinjavaHasCodeBody +@JinjavaTextMateSnippet(code = "{% block ${1:name} %}\n$0\n{% endblock $1 %}") public class BlockTag implements Tag { public static final String TAG_NAME = "block"; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/ExtendsTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/ExtendsTag.java index 447c31652..ab9a14fb0 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/ExtendsTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/ExtendsTag.java @@ -18,6 +18,7 @@ import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; @@ -78,6 +79,7 @@ ) } ) +@JinjavaTextMateSnippet(code = "{% extends '${1:path}' %}") public class ExtendsTag implements Tag { public static final String TAG_NAME = "extends"; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java index 82b968b1c..34631a01a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java @@ -90,7 +90,7 @@ ) @JinjavaHasCodeBody @JinjavaTextMateSnippet( - code = "{% for ${1:items} in ${2:list} %}\n" + "{{ ${1} }}$0\n" + "{% endfor %}" + code = "{% for ${1:items} in ${2:list} %}\n" + "$0\n" + "{% endfor %}" ) public class ForTag implements Tag { public static final String TAG_NAME = "for"; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java index a44f59a62..f3ca66e88 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java @@ -15,10 +15,7 @@ **********************************************************************/ package com.hubspot.jinjava.lib.tag; -import com.hubspot.jinjava.doc.annotations.JinjavaDoc; -import com.hubspot.jinjava.doc.annotations.JinjavaHasCodeBody; -import com.hubspot.jinjava.doc.annotations.JinjavaParam; -import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.doc.annotations.*; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.interpret.TemplateError; @@ -58,6 +55,7 @@ ) } ) +@JinjavaTextMateSnippet(code = "{% if '${1:condition}' %}\n\n{% endif %}") @JinjavaHasCodeBody public class IfTag implements Tag { public static final String TAG_NAME = "if"; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/IncludeTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/IncludeTag.java index d8b985c98..d4028898a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/IncludeTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/IncludeTag.java @@ -19,6 +19,7 @@ import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet; import com.hubspot.jinjava.interpret.IncludeTagCycleException; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; @@ -48,6 +49,7 @@ @JinjavaSnippet(code = "{% include \"hubspot/styles/patches/recommended.css\" %}") } ) +@JinjavaTextMateSnippet(code = "{% include '${1:path}' %}") public class IncludeTag implements Tag { public static final String TAG_NAME = "include"; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java index dcf22e60e..05ac9af8d 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java @@ -3,10 +3,7 @@ import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Lists; -import com.hubspot.jinjava.doc.annotations.JinjavaDoc; -import com.hubspot.jinjava.doc.annotations.JinjavaHasCodeBody; -import com.hubspot.jinjava.doc.annotations.JinjavaParam; -import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.doc.annotations.*; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.DeferredValueException; @@ -57,6 +54,7 @@ } ) @JinjavaHasCodeBody +@JinjavaTextMateSnippet(code = "{% macro ${1:name}(${2:values) %}\n\t$0\n{% endmacro %}") public class MacroTag implements Tag { public static final String TAG_NAME = "macro"; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java index fa8b3cdd0..24fa0d89c 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java @@ -1,9 +1,6 @@ package com.hubspot.jinjava.lib.tag; -import com.hubspot.jinjava.doc.annotations.JinjavaDoc; -import com.hubspot.jinjava.doc.annotations.JinjavaHasCodeBody; -import com.hubspot.jinjava.doc.annotations.JinjavaParam; -import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.doc.annotations.*; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.util.ObjectTruthValue; @@ -29,6 +26,7 @@ ) ) @JinjavaHasCodeBody +@JinjavaTextMateSnippet(code = "{% unless ${1:condition} %}\n\t$0\n{% endunless %}") public class UnlessTag extends IfTag { public static final String TAG_NAME = "unless"; From bbd393f66453e4cc95049c02264a5a65e9c37337 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 9 Jan 2023 11:18:35 -0500 Subject: [PATCH 124/637] Add test showing bug where value is reconstructed extra times --- src/test/java/com/hubspot/jinjava/EagerTest.java | 5 +++++ .../eager/does-not-reconstruct-extra-times.expected.jinja | 6 ++++++ .../resources/eager/does-not-reconstruct-extra-times.jinja | 7 +++++++ 3 files changed, 18 insertions(+) create mode 100644 src/test/resources/eager/does-not-reconstruct-extra-times.expected.jinja create mode 100644 src/test/resources/eager/does-not-reconstruct-extra-times.jinja diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index b4baa9c2a..37914d4e2 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1245,4 +1245,9 @@ public void itReconstructsWordsFromInsideNestedExpressionsSecondPass() { "reconstructs-words-from-inside-nested-expressions.expected" ); } + + @Test + public void itDoesNotReconstructExtraTimes() { + expectedTemplateInterpreter.assertExpectedOutput("does-not-reconstruct-extra-times"); + } } diff --git a/src/test/resources/eager/does-not-reconstruct-extra-times.expected.jinja b/src/test/resources/eager/does-not-reconstruct-extra-times.expected.jinja new file mode 100644 index 000000000..fe5dd7fe1 --- /dev/null +++ b/src/test/resources/eager/does-not-reconstruct-extra-times.expected.jinja @@ -0,0 +1,6 @@ +{% set foo = deferred %} + +{% if deferred %} +{% set foo = 'second' %} +{% endif %} +{{ foo }} \ No newline at end of file diff --git a/src/test/resources/eager/does-not-reconstruct-extra-times.jinja b/src/test/resources/eager/does-not-reconstruct-extra-times.jinja new file mode 100644 index 000000000..02299d6a2 --- /dev/null +++ b/src/test/resources/eager/does-not-reconstruct-extra-times.jinja @@ -0,0 +1,7 @@ +{% set foo = 'first' %} +{% set foo = deferred %} + +{% if deferred %} +{% set foo = 'second' %} +{% endif %} +{{ foo }} From 161383a85aa6c0b09d29b92ec834e751a2e36f0d Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 9 Jan 2023 14:30:16 -0500 Subject: [PATCH 125/637] Don't double-reconstruct already deferred keys before running function in deferred execution mode --- .../jinjava/util/EagerContextWatcher.java | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java index a528d3be9..14fa1ac1f 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java @@ -73,12 +73,17 @@ public static EagerExecutionResult executeInChildContext( initialResult.getSpeculativeBindings() ); } else { + Set ignoredKeys = getKeysToIgnore( + interpreter, + metaContextVariables, + eagerChildContextConfig + ); initialResult = applyFunction(function, interpreter, eagerChildContextConfig); speculativeBindings = getBasicSpeculativeBindings( interpreter, eagerChildContextConfig, - metaContextVariables, + ignoredKeys, initialResult.getSpeculativeBindings() ); } @@ -154,10 +159,35 @@ private static Map getInitiallyResolvedHashes( return mapOfHashes; } + private static Set getKeysToIgnore( + JinjavaInterpreter interpreter, + Set metaContextVariables, + EagerChildContextConfig eagerChildContextConfig + ) { + // We don't need to reconstruct already deferred keys. + // This ternary expression is an optimization to call entrySet fewer times + return ( + interpreter.getContext().isDeferredExecutionMode() && + !eagerChildContextConfig.takeNewValue + ) + ? Stream + .concat( + metaContextVariables.stream(), + interpreter + .getContext() + .entrySet() + .stream() + .filter(entry -> entry.getValue() instanceof DeferredValue) + .map(Entry::getKey) + ) + .collect(Collectors.toSet()) + : metaContextVariables; + } + private static Map getBasicSpeculativeBindings( JinjavaInterpreter interpreter, EagerChildContextConfig eagerChildContextConfig, - Set metaContextVariables, + Set ignoredKeys, Map speculativeBindings ) { speculativeBindings.putAll( @@ -184,7 +214,7 @@ private static Map getBasicSpeculativeBindings( return speculativeBindings .entrySet() .stream() - .filter(entry -> !metaContextVariables.contains(entry.getKey())) + .filter(entry -> !ignoredKeys.contains(entry.getKey())) .filter(entry -> !"loop".equals(entry.getKey())) .map( entry -> { From b8e4e2cdfc9c0a72596508570e582de0a50ce1a6 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 9 Jan 2023 14:32:37 -0500 Subject: [PATCH 126/637] Improve cases that new test covers --- ...not-reconstruct-extra-times.expected.jinja | 19 ++++++++++++++++++- .../does-not-reconstruct-extra-times.jinja | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/test/resources/eager/does-not-reconstruct-extra-times.expected.jinja b/src/test/resources/eager/does-not-reconstruct-extra-times.expected.jinja index fe5dd7fe1..eddbba409 100644 --- a/src/test/resources/eager/does-not-reconstruct-extra-times.expected.jinja +++ b/src/test/resources/eager/does-not-reconstruct-extra-times.expected.jinja @@ -1,6 +1,23 @@ +{% for __ignored__ in [0] %} + {% set foo = deferred %} +{% endfor %} + + +{% set foo = deferred %} + + +{% for __ignored__ in [0] %} +{% if deferred %} +{{ foo }} +{% set foo = 'second' %} +{% endif %} +{{ foo }} +{% endfor %} +{{ foo }} + {% if deferred %} {% set foo = 'second' %} {% endif %} -{{ foo }} \ No newline at end of file +{{ foo }} diff --git a/src/test/resources/eager/does-not-reconstruct-extra-times.jinja b/src/test/resources/eager/does-not-reconstruct-extra-times.jinja index 02299d6a2..29e3a601a 100644 --- a/src/test/resources/eager/does-not-reconstruct-extra-times.jinja +++ b/src/test/resources/eager/does-not-reconstruct-extra-times.jinja @@ -1,7 +1,25 @@ {% set foo = 'first' %} +{% for i in range(1) %} +{# this should do nothing because it's in a for loop #} {% set foo = deferred %} +{% endfor %} +{# actually defer foo #} +{% set foo = deferred %} + +{# make sure we don't reconstruct foo = 'first' in front of the for block #} +{% for i in range(1) %} +{% if deferred %} +{{ foo }} +{% set foo = 'second' %} +{% endif %} +{{ foo }} +{% endfor %} +{{ foo }} + +{# make sure we don't reconstruct foo = 'first' in front of the if block #} {% if deferred %} {% set foo = 'second' %} {% endif %} {{ foo }} + From 4e3ad098afc087ae579980bd809389b363e7316b Mon Sep 17 00:00:00 2001 From: Anthony Pizzurro Date: Mon, 9 Jan 2023 15:20:54 -0500 Subject: [PATCH 127/637] remove wildcard imports --- src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java | 5 ++++- src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java | 5 ++++- src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java | 5 ++++- src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java index 3a8050e62..e4b897971 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java @@ -15,7 +15,10 @@ **********************************************************************/ package com.hubspot.jinjava.lib.tag; -import com.hubspot.jinjava.doc.annotations.*; +import com.hubspot.jinjava.doc.annotations.JinjavaHasCodeBody; +import com.hubspot.jinjava.doc.annotations.JinjavaParam; +import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.tree.TagNode; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java index f3ca66e88..3df349311 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java @@ -15,7 +15,10 @@ **********************************************************************/ package com.hubspot.jinjava.lib.tag; -import com.hubspot.jinjava.doc.annotations.*; +import com.hubspot.jinjava.doc.annotations.JinjavaHasCodeBody; +import com.hubspot.jinjava.doc.annotations.JinjavaParam; +import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.interpret.TemplateError; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java index 05ac9af8d..3913f4144 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java @@ -3,7 +3,10 @@ import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Lists; -import com.hubspot.jinjava.doc.annotations.*; +import com.hubspot.jinjava.doc.annotations.JinjavaHasCodeBody; +import com.hubspot.jinjava.doc.annotations.JinjavaParam; +import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.DeferredValueException; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java index 24fa0d89c..31fede9c3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java @@ -1,6 +1,9 @@ package com.hubspot.jinjava.lib.tag; -import com.hubspot.jinjava.doc.annotations.*; +import com.hubspot.jinjava.doc.annotations.JinjavaHasCodeBody; +import com.hubspot.jinjava.doc.annotations.JinjavaParam; +import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.util.ObjectTruthValue; From 2e8dcd67cad9a570c69822d9a939436d2c9a0a22 Mon Sep 17 00:00:00 2001 From: Anthony Pizzurro Date: Mon, 9 Jan 2023 15:27:16 -0500 Subject: [PATCH 128/637] Forogt to add one previously removed imports --- src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java | 1 + src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java | 1 + src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java | 1 + src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java | 1 + 4 files changed, 4 insertions(+) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java index e4b897971..fe525acc0 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java @@ -15,6 +15,7 @@ **********************************************************************/ package com.hubspot.jinjava.lib.tag; +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaHasCodeBody; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java index 3df349311..96e6f8cc8 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java @@ -15,6 +15,7 @@ **********************************************************************/ package com.hubspot.jinjava.lib.tag; +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaHasCodeBody; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java index 3913f4144..3b03e58b9 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java @@ -3,6 +3,7 @@ import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Lists; +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaHasCodeBody; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java index 31fede9c3..9c49bc077 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.lib.tag; +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaHasCodeBody; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; From 8a28093162d1b9e4a9ee0dc62b7634b128114e0b Mon Sep 17 00:00:00 2001 From: Anthony Pizzurro Date: Tue, 10 Jan 2023 11:19:13 -0500 Subject: [PATCH 129/637] Make sure hidden tags are filtered out and fix snippet param structure --- .../jinjava/doc/JinjavaDocFactory.java | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocFactory.java b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocFactory.java index a937d7afd..0534a3890 100644 --- a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocFactory.java +++ b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocFactory.java @@ -63,7 +63,31 @@ private void addCodeSnippets(JinjavaDoc doc) { if (tag instanceof EndTag) { continue; } - doc.addCodeSnippet(tag.getName(), getTagSnippet(tag)); + com.hubspot.jinjava.doc.annotations.JinjavaDoc docAnnotation = getJinjavaDocAnnotation( + tag.getClass() + ); + + if (docAnnotation == null) { + LOG.warn( + "Expression Test {} doesn't have a @{} annotation", + tag.getName(), + JINJAVA_DOC_CLASS.getName() + ); + doc.addExpTest( + new JinjavaDocExpTest( + tag.getName(), + "", + "", + false, + new JinjavaDocParam[] {}, + new JinjavaDocParam[] {}, + new JinjavaDocSnippet[] {}, + Collections.emptyMap() + ) + ); + } else if (!docAnnotation.hidden()) { + doc.addCodeSnippet(tag.getName(), getTagSnippet(tag)); + } } } @@ -330,7 +354,7 @@ private String getTagSnippet(Tag tag) { } for (JinjavaParam param : docAnnotation.params()) { - String paramValue = "${" + i + ":" + param.value() + "}"; + String paramValue = param.value() + "=\"${" + i + ":" + param.value() + "}\""; if (param.value().equalsIgnoreCase("path")) { paramValue = "'" + paramValue + "'"; } else if (param.value().equalsIgnoreCase("argument_names")) { From 1f3ce5fd768789e43c26861bf2c02d0329910dd8 Mon Sep 17 00:00:00 2001 From: Anthony Pizzurro Date: Tue, 10 Jan 2023 14:12:20 -0500 Subject: [PATCH 130/637] add correct empty doc for tags --- .../java/com/hubspot/jinjava/doc/JinjavaDocFactory.java | 7 ++++--- src/main/java/com/hubspot/jinjava/lib/tag/CycleTag.java | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocFactory.java b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocFactory.java index 0534a3890..6436717ab 100644 --- a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocFactory.java +++ b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocFactory.java @@ -69,13 +69,14 @@ private void addCodeSnippets(JinjavaDoc doc) { if (docAnnotation == null) { LOG.warn( - "Expression Test {} doesn't have a @{} annotation", + "Tag {} doesn't have a @{} annotation", tag.getName(), JINJAVA_DOC_CLASS.getName() ); - doc.addExpTest( - new JinjavaDocExpTest( + doc.addTag( + new JinjavaDocTag( tag.getName(), + StringUtils.isBlank(tag.getEndTagName()), "", "", false, diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/CycleTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/CycleTag.java index ef674ff0f..75bb5fc09 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/CycleTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/CycleTag.java @@ -18,6 +18,7 @@ import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.tree.TagNode; @@ -48,6 +49,7 @@ ) } ) +@JinjavaTextMateSnippet(code = "{% cycle '${1:string_to_print}' %}") public class CycleTag implements Tag { public static final String TAG_NAME = "cycle"; public static final String LOOP_INDEX = "loop.index0"; From 1f75ad21349d43f9286eebc7aba1b40a5d40ed44 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 10 Jan 2023 16:38:01 -0500 Subject: [PATCH 131/637] Disallow nested tags that are broken across multiple quote blocks --- .../jinjava/util/EagerExpressionResolver.java | 46 +++++++++++++++---- .../util/EagerExpressionResolverTest.java | 7 +++ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java index be56e4740..153dcafba 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java @@ -129,6 +129,7 @@ private static Set findDeferredWords( boolean throwInterpreterErrorsStart = interpreter .getContext() .getThrowInterpreterErrors(); + FoundQuotedExpressionTags foundQuotedExpressionTags = new FoundQuotedExpressionTags(); try { interpreter.getContext().setThrowInterpreterErrors(true); Set words = new HashSet<>(); @@ -148,7 +149,9 @@ private static Set findDeferredWords( interpreter, scannerSymbols, words, - partiallyResolved.substring(prevQuotePos, curPos + 1) + partiallyResolved.substring(prevQuotePos, curPos + 1), + prevQuotePos, + foundQuotedExpressionTags ); } inQuote = false; @@ -178,6 +181,12 @@ private static Set findDeferredWords( interpreter ) ); + + if (foundQuotedExpressionTags.fullTagMayExist()) { + throw new DeferredValueException( + "Cannot get words inside nested interpretation tags" + ); + } return words; } finally { interpreter.getContext().setThrowInterpreterErrors(throwInterpreterErrorsStart); @@ -188,15 +197,21 @@ private static void getDeferredWordsInsideNestedExpression( JinjavaInterpreter interpreter, TokenScannerSymbols scannerSymbols, Set words, - String quoted + String quoted, + int offset, + FoundQuotedExpressionTags foundQuotedExpressionTags ) { - if ( - quoted.contains(scannerSymbols.getExpressionStartWithTag()) && - quoted.contains(scannerSymbols.getExpressionEndWithTag()) - ) { - throw new DeferredValueException( - "Cannot get words inside nested interpretation tags" - ); + if (foundQuotedExpressionTags.firstStartTagFoundLocation == null) { + int startWithIndex = quoted.indexOf(scannerSymbols.getExpressionStartWithTag()); + if (startWithIndex >= 0) { + foundQuotedExpressionTags.firstStartTagFoundLocation = startWithIndex + offset; + } + } + if (foundQuotedExpressionTags.firstStartTagFoundLocation != null) { + int endWithIndex = quoted.indexOf(scannerSymbols.getExpressionEndWithTag()); + if (endWithIndex >= 0) { + foundQuotedExpressionTags.lastEndTagFoundLocation = endWithIndex + offset; + } } if ( @@ -477,4 +492,17 @@ public enum ResolutionState { } } } + + private static class FoundQuotedExpressionTags { + Integer firstStartTagFoundLocation; + Integer lastEndTagFoundLocation; + + boolean fullTagMayExist() { + return ( + firstStartTagFoundLocation != null && + lastEndTagFoundLocation != null && + firstStartTagFoundLocation < lastEndTagFoundLocation + ); + } + } } diff --git a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java index be2824921..eb3c63c2a 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java @@ -682,6 +682,13 @@ public void itFailsWhenThereIsANestedTag() { eagerResolveExpression("deferred.append('{% do foo.append(bar) %}')"); } + @Test(expected = DeferredValueException.class) + public void itFailsWhenThereIsANestedTagFromSeparateQuoteBlocks() { + eagerResolveExpression( + "\"{% import '../../settings/localization/\" + deferred + \"-website.html' as config %}\"" + ); + } + @Test public void itHandlesDeferredNamedParameter() { context.put("foo", "foo"); From 37b91c2c6b4a56ea80eff2df555b37bfa8d8eedb Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 11 Jan 2023 16:59:44 -0500 Subject: [PATCH 132/637] Preserve identifiers when setting values and reconstructing that tag --- .../el/ext/eager/EagerAstIdentifier.java | 23 ++++++++- .../hubspot/jinjava/interpret/Context.java | 23 +++++++++ .../tag/eager/EagerBlockSetTagStrategy.java | 49 ++++++++++-------- .../jinjava/lib/tag/eager/EagerForTag.java | 50 +++++++++++-------- .../tag/eager/EagerInlineSetTagStrategy.java | 25 ++++++---- 5 files changed, 117 insertions(+), 53 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java index a7b631e4a..443cc4c49 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java @@ -1,6 +1,11 @@ package com.hubspot.jinjava.el.ext.eager; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.ExtendedParser; +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.filter.Filter; +import com.hubspot.jinjava.util.EagerExpressionResolver; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstIdentifier; import javax.el.ELContext; @@ -16,7 +21,23 @@ public EagerAstIdentifier(String name, int index, boolean ignoreReturnType) { @Override public Object eval(Bindings bindings, ELContext context) { return EvalResultHolder.super.eval( - () -> super.eval(bindings, context), + () -> { + Object result = super.eval(bindings, context); + if ( + !ExtendedParser.INTERPRETER.equals(getName()) && + !EagerExpressionResolver.isPrimitive(result) && + !(result instanceof Filter) && + ( + (JinjavaInterpreter) context + .getELResolver() + .getValue(context, null, ExtendedParser.INTERPRETER) + ).getContext() + .isPreserveAllIdentifiers() + ) { + throw new DeferredValueException("Preserving all non-primitive identifiers"); + } + return result; + }, bindings, context ); diff --git a/src/main/java/com/hubspot/jinjava/interpret/Context.java b/src/main/java/com/hubspot/jinjava/interpret/Context.java index 8e4d2d30d..bdbe61120 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/Context.java +++ b/src/main/java/com/hubspot/jinjava/interpret/Context.java @@ -107,6 +107,8 @@ public enum Library { private boolean validationMode = false; private boolean deferredExecutionMode = false; private boolean deferLargeObjects = false; + + private boolean preserveAllIdentifiers = false; private boolean throwInterpreterErrors = false; private boolean partialMacroEvaluation = false; private boolean unwrapRawOverride = false; @@ -211,6 +213,7 @@ public Context( this.dynamicVariableResolver = parent.dynamicVariableResolver; this.deferredExecutionMode = parent.deferredExecutionMode; this.deferLargeObjects = parent.deferLargeObjects; + this.preserveAllIdentifiers = parent.preserveAllIdentifiers; this.throwInterpreterErrors = parent.throwInterpreterErrors; } } @@ -729,6 +732,26 @@ public TemporaryValueClosable withDeferLargeObjects( return temporaryValueClosable; } + public boolean isPreserveAllIdentifiers() { + return preserveAllIdentifiers; + } + + public Context setPreserveAllIdentifiers(boolean preserveAllIdentifiers) { + this.preserveAllIdentifiers = preserveAllIdentifiers; + return this; + } + + public TemporaryValueClosable withPreserveAllIdentifiers( + boolean preserveAllIdentifiers + ) { + TemporaryValueClosable temporaryValueClosable = new TemporaryValueClosable<>( + this.preserveAllIdentifiers, + this::setPreserveAllIdentifiers + ); + this.preserveAllIdentifiers = preserveAllIdentifiers; + return temporaryValueClosable; + } + public boolean getThrowInterpreterErrors() { return throwInterpreterErrors; } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java index 25337c4f2..ade4dab17 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.lib.tag.eager; import com.google.common.collect.Sets; +import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.SetTag; @@ -31,28 +32,34 @@ protected EagerExecutionResult getEagerExecutionResult( String expression, JinjavaInterpreter interpreter ) { - EagerExecutionResult result = EagerContextWatcher.executeInChildContext( - eagerInterpreter -> - EagerExpressionResult.fromSupplier( - () -> { - StringBuilder sb = new StringBuilder(); - for (Node child : tagNode.getChildren()) { - sb.append(child.render(eagerInterpreter).getValue()); - } - return sb.toString(); - }, - eagerInterpreter - ), - interpreter, - EagerContextWatcher - .EagerChildContextConfig.newBuilder() - .withTakeNewValue(true) - .build() - ); - if (result.getResult().getResolutionState() == ResolutionState.NONE) { - throw new DeferredValueException(result.getResult().toString()); + try ( + TemporaryValueClosable c = interpreter + .getContext() + .withPreserveAllIdentifiers(interpreter.getContext().isDeferredExecutionMode()) + ) { + EagerExecutionResult result = EagerContextWatcher.executeInChildContext( + eagerInterpreter -> + EagerExpressionResult.fromSupplier( + () -> { + StringBuilder sb = new StringBuilder(); + for (Node child : tagNode.getChildren()) { + sb.append(child.render(eagerInterpreter).getValue()); + } + return sb.toString(); + }, + eagerInterpreter + ), + interpreter, + EagerContextWatcher + .EagerChildContextConfig.newBuilder() + .withTakeNewValue(true) + .build() + ); + if (result.getResult().getResolutionState() == ResolutionState.NONE) { + throw new DeferredValueException(result.getResult().toString()); + } + return result; } - return result; } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 13291b73a..06dd16084 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -98,28 +98,34 @@ public String eagerInterpret( interpreter.getContext().isDeferLargeObjects() ) ) { - // separate getEagerImage from renderChildren because the token gets evaluated once - // while the children are evaluated 0...n times. - result.append( - EagerContextWatcher - .executeInChildContext( - eagerInterpreter -> - EagerExpressionResult.fromString( - getEagerImage( - buildToken( - tagNode, - e, - interpreter.getLineNumber(), - interpreter.getPosition() - ), - eagerInterpreter - ) - ), - interpreter, - EagerContextWatcher.EagerChildContextConfig.newBuilder().build() - ) - .asTemplateString() - ); + try ( + TemporaryValueClosable c1 = interpreter + .getContext() + .withPreserveAllIdentifiers(true) + ) { + // separate getEagerImage from renderChildren because the token gets evaluated once + // while the children are evaluated 0...n times. + result.append( + EagerContextWatcher + .executeInChildContext( + eagerInterpreter -> + EagerExpressionResult.fromString( + getEagerImage( + buildToken( + tagNode, + e, + interpreter.getLineNumber(), + interpreter.getPosition() + ), + eagerInterpreter + ) + ), + interpreter, + EagerContextWatcher.EagerChildContextConfig.newBuilder().build() + ) + .asTemplateString() + ); + } } EagerExecutionResult firstRunResult = runLoopOnce(tagNode, interpreter); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java index 9b786a84d..9526dcacc 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; @@ -31,15 +32,21 @@ public EagerExecutionResult getEagerExecutionResult( String expression, JinjavaInterpreter interpreter ) { - return EagerContextWatcher.executeInChildContext( - eagerInterpreter -> - EagerExpressionResolver.resolveExpression('[' + expression + ']', interpreter), - interpreter, - EagerContextWatcher - .EagerChildContextConfig.newBuilder() - .withTakeNewValue(true) - .build() - ); + try ( + TemporaryValueClosable c = interpreter + .getContext() + .withPreserveAllIdentifiers(interpreter.getContext().isDeferredExecutionMode()) + ) { + return EagerContextWatcher.executeInChildContext( + eagerInterpreter -> + EagerExpressionResolver.resolveExpression('[' + expression + ']', interpreter), + interpreter, + EagerContextWatcher + .EagerChildContextConfig.newBuilder() + .withTakeNewValue(true) + .build() + ); + } } @Override From 5c3355204b4f2aff09b2acfb4ce18ebd1f1dfc4d Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 11 Jan 2023 17:17:47 -0500 Subject: [PATCH 133/637] Add tests for preserving identifiers in set tag and for tag --- .../tag/eager/EagerBlockSetTagStrategy.java | 49 ++++++++----------- .../tag/eager/EagerInlineSetTagStrategy.java | 2 +- .../java/com/hubspot/jinjava/EagerTest.java | 15 ++++++ ...ves-identifiers-in-for-loop.expected.jinja | 8 +++ ...ly-preserves-identifiers-in-for-loop.jinja | 10 ++++ ...s-identifiers-in-inline-set.expected.jinja | 5 ++ ...-preserves-identifiers-in-inline-set.jinja | 7 +++ 7 files changed, 67 insertions(+), 29 deletions(-) create mode 100644 src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.expected.jinja create mode 100644 src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.jinja create mode 100644 src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.expected.jinja create mode 100644 src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.jinja diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java index ade4dab17..25337c4f2 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java @@ -1,7 +1,6 @@ package com.hubspot.jinjava.lib.tag.eager; import com.google.common.collect.Sets; -import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.SetTag; @@ -32,34 +31,28 @@ protected EagerExecutionResult getEagerExecutionResult( String expression, JinjavaInterpreter interpreter ) { - try ( - TemporaryValueClosable c = interpreter - .getContext() - .withPreserveAllIdentifiers(interpreter.getContext().isDeferredExecutionMode()) - ) { - EagerExecutionResult result = EagerContextWatcher.executeInChildContext( - eagerInterpreter -> - EagerExpressionResult.fromSupplier( - () -> { - StringBuilder sb = new StringBuilder(); - for (Node child : tagNode.getChildren()) { - sb.append(child.render(eagerInterpreter).getValue()); - } - return sb.toString(); - }, - eagerInterpreter - ), - interpreter, - EagerContextWatcher - .EagerChildContextConfig.newBuilder() - .withTakeNewValue(true) - .build() - ); - if (result.getResult().getResolutionState() == ResolutionState.NONE) { - throw new DeferredValueException(result.getResult().toString()); - } - return result; + EagerExecutionResult result = EagerContextWatcher.executeInChildContext( + eagerInterpreter -> + EagerExpressionResult.fromSupplier( + () -> { + StringBuilder sb = new StringBuilder(); + for (Node child : tagNode.getChildren()) { + sb.append(child.render(eagerInterpreter).getValue()); + } + return sb.toString(); + }, + eagerInterpreter + ), + interpreter, + EagerContextWatcher + .EagerChildContextConfig.newBuilder() + .withTakeNewValue(true) + .build() + ); + if (result.getResult().getResolutionState() == ResolutionState.NONE) { + throw new DeferredValueException(result.getResult().toString()); } + return result; } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java index 9526dcacc..593f6b518 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java @@ -35,7 +35,7 @@ public EagerExecutionResult getEagerExecutionResult( try ( TemporaryValueClosable c = interpreter .getContext() - .withPreserveAllIdentifiers(interpreter.getContext().isDeferredExecutionMode()) + .withPreserveAllIdentifiers(true) ) { return EagerContextWatcher.executeInChildContext( eagerInterpreter -> diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 37914d4e2..6194decfa 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1250,4 +1250,19 @@ public void itReconstructsWordsFromInsideNestedExpressionsSecondPass() { public void itDoesNotReconstructExtraTimes() { expectedTemplateInterpreter.assertExpectedOutput("does-not-reconstruct-extra-times"); } + + @Test + public void itCorrectlyPreservesIdentifiersInInlineSet() { + // This doesn't need to happen in block sets, because they only store string values + expectedTemplateInterpreter.assertExpectedOutput( + "correctly-preserves-identifiers-in-inline-set" + ); + } + + @Test + public void itCorrectlyPreservesIdentifiersInForLoop() { + expectedTemplateInterpreter.assertExpectedOutput( + "correctly-preserves-identifiers-in-for-loop" + ); + } } diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.expected.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.expected.jinja new file mode 100644 index 000000000..83aa2c074 --- /dev/null +++ b/src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.expected.jinja @@ -0,0 +1,8 @@ +{% set dict = {} %}{% set dict = {} %}{% for item in [dict] %} +{% if deferred %} +{% do item.update({'c': 'd'} ) %} +{% endif %} +{{ item }} +{% endfor %} + +{% do dict.update({'a': 'b'} ) %} diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.jinja new file mode 100644 index 000000000..061461d8b --- /dev/null +++ b/src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.jinja @@ -0,0 +1,10 @@ +{% set dict = {} %} + +{% for item in [dict] %} +{% if deferred %} +{% do item.update({'c': 'd'}) %} +{% endif %} +{{ item }} +{% endfor %} + +{% do dict.update({'a': 'b'}) %} diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.expected.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.expected.jinja new file mode 100644 index 000000000..78328bf02 --- /dev/null +++ b/src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.expected.jinja @@ -0,0 +1,5 @@ +{% set dict = {} %}{% set foo = (dict, deferred) %} + +{% do dict.update({'a': 'b'} ) %} + +{{ foo }} diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.jinja new file mode 100644 index 000000000..5e11bf14e --- /dev/null +++ b/src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.jinja @@ -0,0 +1,7 @@ +{% set dict = {} %} + +{% set foo = (dict, deferred) %} + +{% do dict.update({'a': 'b'}) %} + +{{ foo }} From fa3997c0d7934f3845f3e310302b82445a3d72ce Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 11 Jan 2023 17:37:31 -0500 Subject: [PATCH 134/637] Only preserve identifiers when reconstructing set tags --- .../tag/eager/EagerBlockSetTagStrategy.java | 10 +++++++ .../tag/eager/EagerInlineSetTagStrategy.java | 30 +++++++++++++++++-- .../lib/tag/eager/EagerSetTagStrategy.java | 14 +++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java index 25337c4f2..0b9ca7653 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java @@ -55,6 +55,16 @@ protected EagerExecutionResult getEagerExecutionResult( return result; } + @Override + protected EagerExecutionResult getDeferredEagerExecutionResult( + TagNode tagNode, + String expression, + JinjavaInterpreter interpreter, + EagerExecutionResult firstResult + ) { + return firstResult; + } + @Override protected Optional resolveSet( TagNode tagNode, diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java index 593f6b518..0605e0020 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java @@ -9,6 +9,7 @@ import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.util.EagerContextWatcher; import com.hubspot.jinjava.util.EagerExpressionResolver; +import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import com.hubspot.jinjava.util.EagerReconstructionUtils; import com.hubspot.jinjava.util.LengthLimitingStringJoiner; import com.hubspot.jinjava.util.WhitespaceUtils; @@ -32,23 +33,48 @@ public EagerExecutionResult getEagerExecutionResult( String expression, JinjavaInterpreter interpreter ) { + return EagerContextWatcher.executeInChildContext( + eagerInterpreter -> getEagerExpressionResult(expression, interpreter), + interpreter, + EagerContextWatcher + .EagerChildContextConfig.newBuilder() + .withTakeNewValue(true) + .build() + ); + } + + @Override + protected EagerExecutionResult getDeferredEagerExecutionResult( + TagNode tagNode, + String expression, + JinjavaInterpreter interpreter, + EagerExecutionResult firstResult + ) { + // Preserve identifiers when reconstructing to maintain proper object references try ( TemporaryValueClosable c = interpreter .getContext() .withPreserveAllIdentifiers(true) ) { return EagerContextWatcher.executeInChildContext( - eagerInterpreter -> - EagerExpressionResolver.resolveExpression('[' + expression + ']', interpreter), + eagerInterpreter -> getEagerExpressionResult(expression, interpreter), interpreter, EagerContextWatcher .EagerChildContextConfig.newBuilder() .withTakeNewValue(true) + .withForceDeferredExecutionMode(true) .build() ); } } + private static EagerExpressionResult getEagerExpressionResult( + String expression, + JinjavaInterpreter interpreter + ) { + return EagerExpressionResolver.resolveExpression('[' + expression + ']', interpreter); + } + @Override public Optional resolveSet( TagNode tagNode, diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java index 191b8ecaf..d3c8f5c33 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java @@ -61,6 +61,13 @@ public String run(TagNode tagNode, JinjavaInterpreter interpreter) { return maybeResolved.get(); } } + eagerExecutionResult = + getDeferredEagerExecutionResult( + tagNode, + expression, + interpreter, + eagerExecutionResult + ); Triple triple = getPrefixTokenAndSuffix( tagNode, variables, @@ -82,6 +89,13 @@ protected abstract EagerExecutionResult getEagerExecutionResult( JinjavaInterpreter interpreter ); + protected abstract EagerExecutionResult getDeferredEagerExecutionResult( + TagNode tagNode, + String expression, + JinjavaInterpreter interpreter, + EagerExecutionResult firstResult + ); + protected abstract Optional resolveSet( TagNode tagNode, String[] variables, From 15a742d72cdb3a2e573f1bd8edc6a851d113d4ac Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 11 Jan 2023 17:42:35 -0500 Subject: [PATCH 135/637] Fix expected results now that identifiers are preserved --- src/test/java/com/hubspot/jinjava/EagerTest.java | 5 ++--- .../com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java | 6 +++++- .../eager/handles-deferring-loop-variable.expected.jinja | 2 +- ...duplicate-variable-reference-modification.expected.jinja | 2 +- ...ndles-higher-scope-reference-modification.expected.jinja | 2 +- .../resources/eager/uses-unique-macro-names.expected.jinja | 6 +++--- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 6194decfa..69929778b 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -484,9 +484,8 @@ public void itMarksVariablesUsedAsMapKeysAsDeferred() { DeferredValueUtils.findAndMarkDeferredProperties(localContext); assertThat(deferredValue2).isInstanceOf(DeferredValue.class); assertThat(output) - .contains( - "{% set varSetInside = {'key': 'value'} [deferredValue2.nonexistentprop] %}" - ); + .contains("{% set imported = {'map': {'key': 'value'} } %}") + .contains("{% set varSetInside = imported.map[deferredValue2.nonexistentprop] %}"); } @Test diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java index 956591f5c..5739426f7 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java @@ -192,7 +192,11 @@ public void itAllowsChangesInDeferredForToken() { ); assertThat(output.trim()) .isEqualTo( - "{% for i in range(0, deferred) %}\n" + "{{ i }}\n" + "{% endfor %}\n" + "[0, 1]" + // now foo is being preserved because it may be used to fuel the collection we're looping through + "{% set foo = [0] %}{% set foo = [0] %}{% for i in range(foo.append(1) ? 0 : 1, deferred) %}\n" + + "{{ i }}\n" + + "{% endfor %}\n" + + "{{ foo }}" ); } diff --git a/src/test/resources/eager/handles-deferring-loop-variable.expected.jinja b/src/test/resources/eager/handles-deferring-loop-variable.expected.jinja index 740c79b89..1a1d7ec21 100644 --- a/src/test/resources/eager/handles-deferring-loop-variable.expected.jinja +++ b/src/test/resources/eager/handles-deferring-loop-variable.expected.jinja @@ -7,7 +7,7 @@ {% endif %} 2 {% endfor %} -{% for i in [0, 1] %} +{% set foo = [0, 1] %}{% set foo = [0, 1] %}{% for i in foo %} {% if deferred && loop.isLast() %}last time! {% endif %} {{ loop.index }} diff --git a/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja b/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja index b99253750..51c33fa58 100644 --- a/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja +++ b/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja @@ -1,5 +1,5 @@ {% set the_list = [] %}{% if deferred %} -{% set the_list = [] %}{% set some_list = [] %}{% set the_list = [] %}{% set some_list = the_list %}{% do some_list.append(deferred) %} +{% set the_list = [] %}{% set some_list = the_list %}{% do some_list.append(deferred) %} {% endif %} {{ the_list }} diff --git a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja b/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja index 95a282556..5b7bf126f 100644 --- a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja +++ b/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja @@ -4,7 +4,7 @@ C: {{ c_list }}.{% endmacro %}{% set b_list = a_list %}{% set b_list = a_list %} B: {% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{{ b_list }}.{% do a_list.append(deferred ? 'A' : '') %} A: {{ a_list }}. --- -{% set a_list = ['a'] %}{% set b_list = a_list %}{% for i in [0] %}{% set b_list = a_list %}{% set b_list = a_list %}{% do b_list.append('b') %}{% for __ignored__ in [0] %}{% set b_list = a_list %}{% set c_list = b_list %}{% do c_list.append(deferred ? 'c' : '') %} +{% set a_list = ['a'] %}{% for i in [0] %}{% set b_list = a_list %}{% set b_list = a_list %}{% do b_list.append('b') %}{% for __ignored__ in [0] %}{% set b_list = a_list %}{% set c_list = b_list %}{% do c_list.append(deferred ? 'c' : '') %} C: {{ c_list }}.{% endfor %}{% set b_list = a_list %}{% do b_list.append(deferred ? 'B' : '') %} B: {% set b_list = a_list %}{{ b_list }}.{% endfor %}{% do a_list.append(deferred ? 'A' : '') %} A: {{ a_list }}. diff --git a/src/test/resources/eager/uses-unique-macro-names.expected.jinja b/src/test/resources/eager/uses-unique-macro-names.expected.jinja index 713b24841..cccab7c13 100644 --- a/src/test/resources/eager/uses-unique-macro-names.expected.jinja +++ b/src/test/resources/eager/uses-unique-macro-names.expected.jinja @@ -1,10 +1,10 @@ {% set myname = deferred %} -{% set __macro_foo_97643642_temp_variable_0__ %} +{% set __macro_foo_97643642_temp_variable_1__ %} Goodbye {{ myname }} -{% endset %}{% set a = filter:upper.filter(__macro_foo_97643642_temp_variable_0__, ____int3rpr3t3r____) %} +{% endset %}{% set a = filter:upper.filter(__macro_foo_97643642_temp_variable_1__, ____int3rpr3t3r____) %} {% set __ignored__ %}{% set current_path = 'macro-with-filter.jinja' %} -{% set __macro_foo_927217348_temp_variable_0__ %}Hello {{ myname }}{% endset %}{% set b = filter:upper.filter(__macro_foo_927217348_temp_variable_0__, ____int3rpr3t3r____) %} +{% set __macro_foo_927217348_temp_variable_1__ %}Hello {{ myname }}{% endset %}{% set b = filter:upper.filter(__macro_foo_927217348_temp_variable_1__, ____int3rpr3t3r____) %} {% set current_path = '' %}{% endset %} {{ a }} {{ b }} From d31d7f3a960790dd27a5c28b376502105b80d886 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 11 Jan 2023 21:46:18 -0500 Subject: [PATCH 136/637] Refactor common code into static method --- .../el/ext/eager/EagerAstIdentifier.java | 9 +++----- .../el/ext/eager/EvalResultHolder.java | 13 ++++++----- ...entifiers-in-macro-function.expected.jinja | 15 ++++++++++++ ...serves-identifiers-in-macro-function.jinja | 23 +++++++++++++++++++ 4 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.expected.jinja create mode 100644 src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.jinja diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java index 443cc4c49..c08be9afc 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java @@ -3,7 +3,6 @@ import com.hubspot.jinjava.el.ext.DeferredParsingException; import com.hubspot.jinjava.el.ext.ExtendedParser; import com.hubspot.jinjava.interpret.DeferredValueException; -import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.filter.Filter; import com.hubspot.jinjava.util.EagerExpressionResolver; import de.odysseus.el.tree.Bindings; @@ -27,11 +26,9 @@ public Object eval(Bindings bindings, ELContext context) { !ExtendedParser.INTERPRETER.equals(getName()) && !EagerExpressionResolver.isPrimitive(result) && !(result instanceof Filter) && - ( - (JinjavaInterpreter) context - .getELResolver() - .getValue(context, null, ExtendedParser.INTERPRETER) - ).getContext() + EvalResultHolder + .getJinjavaInterpreter(context) + .getContext() .isPreserveAllIdentifiers() ) { throw new DeferredValueException("Preserving all non-primitive identifiers"); 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 30de67e77..0298c5b43 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 @@ -44,12 +44,7 @@ default Object checkEvalResultSize(ELContext context) { if ( evalResult instanceof Collection && ((Collection) evalResult).size() > 100 && // TODO make size configurable - ( - (JinjavaInterpreter) context - .getELResolver() - .getValue(context, null, ExtendedParser.INTERPRETER) - ).getContext() - .isDeferLargeObjects() + getJinjavaInterpreter(context).getContext().isDeferLargeObjects() ) { throw new DeferredValueException("Collection too big"); } @@ -63,6 +58,12 @@ String getPartiallyResolved( boolean preserveIdentifier ); + static JinjavaInterpreter getJinjavaInterpreter(ELContext context) { + return (JinjavaInterpreter) context + .getELResolver() + .getValue(context, null, ExtendedParser.INTERPRETER); + } + static String reconstructNode( Bindings bindings, ELContext context, diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.expected.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.expected.jinja new file mode 100644 index 000000000..2e33a3857 --- /dev/null +++ b/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.expected.jinja @@ -0,0 +1,15 @@ +{% set list = [] %} +{% macro bar() %} +{% set map1 = {} %} +{% set map2 = {} %} +{% do list.append(map1) %} +{% if deferred %} +{% do list.append(map2) %} +{% endif %} +{% do map1.update({'a': 'A'}) %} +{% do map2.update({'b': 'B'}) %} +{{ list }} +{% endmacro %} + +{% set foobar = bar() %} +{{ list }} \ No newline at end of file diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.jinja new file mode 100644 index 000000000..68937daaa --- /dev/null +++ b/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.jinja @@ -0,0 +1,23 @@ +{#{% set dict = {} %} + +{% set foo = (dict, deferred) %} + +{% do dict.update({'a': 'b'}) %} + +{{ foo }} +#} +{% set list = [] %} +{% macro bar() %} +{% set map1 = {} %} +{% set map2 = {} %} +{% do list.append(map1) %} +{% if deferred %} +{% do list.append(map2) %} +{% endif %} +{% do map1.update({'a': 'A'}) %} +{% do map2.update({'b': 'B'}) %} +{{ list }} +{% endmacro %} + +{% set foobar = bar() %} +{{ list }} \ No newline at end of file From 27f8a8bcd33093c342c41dc09064601076d6ea0e Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 11 Jan 2023 21:48:23 -0500 Subject: [PATCH 137/637] Allow macro functions to start in deferred execution mode to properly preserve identifiers when setting to an updatable value --- .../hubspot/jinjava/lib/fn/MacroFunction.java | 1 - .../lib/tag/eager/EagerExecutionResult.java | 28 +++++++++---------- .../jinjava/lib/tag/eager/EagerForTag.java | 2 +- .../jinjava/lib/tag/eager/EagerIfTag.java | 14 ++-------- .../tag/eager/EagerInlineSetTagStrategy.java | 1 + .../lib/tag/eager/EagerSetTagStrategy.java | 5 +++- .../util/EagerReconstructionUtils.java | 10 +++++++ .../java/com/hubspot/jinjava/EagerTest.java | 7 +++++ .../EagerExpressionStrategyTest.java | 4 +-- ...entifiers-in-macro-function.expected.jinja | 23 +++++++-------- ...serves-identifiers-in-macro-function.jinja | 10 +------ 11 files changed, 53 insertions(+), 52 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java index c63f9453b..b2efa44fd 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java @@ -77,7 +77,6 @@ public Object doEvaluate( JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); Optional importFile = getImportFile(interpreter); try (InterpreterScopeClosable c = interpreter.enterScope()) { - interpreter.getContext().setDeferredExecutionMode(false); String result = getEvaluationResult(argMap, kwargMap, varArgs, interpreter); if ( diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java index 698543906..bab3117bd 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java @@ -55,20 +55,6 @@ public String getPrefixToPreserveState(boolean registerDeferredToken) { } JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); prefixToPreserveState = - speculativeBindings - .entrySet() - .stream() - .filter(entry -> entry.getValue() instanceof PyishBlockSetSerializable) - .map( - entry -> - buildBlockSetTag( - entry.getKey(), - ((PyishBlockSetSerializable) entry.getValue()).getBlockSetBody(), - interpreter, - registerDeferredToken - ) - ) - .collect(Collectors.joining()) + buildSetTag( speculativeBindings .entrySet() @@ -84,6 +70,20 @@ public String getPrefixToPreserveState(boolean registerDeferredToken) { interpreter, registerDeferredToken ) + + speculativeBindings + .entrySet() + .stream() + .filter(entry -> entry.getValue() instanceof PyishBlockSetSerializable) + .map( + entry -> + buildBlockSetTag( + entry.getKey(), + ((PyishBlockSetSerializable) entry.getValue()).getBlockSetBody(), + interpreter, + registerDeferredToken + ) + ) + .collect(Collectors.joining()) + speculativeBindings .entrySet() .stream() diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 06dd16084..7d42cc242 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -58,7 +58,7 @@ public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { !result.getSpeculativeBindings().isEmpty() ) ) { - EagerIfTag.resetBindingsForNextBranch(interpreter, result); + EagerReconstructionUtils.resetSpeculativeBindings(interpreter, result); interpreter.getContext().removeDeferredTokens(addedTokens); throw new DeferredValueException( result.getResult().getResolutionState() == ResolutionState.NONE diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java index 25d1e2484..151676870 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java @@ -121,7 +121,9 @@ public String eagerRenderBranches( .build() ); sb.append(result.getResult()); - bindingsToDefer.addAll(resetBindingsForNextBranch(interpreter, result)); + bindingsToDefer.addAll( + EagerReconstructionUtils.resetSpeculativeBindings(interpreter, result) + ); } if (branchEnd >= childrenSize || definitelyExecuted) { break; @@ -180,16 +182,6 @@ public String eagerRenderBranches( return sb.toString(); } - public static Set resetBindingsForNextBranch( - JinjavaInterpreter interpreter, - EagerExecutionResult result - ) { - result - .getSpeculativeBindings() - .forEach((k, v) -> interpreter.getContext().replace(k, v)); - return result.getSpeculativeBindings().keySet(); - } - private String evaluateBranch( TagNode tagNode, int startIdx, diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java index 0605e0020..d88a87232 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java @@ -50,6 +50,7 @@ protected EagerExecutionResult getDeferredEagerExecutionResult( JinjavaInterpreter interpreter, EagerExecutionResult firstResult ) { + EagerReconstructionUtils.resetSpeculativeBindings(interpreter, firstResult); // Preserve identifiers when reconstructing to maintain proper object references try ( TemporaryValueClosable c = interpreter diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java index d3c8f5c33..e1ae274f5 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java @@ -131,7 +131,10 @@ protected String getPrefixToPreserveState( JinjavaInterpreter interpreter ) { StringBuilder prefixToPreserveState = new StringBuilder(); - if (interpreter.getContext().isDeferredExecutionMode()) { + if ( + interpreter.getContext().isDeferredExecutionMode() || + !eagerExecutionResult.getResult().isFullyResolved() + ) { prefixToPreserveState.append(eagerExecutionResult.getPrefixToPreserveState()); } else { interpreter.getContext().putAll(eagerExecutionResult.getSpeculativeBindings()); diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index b364e61ba..6ad78e7d1 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -653,4 +653,14 @@ public static String reconstructDeferredReferences( ) ); } + + public static Set resetSpeculativeBindings( + JinjavaInterpreter interpreter, + EagerExecutionResult result + ) { + result + .getSpeculativeBindings() + .forEach((k, v) -> interpreter.getContext().replace(k, v)); + return result.getSpeculativeBindings().keySet(); + } } diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 69929778b..65e71d0f7 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1264,4 +1264,11 @@ public void itCorrectlyPreservesIdentifiersInForLoop() { "correctly-preserves-identifiers-in-for-loop" ); } + + @Test + public void itCorrectlyPreservesIdentifiersInMacroFunction() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "correctly-preserves-identifiers-in-macro-function" + ); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java b/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java index c3c457397..9eae2ff80 100644 --- a/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java @@ -133,13 +133,13 @@ public void itGoesIntoDeferredExecutionMode() { } @Test - public void itDoesNotGoIntoDeferredExecutionModeWithMacro() { + public void itGoesIntoDeferredExecutionModeWithMacro() { assertExpectedOutput( "{% macro def() %}{{ is_deferred_execution_mode() }}{% endmacro %}" + "{{ def() }}" + "{% if deferred %}{{ def() }}{% endif %}" + "{{ def() }}", - "false{% if deferred %}false{% endif %}false" + "false{% if deferred %}true{% endif %}false" ); } diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.expected.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.expected.jinja index 2e33a3857..9252a99ed 100644 --- a/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.expected.jinja +++ b/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.expected.jinja @@ -1,15 +1,12 @@ -{% set list = [] %} -{% macro bar() %} -{% set map1 = {} %} -{% set map2 = {} %} -{% do list.append(map1) %} -{% if deferred %} -{% do list.append(map2) %} +{% set list = [{'a': 'A'} ] %}{% set __macro_bar_93535367_temp_variable_1__ %} +{% set map1 = {} %} +{% set map2 = {} %} +{% set map1 = {} %}{% do list.append(map1) %} +{% set map2 = {} %}{% if deferred %} +{% set map2 = {} %}{% do list.append(map2) %} {% endif %} -{% do map1.update({'a': 'A'}) %} -{% do map2.update({'b': 'B'}) %} +{% do map1.update({'a': 'A'} ) %} +{% do map2.update({'b': 'B'} ) %} +{{ list }} +{% endset %}{% set foobar = __macro_bar_93535367_temp_variable_1__ %} {{ list }} -{% endmacro %} - -{% set foobar = bar() %} -{{ list }} \ No newline at end of file diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.jinja index 68937daaa..2d14eb5c1 100644 --- a/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.jinja +++ b/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.jinja @@ -1,11 +1,3 @@ -{#{% set dict = {} %} - -{% set foo = (dict, deferred) %} - -{% do dict.update({'a': 'b'}) %} - -{{ foo }} -#} {% set list = [] %} {% macro bar() %} {% set map1 = {} %} @@ -20,4 +12,4 @@ {% endmacro %} {% set foobar = bar() %} -{{ list }} \ No newline at end of file +{{ list }} From ddf9f3b1ecd01cd17bc9d4a96cd062028ed65a07 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 12 Jan 2023 16:43:39 -0500 Subject: [PATCH 138/637] If an eager execution result is fully resolved, the new value can be taken --- .../jinjava/util/EagerContextWatcher.java | 94 ++++++++++--------- .../java/com/hubspot/jinjava/EagerTest.java | 7 ++ ...cation-in-resolved-for-loop.expected.jinja | 1 + ...ws-modification-in-resolved-for-loop.jinja | 12 +++ 4 files changed, 71 insertions(+), 43 deletions(-) create mode 100644 src/test/resources/eager/allows-modification-in-resolved-for-loop.expected.jinja create mode 100644 src/test/resources/eager/allows-modification-in-resolved-for-loop.jinja diff --git a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java index 2e43084dc..f6bf8d95b 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java @@ -70,7 +70,7 @@ public static EagerExecutionResult executeInChildContext( metaContextVariables, initiallyResolvedHashes, initiallyResolvedAsStrings, - initialResult.getSpeculativeBindings() + initialResult ); } else { Set ignoredKeys = getKeysToIgnore( @@ -84,7 +84,7 @@ public static EagerExecutionResult executeInChildContext( interpreter, eagerChildContextConfig, ignoredKeys, - initialResult.getSpeculativeBindings() + initialResult ); } return new EagerExecutionResult(initialResult.getResult(), speculativeBindings); @@ -188,30 +188,34 @@ private static Map getBasicSpeculativeBindings( JinjavaInterpreter interpreter, EagerChildContextConfig eagerChildContextConfig, Set ignoredKeys, - Map speculativeBindings + EagerExecutionResult eagerExecutionResult ) { - speculativeBindings.putAll( - interpreter - .getContext() - .getScope() - .entrySet() - .stream() - .filter( - entry -> - entry.getValue() instanceof DeferredLazyReferenceSource && - !(((DeferredLazyReferenceSource) entry.getValue()).isReconstructed()) - ) - .peek( - entry -> ((DeferredLazyReferenceSource) entry.getValue()).setReconstructed(true) - ) - .collect( - Collectors.toMap( - Entry::getKey, - entry -> ((DeferredLazyReferenceSource) entry.getValue()).getOriginalValue() + eagerExecutionResult + .getSpeculativeBindings() + .putAll( + interpreter + .getContext() + .getScope() + .entrySet() + .stream() + .filter( + entry -> + entry.getValue() instanceof DeferredLazyReferenceSource && + !(((DeferredLazyReferenceSource) entry.getValue()).isReconstructed()) ) - ) - ); - return speculativeBindings + .peek( + entry -> + ((DeferredLazyReferenceSource) entry.getValue()).setReconstructed(true) + ) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> ((DeferredLazyReferenceSource) entry.getValue()).getOriginalValue() + ) + ) + ); + return eagerExecutionResult + .getSpeculativeBindings() .entrySet() .stream() .filter(entry -> !ignoredKeys.contains(entry.getKey())) @@ -219,7 +223,10 @@ private static Map getBasicSpeculativeBindings( .map( entry -> { if ( - eagerChildContextConfig.takeNewValue && + ( + eagerExecutionResult.getResult().isFullyResolved() || + eagerChildContextConfig.takeNewValue + ) && !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null ) { @@ -264,22 +271,21 @@ private static Map getAllSpeculativeBindings( Set metaContextVariables, Map initiallyResolvedHashes, Map initiallyResolvedAsStrings, - Map speculativeBindings + EagerExecutionResult eagerExecutionResult ) { - speculativeBindings = - speculativeBindings - .entrySet() - .stream() - .filter( - entry -> - entry.getValue() != null && - !entry.getValue().equals(interpreter.getContext().get(entry.getKey())) - ) - .filter( - entry -> - !(interpreter.getContext().get(entry.getKey()) instanceof DeferredValue) - ) - .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + Map speculativeBindings = eagerExecutionResult + .getSpeculativeBindings() + .entrySet() + .stream() + .filter( + entry -> + entry.getValue() != null && + !entry.getValue().equals(interpreter.getContext().get(entry.getKey())) + ) + .filter( + entry -> !(interpreter.getContext().get(entry.getKey()) instanceof DeferredValue) + ) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); speculativeBindings.putAll( initiallyResolvedHashes .keySet() @@ -304,7 +310,8 @@ private static Map getAllSpeculativeBindings( eagerChildContextConfig, initiallyResolvedHashes, initiallyResolvedAsStrings, - entry + entry, + eagerExecutionResult.getResult().isFullyResolved() ) ) ) @@ -359,9 +366,10 @@ private static Object getOriginalValue( EagerChildContextConfig eagerChildContextConfig, Map initiallyResolvedHashes, Map initiallyResolvedAsStrings, - Entry e + Entry e, + boolean isFullyResolved ) { - if (eagerChildContextConfig.takeNewValue) { + if (eagerChildContextConfig.takeNewValue || isFullyResolved) { if (e.getValue() instanceof DeferredValue) { return ((DeferredValue) e.getValue()).getOriginalValue(); } diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 65e71d0f7..61e2aff16 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1271,4 +1271,11 @@ public void itCorrectlyPreservesIdentifiersInMacroFunction() { "correctly-preserves-identifiers-in-macro-function" ); } + + @Test + public void itAllowsModificationInResolvedForLoop() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "allows-modification-in-resolved-for-loop" + ); + } } diff --git a/src/test/resources/eager/allows-modification-in-resolved-for-loop.expected.jinja b/src/test/resources/eager/allows-modification-in-resolved-for-loop.expected.jinja new file mode 100644 index 000000000..82af21f61 --- /dev/null +++ b/src/test/resources/eager/allows-modification-in-resolved-for-loop.expected.jinja @@ -0,0 +1 @@ +[[0, [1, [2, [3, [4, [5, [6, [7, [8, [9, [10, [11, [12, [13, [14, [15, [16, [17, [18, [19]]]]]]]]]]]]]]]]]]]], 'END'] diff --git a/src/test/resources/eager/allows-modification-in-resolved-for-loop.jinja b/src/test/resources/eager/allows-modification-in-resolved-for-loop.jinja new file mode 100644 index 000000000..82ef7ee91 --- /dev/null +++ b/src/test/resources/eager/allows-modification-in-resolved-for-loop.jinja @@ -0,0 +1,12 @@ +{% set list = [] %} +{% set temp = list %} +{# create an object that is too deep for us to reconstruct via check on EagerExpressionResolver.isResolvableObject #} +{% for i in range(20) %} +{% set temp2 = [i] %} +{% do temp.append(temp2) %} +{% set temp = temp2 %} +{% endfor %} +{% for j in range(1) %} +{% do list.append('END') %} +{% endfor %} +{{ list }} From 0a9e1866cb461167379bcf6af4eacfae815e169a Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 12 Jan 2023 10:52:12 -0500 Subject: [PATCH 139/637] Only defer imported variables when in deferred execution mode When not in deferred execution mode, the resolved variables can still be put onto the context regularly --- .../jinjava/lib/tag/eager/EagerImportTag.java | 10 +++++++++- .../jinjava/lib/tag/eager/EagerImportTagTest.java | 14 ++++++++++++++ .../tags/eager/importtag/set-two-variables.jinja | 2 ++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/tags/eager/importtag/set-two-variables.jinja diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java index 520ba3bfe..1637ab966 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.Set; import java.util.StringJoiner; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -214,7 +215,13 @@ private String getSetTagForDeferredChildBindings( String currentImportAlias, Map childBindings ) { - if (Strings.isNullOrEmpty(currentImportAlias)) { + if ( + Strings.isNullOrEmpty(currentImportAlias) && + interpreter.getContext().isDeferredExecutionMode() + ) { + Set metaContextVariables = interpreter + .getContext() + .getMetaContextVariables(); // defer imported variables EagerReconstructionUtils.buildSetTag( childBindings @@ -224,6 +231,7 @@ private String getSetTagForDeferredChildBindings( entry -> !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null ) + .filter(entry -> !metaContextVariables.contains(entry.getKey())) .collect(Collectors.toMap(Entry::getKey, entry -> "")), interpreter, true 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 96bd42b81..eb3cb1bd4 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 @@ -744,6 +744,20 @@ public void itDoesNotSilentlyOverrideVariableWithoutAlias() { assertThat(interpreter.render(result)).isEqualTo("ab"); } + @Test + public void itDoesNotDeferImportedVariablesWhenNotInDeferredExecutionMode() { + setupResourceLocator(); + String result = interpreter + .render("{% import 'set-two-variables.jinja' %}" + "{{ foo }} {{ bar }}") + .trim(); + assertThat(result) + .isEqualTo( + "{% set __ignored__ %}{% set current_path = 'set-two-variables.jinja' %}{% set foo = deferred %}\n" + + "\n" + + "{% set current_path = '' %}{% endset %}{{ foo }} bar" + ); + } + private static JinjavaInterpreter getChildInterpreter( JinjavaInterpreter interpreter, String alias diff --git a/src/test/resources/tags/eager/importtag/set-two-variables.jinja b/src/test/resources/tags/eager/importtag/set-two-variables.jinja new file mode 100644 index 000000000..2f69297f9 --- /dev/null +++ b/src/test/resources/tags/eager/importtag/set-two-variables.jinja @@ -0,0 +1,2 @@ +{% set foo = deferred %} +{% set bar = 'bar' %} From 073b55924cea13a0a4b24116f1bdbf2ec4d78550 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 13 Jan 2023 15:10:44 -0500 Subject: [PATCH 140/637] Add test demonstrating problem if for loop's item is deferred on higher context level --- src/test/java/com/hubspot/jinjava/EagerTest.java | 7 +++++++ ...ers-loop-item-on-current-context.expected.jinja | 13 +++++++++++++ .../only-defers-loop-item-on-current-context.jinja | 14 ++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 src/test/resources/eager/only-defers-loop-item-on-current-context.expected.jinja create mode 100644 src/test/resources/eager/only-defers-loop-item-on-current-context.jinja diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 65e71d0f7..4618ed696 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1271,4 +1271,11 @@ public void itCorrectlyPreservesIdentifiersInMacroFunction() { "correctly-preserves-identifiers-in-macro-function" ); } + + @Test + public void itOnlyDefersLoopItemOnCurrentContext() { + expectedTemplateInterpreter.assertExpectedOutput( + "only-defers-loop-item-on-current-context" + ); + } } diff --git a/src/test/resources/eager/only-defers-loop-item-on-current-context.expected.jinja b/src/test/resources/eager/only-defers-loop-item-on-current-context.expected.jinja new file mode 100644 index 000000000..f8d4f070b --- /dev/null +++ b/src/test/resources/eager/only-defers-loop-item-on-current-context.expected.jinja @@ -0,0 +1,13 @@ +{% set outer_val = 'start' %}{% for def in deferred %} +{% set outer_list = [{'a': ['b']} ] %} +{% for __ignored__ in [0] %} +{% set map = {'a': ['b']} %}{% set inner_list = map.a %} +{% for x in inner_list %} + +{% set outer_val = outer_val ~ '-' %} +{% if outer_val == deferred %} +{{ outer_val }} +{% endif %} +{% endfor %} +{% endfor %} +{% endfor %} diff --git a/src/test/resources/eager/only-defers-loop-item-on-current-context.jinja b/src/test/resources/eager/only-defers-loop-item-on-current-context.jinja new file mode 100644 index 000000000..3bc01a0a5 --- /dev/null +++ b/src/test/resources/eager/only-defers-loop-item-on-current-context.jinja @@ -0,0 +1,14 @@ +{% set outer_val = 'start' %} +{% for def in deferred %} +{% set outer_list = [{'a': ['b']}] %} +{% for map in outer_list %} +{% set inner_list = map.a %} +{% for x in inner_list %} +{# make outer_val a speculative binding #} +{% set outer_val = outer_val ~ '-' %} +{% if outer_val == deferred %} +{{ outer_val }} +{% endif %} +{% endfor %} +{% endfor %} +{% endfor %} \ No newline at end of file From db1aef9b881ff103ea6046ff172f2b17f8d2ade2 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 13 Jan 2023 15:12:35 -0500 Subject: [PATCH 141/637] Adjust eager for tag logic to only defer loop items on the context the loop is run in --- .../com/hubspot/jinjava/lib/tag/eager/EagerForTag.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 7d42cc242..01b9bf6ab 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -17,6 +17,7 @@ import com.hubspot.jinjava.util.EagerReconstructionUtils; import com.hubspot.jinjava.util.LengthLimitingStringBuilder; import com.hubspot.jinjava.util.LengthLimitingStringJoiner; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -162,6 +163,11 @@ private EagerExecutionResult runLoopOnce( if (!(eagerInterpreter.getContext().get("loop") instanceof DeferredValue)) { eagerInterpreter.getContext().put("loop", DeferredValue.instance()); } + + getTag() + .getLoopVarsAndExpression((TagToken) tagNode.getMaster()) + .getLeft() + .forEach(var -> interpreter.getContext().put(var, DeferredValue.instance())); return EagerExpressionResult.fromString( renderChildren(tagNode, eagerInterpreter) ); @@ -227,7 +233,7 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter !(interpreter.getContext().get(word) instanceof DeferredMacroValueImpl) ) .collect(Collectors.toSet()), - new HashSet<>(loopVars) + Collections.emptySet() ) ) ); From dd33ca98a4e0c89e23437615a357e0afb42d043e Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 13 Jan 2023 15:27:49 -0500 Subject: [PATCH 142/637] Update tests to no longer expect set deferred words from deferred for tag. And make it so that meta context variables are only temporarily removed when running a for loop --- .../jinjava/lib/tag/eager/EagerForTag.java | 28 ++++++++++++------- .../util/EagerReconstructionUtils.java | 3 +- .../java/com/hubspot/jinjava/EagerTest.java | 4 +-- .../lib/tag/eager/EagerForTagTest.java | 6 ++-- ...meta-context-var-overriding.expected.jinja | 2 +- 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 01b9bf6ab..00746e060 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -163,14 +163,26 @@ private EagerExecutionResult runLoopOnce( if (!(eagerInterpreter.getContext().get("loop") instanceof DeferredValue)) { eagerInterpreter.getContext().put("loop", DeferredValue.instance()); } - - getTag() + List loopVars = getTag() .getLoopVarsAndExpression((TagToken) tagNode.getMaster()) - .getLeft() - .forEach(var -> interpreter.getContext().put(var, DeferredValue.instance())); - return EagerExpressionResult.fromString( - renderChildren(tagNode, eagerInterpreter) + .getLeft(); + Set removedMetaContextVariables = EagerReconstructionUtils.removeMetaContextVariables( + loopVars.stream(), + interpreter.getContext() + ); + loopVars.forEach( + var -> interpreter.getContext().put(var, DeferredValue.instance()) ); + try { + return EagerExpressionResult.fromString( + renderChildren(tagNode, eagerInterpreter) + ); + } finally { + interpreter + .getContext() + .getMetaContextVariables() + .addAll(removedMetaContextVariables); + } }, interpreter, EagerContextWatcher @@ -210,10 +222,6 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter interpreter ); prefixToPreserveState.append(newlyDeferredFunctionImages); - EagerReconstructionUtils.removeMetaContextVariables( - loopVars.stream(), - interpreter.getContext() - ); prefixToPreserveState.append( EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 6ad78e7d1..26d8de6cf 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -562,7 +562,7 @@ public static String wrapInChildScope(String toWrap, JinjavaInterpreter interpre ); } - public static void removeMetaContextVariables( + public static Set removeMetaContextVariables( Stream varStream, Context context ) { @@ -575,6 +575,7 @@ public static void removeMetaContextVariables( ) .immutableCopy(); context.getMetaContextVariables().removeAll(metaSetVars); + return metaSetVars; } public static Boolean isDeferredExecutionMode() { diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 4618ed696..a1d719cfa 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -504,7 +504,7 @@ public void itEvaluatesNonEagerSet() { .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) .collect(Collectors.toSet()) ) - .containsExactlyInAnyOrder("item"); + .isEmpty(); assertThat( localContext .getDeferredTokens() @@ -973,7 +973,7 @@ public void itHandlesImportInDeferredIf() { public void itAllowsMetaContextVarOverriding() { interpreter.getContext().getMetaContextVariables().add("meta"); interpreter.getContext().put("meta", "META"); - expectedTemplateInterpreter.assertExpectedOutput( + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( "allows-meta-context-var-overriding" ); } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java index 5739426f7..000fb9859 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java @@ -57,8 +57,7 @@ public void itRegistersDeferredToken() { .filter(e -> ((TagToken) e.getToken()).getTagName().equals(tag.getName())) .findAny(); assertThat(maybeDeferredToken).isPresent(); - assertThat(maybeDeferredToken.get().getSetDeferredWords()) - .containsExactlyInAnyOrder("item"); + assertThat(maybeDeferredToken.get().getSetDeferredWords()).isEmpty(); assertThat(maybeDeferredToken.get().getUsedDeferredWords()).contains("deferred"); } @@ -72,8 +71,7 @@ public void itHandlesMultipleLoopVars() { .filter(e -> ((TagToken) e.getToken()).getTagName().equals(tag.getName())) .findAny(); assertThat(maybeDeferredToken).isPresent(); - assertThat(maybeDeferredToken.get().getSetDeferredWords()) - .containsExactlyInAnyOrder("item", "item2"); + assertThat(maybeDeferredToken.get().getSetDeferredWords()).isEmpty(); assertThat(maybeDeferredToken.get().getUsedDeferredWords()).contains("deferred"); } diff --git a/src/test/resources/eager/allows-meta-context-var-overriding.expected.jinja b/src/test/resources/eager/allows-meta-context-var-overriding.expected.jinja index 6052f7712..d1e716de3 100644 --- a/src/test/resources/eager/allows-meta-context-var-overriding.expected.jinja +++ b/src/test/resources/eager/allows-meta-context-var-overriding.expected.jinja @@ -1,6 +1,6 @@ 0 META -{% set meta = 'META' %}{% for meta in deferred %} +{% for meta in deferred %} {{ meta }}{% endfor %} {{ meta }} {% set meta = [] %}{% if deferred %} From 9bd59911922d09da7d347e32d1b95f49d5b98e60 Mon Sep 17 00:00:00 2001 From: Jack Smith <72623970+jasmith-hs@users.noreply.github.com> Date: Fri, 13 Jan 2023 15:52:09 -0500 Subject: [PATCH 143/637] Revert "Preserve identifiers when reconstructing a tag which sets a value" --- .../el/ext/eager/EagerAstIdentifier.java | 20 +------ .../el/ext/eager/EvalResultHolder.java | 13 +++-- .../hubspot/jinjava/interpret/Context.java | 23 -------- .../hubspot/jinjava/lib/fn/MacroFunction.java | 1 + .../tag/eager/EagerBlockSetTagStrategy.java | 10 ---- .../lib/tag/eager/EagerExecutionResult.java | 28 +++++----- .../jinjava/lib/tag/eager/EagerForTag.java | 52 ++++++++----------- .../jinjava/lib/tag/eager/EagerIfTag.java | 14 +++-- .../tag/eager/EagerInlineSetTagStrategy.java | 38 +------------- .../lib/tag/eager/EagerSetTagStrategy.java | 19 +------ .../util/EagerReconstructionUtils.java | 10 ---- .../java/com/hubspot/jinjava/EagerTest.java | 27 ++-------- .../EagerExpressionStrategyTest.java | 4 +- .../lib/tag/eager/EagerForTagTest.java | 6 +-- ...ves-identifiers-in-for-loop.expected.jinja | 8 --- ...ly-preserves-identifiers-in-for-loop.jinja | 10 ---- ...s-identifiers-in-inline-set.expected.jinja | 5 -- ...-preserves-identifiers-in-inline-set.jinja | 7 --- ...entifiers-in-macro-function.expected.jinja | 12 ----- ...serves-identifiers-in-macro-function.jinja | 15 ------ ...les-deferring-loop-variable.expected.jinja | 2 +- ...able-reference-modification.expected.jinja | 2 +- ...cope-reference-modification.expected.jinja | 2 +- .../uses-unique-macro-names.expected.jinja | 6 +-- 24 files changed, 71 insertions(+), 263 deletions(-) delete mode 100644 src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.expected.jinja delete mode 100644 src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.jinja delete mode 100644 src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.expected.jinja delete mode 100644 src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.jinja delete mode 100644 src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.expected.jinja delete mode 100644 src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.jinja diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java index c08be9afc..a7b631e4a 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java @@ -1,10 +1,6 @@ package com.hubspot.jinjava.el.ext.eager; import com.hubspot.jinjava.el.ext.DeferredParsingException; -import com.hubspot.jinjava.el.ext.ExtendedParser; -import com.hubspot.jinjava.interpret.DeferredValueException; -import com.hubspot.jinjava.lib.filter.Filter; -import com.hubspot.jinjava.util.EagerExpressionResolver; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstIdentifier; import javax.el.ELContext; @@ -20,21 +16,7 @@ public EagerAstIdentifier(String name, int index, boolean ignoreReturnType) { @Override public Object eval(Bindings bindings, ELContext context) { return EvalResultHolder.super.eval( - () -> { - Object result = super.eval(bindings, context); - if ( - !ExtendedParser.INTERPRETER.equals(getName()) && - !EagerExpressionResolver.isPrimitive(result) && - !(result instanceof Filter) && - EvalResultHolder - .getJinjavaInterpreter(context) - .getContext() - .isPreserveAllIdentifiers() - ) { - throw new DeferredValueException("Preserving all non-primitive identifiers"); - } - return result; - }, + () -> super.eval(bindings, context), bindings, context ); 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 0298c5b43..30de67e77 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 @@ -44,7 +44,12 @@ default Object checkEvalResultSize(ELContext context) { if ( evalResult instanceof Collection && ((Collection) evalResult).size() > 100 && // TODO make size configurable - getJinjavaInterpreter(context).getContext().isDeferLargeObjects() + ( + (JinjavaInterpreter) context + .getELResolver() + .getValue(context, null, ExtendedParser.INTERPRETER) + ).getContext() + .isDeferLargeObjects() ) { throw new DeferredValueException("Collection too big"); } @@ -58,12 +63,6 @@ String getPartiallyResolved( boolean preserveIdentifier ); - static JinjavaInterpreter getJinjavaInterpreter(ELContext context) { - return (JinjavaInterpreter) context - .getELResolver() - .getValue(context, null, ExtendedParser.INTERPRETER); - } - static String reconstructNode( Bindings bindings, ELContext context, diff --git a/src/main/java/com/hubspot/jinjava/interpret/Context.java b/src/main/java/com/hubspot/jinjava/interpret/Context.java index bdbe61120..8e4d2d30d 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/Context.java +++ b/src/main/java/com/hubspot/jinjava/interpret/Context.java @@ -107,8 +107,6 @@ public enum Library { private boolean validationMode = false; private boolean deferredExecutionMode = false; private boolean deferLargeObjects = false; - - private boolean preserveAllIdentifiers = false; private boolean throwInterpreterErrors = false; private boolean partialMacroEvaluation = false; private boolean unwrapRawOverride = false; @@ -213,7 +211,6 @@ public Context( this.dynamicVariableResolver = parent.dynamicVariableResolver; this.deferredExecutionMode = parent.deferredExecutionMode; this.deferLargeObjects = parent.deferLargeObjects; - this.preserveAllIdentifiers = parent.preserveAllIdentifiers; this.throwInterpreterErrors = parent.throwInterpreterErrors; } } @@ -732,26 +729,6 @@ public TemporaryValueClosable withDeferLargeObjects( return temporaryValueClosable; } - public boolean isPreserveAllIdentifiers() { - return preserveAllIdentifiers; - } - - public Context setPreserveAllIdentifiers(boolean preserveAllIdentifiers) { - this.preserveAllIdentifiers = preserveAllIdentifiers; - return this; - } - - public TemporaryValueClosable withPreserveAllIdentifiers( - boolean preserveAllIdentifiers - ) { - TemporaryValueClosable temporaryValueClosable = new TemporaryValueClosable<>( - this.preserveAllIdentifiers, - this::setPreserveAllIdentifiers - ); - this.preserveAllIdentifiers = preserveAllIdentifiers; - return temporaryValueClosable; - } - public boolean getThrowInterpreterErrors() { return throwInterpreterErrors; } diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java index b2efa44fd..c63f9453b 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java @@ -77,6 +77,7 @@ public Object doEvaluate( JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); Optional importFile = getImportFile(interpreter); try (InterpreterScopeClosable c = interpreter.enterScope()) { + interpreter.getContext().setDeferredExecutionMode(false); String result = getEvaluationResult(argMap, kwargMap, varArgs, interpreter); if ( diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java index 0b9ca7653..25337c4f2 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerBlockSetTagStrategy.java @@ -55,16 +55,6 @@ protected EagerExecutionResult getEagerExecutionResult( return result; } - @Override - protected EagerExecutionResult getDeferredEagerExecutionResult( - TagNode tagNode, - String expression, - JinjavaInterpreter interpreter, - EagerExecutionResult firstResult - ) { - return firstResult; - } - @Override protected Optional resolveSet( TagNode tagNode, diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java index bab3117bd..698543906 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java @@ -55,6 +55,20 @@ public String getPrefixToPreserveState(boolean registerDeferredToken) { } JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); prefixToPreserveState = + speculativeBindings + .entrySet() + .stream() + .filter(entry -> entry.getValue() instanceof PyishBlockSetSerializable) + .map( + entry -> + buildBlockSetTag( + entry.getKey(), + ((PyishBlockSetSerializable) entry.getValue()).getBlockSetBody(), + interpreter, + registerDeferredToken + ) + ) + .collect(Collectors.joining()) + buildSetTag( speculativeBindings .entrySet() @@ -70,20 +84,6 @@ public String getPrefixToPreserveState(boolean registerDeferredToken) { interpreter, registerDeferredToken ) + - speculativeBindings - .entrySet() - .stream() - .filter(entry -> entry.getValue() instanceof PyishBlockSetSerializable) - .map( - entry -> - buildBlockSetTag( - entry.getKey(), - ((PyishBlockSetSerializable) entry.getValue()).getBlockSetBody(), - interpreter, - registerDeferredToken - ) - ) - .collect(Collectors.joining()) + speculativeBindings .entrySet() .stream() diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 7d42cc242..13291b73a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -58,7 +58,7 @@ public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { !result.getSpeculativeBindings().isEmpty() ) ) { - EagerReconstructionUtils.resetSpeculativeBindings(interpreter, result); + EagerIfTag.resetBindingsForNextBranch(interpreter, result); interpreter.getContext().removeDeferredTokens(addedTokens); throw new DeferredValueException( result.getResult().getResolutionState() == ResolutionState.NONE @@ -98,34 +98,28 @@ public String eagerInterpret( interpreter.getContext().isDeferLargeObjects() ) ) { - try ( - TemporaryValueClosable c1 = interpreter - .getContext() - .withPreserveAllIdentifiers(true) - ) { - // separate getEagerImage from renderChildren because the token gets evaluated once - // while the children are evaluated 0...n times. - result.append( - EagerContextWatcher - .executeInChildContext( - eagerInterpreter -> - EagerExpressionResult.fromString( - getEagerImage( - buildToken( - tagNode, - e, - interpreter.getLineNumber(), - interpreter.getPosition() - ), - eagerInterpreter - ) - ), - interpreter, - EagerContextWatcher.EagerChildContextConfig.newBuilder().build() - ) - .asTemplateString() - ); - } + // separate getEagerImage from renderChildren because the token gets evaluated once + // while the children are evaluated 0...n times. + result.append( + EagerContextWatcher + .executeInChildContext( + eagerInterpreter -> + EagerExpressionResult.fromString( + getEagerImage( + buildToken( + tagNode, + e, + interpreter.getLineNumber(), + interpreter.getPosition() + ), + eagerInterpreter + ) + ), + interpreter, + EagerContextWatcher.EagerChildContextConfig.newBuilder().build() + ) + .asTemplateString() + ); } EagerExecutionResult firstRunResult = runLoopOnce(tagNode, interpreter); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java index 151676870..25d1e2484 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java @@ -121,9 +121,7 @@ public String eagerRenderBranches( .build() ); sb.append(result.getResult()); - bindingsToDefer.addAll( - EagerReconstructionUtils.resetSpeculativeBindings(interpreter, result) - ); + bindingsToDefer.addAll(resetBindingsForNextBranch(interpreter, result)); } if (branchEnd >= childrenSize || definitelyExecuted) { break; @@ -182,6 +180,16 @@ public String eagerRenderBranches( return sb.toString(); } + public static Set resetBindingsForNextBranch( + JinjavaInterpreter interpreter, + EagerExecutionResult result + ) { + result + .getSpeculativeBindings() + .forEach((k, v) -> interpreter.getContext().replace(k, v)); + return result.getSpeculativeBindings().keySet(); + } + private String evaluateBranch( TagNode tagNode, int startIdx, diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java index d88a87232..9b786a84d 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerInlineSetTagStrategy.java @@ -1,6 +1,5 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; @@ -9,7 +8,6 @@ import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.util.EagerContextWatcher; import com.hubspot.jinjava.util.EagerExpressionResolver; -import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import com.hubspot.jinjava.util.EagerReconstructionUtils; import com.hubspot.jinjava.util.LengthLimitingStringJoiner; import com.hubspot.jinjava.util.WhitespaceUtils; @@ -34,7 +32,8 @@ public EagerExecutionResult getEagerExecutionResult( JinjavaInterpreter interpreter ) { return EagerContextWatcher.executeInChildContext( - eagerInterpreter -> getEagerExpressionResult(expression, interpreter), + eagerInterpreter -> + EagerExpressionResolver.resolveExpression('[' + expression + ']', interpreter), interpreter, EagerContextWatcher .EagerChildContextConfig.newBuilder() @@ -43,39 +42,6 @@ public EagerExecutionResult getEagerExecutionResult( ); } - @Override - protected EagerExecutionResult getDeferredEagerExecutionResult( - TagNode tagNode, - String expression, - JinjavaInterpreter interpreter, - EagerExecutionResult firstResult - ) { - EagerReconstructionUtils.resetSpeculativeBindings(interpreter, firstResult); - // Preserve identifiers when reconstructing to maintain proper object references - try ( - TemporaryValueClosable c = interpreter - .getContext() - .withPreserveAllIdentifiers(true) - ) { - return EagerContextWatcher.executeInChildContext( - eagerInterpreter -> getEagerExpressionResult(expression, interpreter), - interpreter, - EagerContextWatcher - .EagerChildContextConfig.newBuilder() - .withTakeNewValue(true) - .withForceDeferredExecutionMode(true) - .build() - ); - } - } - - private static EagerExpressionResult getEagerExpressionResult( - String expression, - JinjavaInterpreter interpreter - ) { - return EagerExpressionResolver.resolveExpression('[' + expression + ']', interpreter); - } - @Override public Optional resolveSet( TagNode tagNode, diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java index e1ae274f5..191b8ecaf 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java @@ -61,13 +61,6 @@ public String run(TagNode tagNode, JinjavaInterpreter interpreter) { return maybeResolved.get(); } } - eagerExecutionResult = - getDeferredEagerExecutionResult( - tagNode, - expression, - interpreter, - eagerExecutionResult - ); Triple triple = getPrefixTokenAndSuffix( tagNode, variables, @@ -89,13 +82,6 @@ protected abstract EagerExecutionResult getEagerExecutionResult( JinjavaInterpreter interpreter ); - protected abstract EagerExecutionResult getDeferredEagerExecutionResult( - TagNode tagNode, - String expression, - JinjavaInterpreter interpreter, - EagerExecutionResult firstResult - ); - protected abstract Optional resolveSet( TagNode tagNode, String[] variables, @@ -131,10 +117,7 @@ protected String getPrefixToPreserveState( JinjavaInterpreter interpreter ) { StringBuilder prefixToPreserveState = new StringBuilder(); - if ( - interpreter.getContext().isDeferredExecutionMode() || - !eagerExecutionResult.getResult().isFullyResolved() - ) { + if (interpreter.getContext().isDeferredExecutionMode()) { prefixToPreserveState.append(eagerExecutionResult.getPrefixToPreserveState()); } else { interpreter.getContext().putAll(eagerExecutionResult.getSpeculativeBindings()); diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 6ad78e7d1..b364e61ba 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -653,14 +653,4 @@ public static String reconstructDeferredReferences( ) ); } - - public static Set resetSpeculativeBindings( - JinjavaInterpreter interpreter, - EagerExecutionResult result - ) { - result - .getSpeculativeBindings() - .forEach((k, v) -> interpreter.getContext().replace(k, v)); - return result.getSpeculativeBindings().keySet(); - } } diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 65e71d0f7..37914d4e2 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -484,8 +484,9 @@ public void itMarksVariablesUsedAsMapKeysAsDeferred() { DeferredValueUtils.findAndMarkDeferredProperties(localContext); assertThat(deferredValue2).isInstanceOf(DeferredValue.class); assertThat(output) - .contains("{% set imported = {'map': {'key': 'value'} } %}") - .contains("{% set varSetInside = imported.map[deferredValue2.nonexistentprop] %}"); + .contains( + "{% set varSetInside = {'key': 'value'} [deferredValue2.nonexistentprop] %}" + ); } @Test @@ -1249,26 +1250,4 @@ public void itReconstructsWordsFromInsideNestedExpressionsSecondPass() { public void itDoesNotReconstructExtraTimes() { expectedTemplateInterpreter.assertExpectedOutput("does-not-reconstruct-extra-times"); } - - @Test - public void itCorrectlyPreservesIdentifiersInInlineSet() { - // This doesn't need to happen in block sets, because they only store string values - expectedTemplateInterpreter.assertExpectedOutput( - "correctly-preserves-identifiers-in-inline-set" - ); - } - - @Test - public void itCorrectlyPreservesIdentifiersInForLoop() { - expectedTemplateInterpreter.assertExpectedOutput( - "correctly-preserves-identifiers-in-for-loop" - ); - } - - @Test - public void itCorrectlyPreservesIdentifiersInMacroFunction() { - expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "correctly-preserves-identifiers-in-macro-function" - ); - } } diff --git a/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java b/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java index 9eae2ff80..c3c457397 100644 --- a/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java @@ -133,13 +133,13 @@ public void itGoesIntoDeferredExecutionMode() { } @Test - public void itGoesIntoDeferredExecutionModeWithMacro() { + public void itDoesNotGoIntoDeferredExecutionModeWithMacro() { assertExpectedOutput( "{% macro def() %}{{ is_deferred_execution_mode() }}{% endmacro %}" + "{{ def() }}" + "{% if deferred %}{{ def() }}{% endif %}" + "{{ def() }}", - "false{% if deferred %}true{% endif %}false" + "false{% if deferred %}false{% endif %}false" ); } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java index 5739426f7..956591f5c 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java @@ -192,11 +192,7 @@ public void itAllowsChangesInDeferredForToken() { ); assertThat(output.trim()) .isEqualTo( - // now foo is being preserved because it may be used to fuel the collection we're looping through - "{% set foo = [0] %}{% set foo = [0] %}{% for i in range(foo.append(1) ? 0 : 1, deferred) %}\n" + - "{{ i }}\n" + - "{% endfor %}\n" + - "{{ foo }}" + "{% for i in range(0, deferred) %}\n" + "{{ i }}\n" + "{% endfor %}\n" + "[0, 1]" ); } diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.expected.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.expected.jinja deleted file mode 100644 index 83aa2c074..000000000 --- a/src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.expected.jinja +++ /dev/null @@ -1,8 +0,0 @@ -{% set dict = {} %}{% set dict = {} %}{% for item in [dict] %} -{% if deferred %} -{% do item.update({'c': 'd'} ) %} -{% endif %} -{{ item }} -{% endfor %} - -{% do dict.update({'a': 'b'} ) %} diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.jinja deleted file mode 100644 index 061461d8b..000000000 --- a/src/test/resources/eager/correctly-preserves-identifiers-in-for-loop.jinja +++ /dev/null @@ -1,10 +0,0 @@ -{% set dict = {} %} - -{% for item in [dict] %} -{% if deferred %} -{% do item.update({'c': 'd'}) %} -{% endif %} -{{ item }} -{% endfor %} - -{% do dict.update({'a': 'b'}) %} diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.expected.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.expected.jinja deleted file mode 100644 index 78328bf02..000000000 --- a/src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.expected.jinja +++ /dev/null @@ -1,5 +0,0 @@ -{% set dict = {} %}{% set foo = (dict, deferred) %} - -{% do dict.update({'a': 'b'} ) %} - -{{ foo }} diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.jinja deleted file mode 100644 index 5e11bf14e..000000000 --- a/src/test/resources/eager/correctly-preserves-identifiers-in-inline-set.jinja +++ /dev/null @@ -1,7 +0,0 @@ -{% set dict = {} %} - -{% set foo = (dict, deferred) %} - -{% do dict.update({'a': 'b'}) %} - -{{ foo }} diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.expected.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.expected.jinja deleted file mode 100644 index 9252a99ed..000000000 --- a/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.expected.jinja +++ /dev/null @@ -1,12 +0,0 @@ -{% set list = [{'a': 'A'} ] %}{% set __macro_bar_93535367_temp_variable_1__ %} -{% set map1 = {} %} -{% set map2 = {} %} -{% set map1 = {} %}{% do list.append(map1) %} -{% set map2 = {} %}{% if deferred %} -{% set map2 = {} %}{% do list.append(map2) %} -{% endif %} -{% do map1.update({'a': 'A'} ) %} -{% do map2.update({'b': 'B'} ) %} -{{ list }} -{% endset %}{% set foobar = __macro_bar_93535367_temp_variable_1__ %} -{{ list }} diff --git a/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.jinja b/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.jinja deleted file mode 100644 index 2d14eb5c1..000000000 --- a/src/test/resources/eager/correctly-preserves-identifiers-in-macro-function.jinja +++ /dev/null @@ -1,15 +0,0 @@ -{% set list = [] %} -{% macro bar() %} -{% set map1 = {} %} -{% set map2 = {} %} -{% do list.append(map1) %} -{% if deferred %} -{% do list.append(map2) %} -{% endif %} -{% do map1.update({'a': 'A'}) %} -{% do map2.update({'b': 'B'}) %} -{{ list }} -{% endmacro %} - -{% set foobar = bar() %} -{{ list }} diff --git a/src/test/resources/eager/handles-deferring-loop-variable.expected.jinja b/src/test/resources/eager/handles-deferring-loop-variable.expected.jinja index 1a1d7ec21..740c79b89 100644 --- a/src/test/resources/eager/handles-deferring-loop-variable.expected.jinja +++ b/src/test/resources/eager/handles-deferring-loop-variable.expected.jinja @@ -7,7 +7,7 @@ {% endif %} 2 {% endfor %} -{% set foo = [0, 1] %}{% set foo = [0, 1] %}{% for i in foo %} +{% for i in [0, 1] %} {% if deferred && loop.isLast() %}last time! {% endif %} {{ loop.index }} diff --git a/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja b/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja index 51c33fa58..b99253750 100644 --- a/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja +++ b/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja @@ -1,5 +1,5 @@ {% set the_list = [] %}{% if deferred %} -{% set the_list = [] %}{% set some_list = the_list %}{% do some_list.append(deferred) %} +{% set the_list = [] %}{% set some_list = [] %}{% set the_list = [] %}{% set some_list = the_list %}{% do some_list.append(deferred) %} {% endif %} {{ the_list }} diff --git a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja b/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja index 5b7bf126f..95a282556 100644 --- a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja +++ b/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja @@ -4,7 +4,7 @@ C: {{ c_list }}.{% endmacro %}{% set b_list = a_list %}{% set b_list = a_list %} B: {% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{% set b_list = a_list %}{{ b_list }}.{% do a_list.append(deferred ? 'A' : '') %} A: {{ a_list }}. --- -{% set a_list = ['a'] %}{% for i in [0] %}{% set b_list = a_list %}{% set b_list = a_list %}{% do b_list.append('b') %}{% for __ignored__ in [0] %}{% set b_list = a_list %}{% set c_list = b_list %}{% do c_list.append(deferred ? 'c' : '') %} +{% set a_list = ['a'] %}{% set b_list = a_list %}{% for i in [0] %}{% set b_list = a_list %}{% set b_list = a_list %}{% do b_list.append('b') %}{% for __ignored__ in [0] %}{% set b_list = a_list %}{% set c_list = b_list %}{% do c_list.append(deferred ? 'c' : '') %} C: {{ c_list }}.{% endfor %}{% set b_list = a_list %}{% do b_list.append(deferred ? 'B' : '') %} B: {% set b_list = a_list %}{{ b_list }}.{% endfor %}{% do a_list.append(deferred ? 'A' : '') %} A: {{ a_list }}. diff --git a/src/test/resources/eager/uses-unique-macro-names.expected.jinja b/src/test/resources/eager/uses-unique-macro-names.expected.jinja index cccab7c13..713b24841 100644 --- a/src/test/resources/eager/uses-unique-macro-names.expected.jinja +++ b/src/test/resources/eager/uses-unique-macro-names.expected.jinja @@ -1,10 +1,10 @@ {% set myname = deferred %} -{% set __macro_foo_97643642_temp_variable_1__ %} +{% set __macro_foo_97643642_temp_variable_0__ %} Goodbye {{ myname }} -{% endset %}{% set a = filter:upper.filter(__macro_foo_97643642_temp_variable_1__, ____int3rpr3t3r____) %} +{% endset %}{% set a = filter:upper.filter(__macro_foo_97643642_temp_variable_0__, ____int3rpr3t3r____) %} {% set __ignored__ %}{% set current_path = 'macro-with-filter.jinja' %} -{% set __macro_foo_927217348_temp_variable_1__ %}Hello {{ myname }}{% endset %}{% set b = filter:upper.filter(__macro_foo_927217348_temp_variable_1__, ____int3rpr3t3r____) %} +{% set __macro_foo_927217348_temp_variable_0__ %}Hello {{ myname }}{% endset %}{% set b = filter:upper.filter(__macro_foo_927217348_temp_variable_0__, ____int3rpr3t3r____) %} {% set current_path = '' %}{% endset %} {{ a }} {{ b }} From 1e87b24183ef5766d47f2d3add08474378adfdca Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 13 Jan 2023 16:11:47 -0500 Subject: [PATCH 144/637] Move resetBindingsForNextBranch method to EagerReconstructionUtils.resetSpeculativeBindings --- .../hubspot/jinjava/lib/tag/eager/EagerForTag.java | 2 +- .../hubspot/jinjava/lib/tag/eager/EagerIfTag.java | 14 +++----------- .../jinjava/util/EagerReconstructionUtils.java | 10 ++++++++++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java index 13291b73a..5b95c403c 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java @@ -58,7 +58,7 @@ public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { !result.getSpeculativeBindings().isEmpty() ) ) { - EagerIfTag.resetBindingsForNextBranch(interpreter, result); + EagerReconstructionUtils.resetSpeculativeBindings(interpreter, result); interpreter.getContext().removeDeferredTokens(addedTokens); throw new DeferredValueException( result.getResult().getResolutionState() == ResolutionState.NONE diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java index 25d1e2484..151676870 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIfTag.java @@ -121,7 +121,9 @@ public String eagerRenderBranches( .build() ); sb.append(result.getResult()); - bindingsToDefer.addAll(resetBindingsForNextBranch(interpreter, result)); + bindingsToDefer.addAll( + EagerReconstructionUtils.resetSpeculativeBindings(interpreter, result) + ); } if (branchEnd >= childrenSize || definitelyExecuted) { break; @@ -180,16 +182,6 @@ public String eagerRenderBranches( return sb.toString(); } - public static Set resetBindingsForNextBranch( - JinjavaInterpreter interpreter, - EagerExecutionResult result - ) { - result - .getSpeculativeBindings() - .forEach((k, v) -> interpreter.getContext().replace(k, v)); - return result.getSpeculativeBindings().keySet(); - } - private String evaluateBranch( TagNode tagNode, int startIdx, diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index b364e61ba..6ad78e7d1 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -653,4 +653,14 @@ public static String reconstructDeferredReferences( ) ); } + + public static Set resetSpeculativeBindings( + JinjavaInterpreter interpreter, + EagerExecutionResult result + ) { + result + .getSpeculativeBindings() + .forEach((k, v) -> interpreter.getContext().replace(k, v)); + return result.getSpeculativeBindings().keySet(); + } } From ca6a8c234486fa6a6349c71d2d637c6a3b41d8bc Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 17 Jan 2023 14:57:45 -0500 Subject: [PATCH 145/637] Ignore this test which was only really necessary before https://github.com/HubSpot/jinjava/pull/988 got reverted --- src/test/java/com/hubspot/jinjava/EagerTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index c750be73b..04544fa0f 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; public class EagerTest { @@ -1259,6 +1260,7 @@ public void itAllowsModificationInResolvedForLoop() { } @Test + @Ignore // Test isn't necessary after https://github.com/HubSpot/jinjava/pull/988 got reverted public void itOnlyDefersLoopItemOnCurrentContext() { expectedTemplateInterpreter.assertExpectedOutput( "only-defers-loop-item-on-current-context" From 1bac4918f398390b790a1925a73e8865655c1f79 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 18 Jan 2023 08:21:18 -0500 Subject: [PATCH 146/637] Get invoke method which best matches the parameter types even when none are specified --- .../jinjava/el/ext/JinjavaBeanELResolver.java | 265 +++++++++++++++++- .../java/com/hubspot/jinjava/EagerTest.java | 10 +- 2 files changed, 268 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java index 75a140aa2..f1e4584a9 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java @@ -5,12 +5,19 @@ import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.util.EagerReconstructionUtils; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.LinkedList; +import java.util.List; import java.util.Set; import javax.el.BeanELResolver; import javax.el.ELContext; +import javax.el.ELException; +import javax.el.ExpressionFactory; import javax.el.MethodNotFoundException; /** @@ -57,6 +64,8 @@ public class JinjavaBeanELResolver extends BeanELResolver { .add("merge") .build(); + private ExpressionFactory defaultFactory; + /** * Creates a new read/write {@link JinjavaBeanELResolver}. */ @@ -122,9 +131,12 @@ public Object invoke( ) ); } - - Object result = super.invoke(context, base, method, paramTypes, params); - + Object result; + if (paramTypes == null) { + result = invokeBestMatch(context, base, method, params); + } else { + result = super.invoke(context, base, method, paramTypes, params); + } if (isRestrictedClass(result)) { throw new MethodNotFoundException( "Cannot find method '" + method + "' in " + base.getClass() @@ -134,6 +146,253 @@ public Object invoke( return result; } + protected Object invokeBestMatch( + ELContext context, + Object base, + Object method, + Object[] params + ) { + if (context == null) { + throw new NullPointerException(); + } else { + Object result = null; + if (base != null) { + if (params == null) { + params = new Object[0]; + } + + String name = method.toString(); + Method target = this.findMethod(base, name, params, params.length); + if (target == null) { + throw new MethodNotFoundException( + "Cannot find method " + + name + + " with " + + params.length + + " parameters in " + + base.getClass() + ); + } + + try { + result = + target.invoke( + base, + this.coerceParams(this.getExpressionFactory(context), target, params) + ); + } catch (InvocationTargetException var10) { + throw new ELException(var10.getCause()); + } catch (IllegalAccessException var11) { + throw new ELException(var11); + } + + context.setPropertyResolved(true); + } + + return result; + } + } + + protected Method findMethod(Object base, String name, Object[] params, int paramCount) { + Method varArgsMethod = null; + Method[] methods = base.getClass().getMethods(); + List potentialMethods = new LinkedList<>(); + + for (Method method : methods) { + if (method.getName().equals(name)) { + int formalParamCount = method.getParameterTypes().length; + if (method.isVarArgs() && paramCount >= formalParamCount - 1) { + varArgsMethod = method; + } else if (paramCount == formalParamCount) { + potentialMethods.add(findAccessibleMethod(method)); + } + } + } + final Method finalVarArgsMethod = varArgsMethod; + return potentialMethods + .stream() + .filter( + method -> { + for (int i = 0; i < method.getParameterTypes().length; i++) { + if ( + params[i] != null && + !method.getParameterTypes()[i].isAssignableFrom(params[i].getClass()) + ) { + return false; + } + } + return true; + } + ) + .findAny() + .orElseGet( + () -> + potentialMethods + .stream() + .findAny() + .orElseGet( + () -> + finalVarArgsMethod == null + ? null + : findAccessibleMethod(finalVarArgsMethod) + ) + ); + } + + private static Method findAccessibleMethod(Method method) { + Method result = findPublicAccessibleMethod(method); + if (result == null && method != null && Modifier.isPublic(method.getModifiers())) { + result = method; + + try { + method.setAccessible(true); + } catch (SecurityException var3) { + result = null; + } + } + + return result; + } + + private static Method findPublicAccessibleMethod(Method method) { + if (method != null && Modifier.isPublic(method.getModifiers())) { + if ( + !method.isAccessible() && + !Modifier.isPublic(method.getDeclaringClass().getModifiers()) + ) { + Class[] arr$ = method.getDeclaringClass().getInterfaces(); + int len$ = arr$.length; + + for (int i$ = 0; i$ < len$; ++i$) { + Class cls = arr$[i$]; + Method mth = null; + + try { + mth = + findPublicAccessibleMethod( + cls.getMethod(method.getName(), method.getParameterTypes()) + ); + if (mth != null) { + return mth; + } + } catch (NoSuchMethodException var8) {} + } + + Class cls = method.getDeclaringClass().getSuperclass(); + if (cls != null) { + Method mth = null; + + try { + mth = + findPublicAccessibleMethod( + cls.getMethod(method.getName(), method.getParameterTypes()) + ); + if (mth != null) { + return mth; + } + } catch (NoSuchMethodException var7) {} + } + + return null; + } else { + return method; + } + } else { + return null; + } + } + + private Object[] coerceParams( + ExpressionFactory factory, + Method method, + Object[] params + ) { + Class[] types = method.getParameterTypes(); + Object[] args = new Object[types.length]; + int varargIndex; + if (method.isVarArgs()) { + varargIndex = types.length - 1; + if (params.length < varargIndex) { + throw new ELException("Bad argument count"); + } + + for (int i = 0; i < varargIndex; ++i) { + this.coerceValue(args, i, factory, params[i], types[i]); + } + + Class varargType = types[varargIndex].getComponentType(); + int length = params.length - varargIndex; + Object array = null; + if (length == 1) { + Object source = params[varargIndex]; + if (source != null && source.getClass().isArray()) { + if (types[varargIndex].isInstance(source)) { + array = source; + } else { + length = Array.getLength(source); + array = Array.newInstance(varargType, length); + + for (int i = 0; i < length; ++i) { + this.coerceValue(array, i, factory, Array.get(source, i), varargType); + } + } + } else { + array = Array.newInstance(varargType, 1); + this.coerceValue(array, 0, factory, source, varargType); + } + } else { + array = Array.newInstance(varargType, length); + + for (int i = 0; i < length; ++i) { + this.coerceValue(array, i, factory, params[varargIndex + i], varargType); + } + } + + args[varargIndex] = array; + } else { + if (params.length != args.length) { + throw new ELException("Bad argument count"); + } + + for (varargIndex = 0; varargIndex < args.length; ++varargIndex) { + this.coerceValue( + args, + varargIndex, + factory, + params[varargIndex], + types[varargIndex] + ); + } + } + + return args; + } + + private void coerceValue( + Object array, + int index, + ExpressionFactory factory, + Object value, + Class type + ) { + if (value != null || type.isPrimitive()) { + Array.set(array, index, factory.coerceToType(value, type)); + } + } + + private ExpressionFactory getExpressionFactory(ELContext context) { + Object obj = context.getContext(ExpressionFactory.class); + if (obj instanceof ExpressionFactory) { + return (ExpressionFactory) obj; + } else { + if (this.defaultFactory == null) { + this.defaultFactory = ExpressionFactory.newInstance(); + } + + return this.defaultFactory; + } + } + private String validatePropertyName(Object property) { String propertyName = transformPropertyName(property); diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 04544fa0f..007707f55 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1241,10 +1241,12 @@ public void itReconstructsWordsFromInsideNestedExpressions() { @Test public void itReconstructsWordsFromInsideNestedExpressionsSecondPass() { - interpreter.getContext().put("deferred", new PyList(new ArrayList<>())); - expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "reconstructs-words-from-inside-nested-expressions.expected" - ); + // interpreter.getContext().put("deferred", new PyList(new ArrayList<>())); + // expectedTemplateInterpreter.assertExpectedNonEagerOutput( + // "reconstructs-words-from-inside-nested-expressions.expected" + // ); + String foo = interpreter.render("{{ 'foo'.replace('foo', '') }}"); + String bar = ""; } @Test From 8b9eac21f4dc80a735f5e1e95ab194dd47f6a528 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 23 Jan 2023 14:25:55 -0500 Subject: [PATCH 147/637] Add testing for finding the best method --- .../jinjava/el/ext/JinjavaBeanELResolver.java | 45 ++++-- .../java/com/hubspot/jinjava/EagerTest.java | 10 +- .../el/ext/JinjavaBeanELResolverTest.java | 145 ++++++++++++++++++ 3 files changed, 180 insertions(+), 20 deletions(-) create mode 100644 src/test/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolverTest.java diff --git a/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java index f1e4584a9..6088ded05 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java @@ -5,6 +5,7 @@ import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.util.EagerReconstructionUtils; +import java.lang.invoke.MethodType; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -195,6 +196,7 @@ protected Object invokeBestMatch( protected Method findMethod(Object base, String name, Object[] params, int paramCount) { Method varArgsMethod = null; + Method[] methods = base.getClass().getMethods(); List potentialMethods = new LinkedList<>(); @@ -211,20 +213,8 @@ protected Method findMethod(Object base, String name, Object[] params, int param final Method finalVarArgsMethod = varArgsMethod; return potentialMethods .stream() - .filter( - method -> { - for (int i = 0; i < method.getParameterTypes().length; i++) { - if ( - params[i] != null && - !method.getParameterTypes()[i].isAssignableFrom(params[i].getClass()) - ) { - return false; - } - } - return true; - } - ) - .findAny() + .filter(method -> checkAssignableParameterTypes(params, method)) + .min(JinjavaBeanELResolver::pickMoreSpecificMethod) .orElseGet( () -> potentialMethods @@ -239,6 +229,33 @@ protected Method findMethod(Object base, String name, Object[] params, int param ); } + private static boolean checkAssignableParameterTypes(Object[] params, Method method) { + for (int i = 0; i < method.getParameterTypes().length; i++) { + Class paramType = method.getParameterTypes()[i]; + if (paramType.isPrimitive()) { + paramType = MethodType.methodType(paramType).wrap().returnType(); + } + if (params[i] != null && !paramType.isAssignableFrom(params[i].getClass())) { + return false; + } + } + return true; + } + + private static int pickMoreSpecificMethod(Method methodA, Method methodB) { + Class[] typesA = methodA.getParameterTypes(); + Class[] typesB = methodB.getParameterTypes(); + for (int i = 0; i < typesA.length; i++) { + if (!typesA[i].isAssignableFrom(typesB[i])) { + if (typesB[i].isPrimitive()) { + return 1; + } + return -1; + } + } + return 1; + } + private static Method findAccessibleMethod(Method method) { Method result = findPublicAccessibleMethod(method); if (result == null && method != null && Modifier.isPublic(method.getModifiers())) { diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 007707f55..04544fa0f 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1241,12 +1241,10 @@ public void itReconstructsWordsFromInsideNestedExpressions() { @Test public void itReconstructsWordsFromInsideNestedExpressionsSecondPass() { - // interpreter.getContext().put("deferred", new PyList(new ArrayList<>())); - // expectedTemplateInterpreter.assertExpectedNonEagerOutput( - // "reconstructs-words-from-inside-nested-expressions.expected" - // ); - String foo = interpreter.render("{{ 'foo'.replace('foo', '') }}"); - String bar = ""; + interpreter.getContext().put("deferred", new PyList(new ArrayList<>())); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "reconstructs-words-from-inside-nested-expressions.expected" + ); } @Test diff --git a/src/test/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolverTest.java b/src/test/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolverTest.java new file mode 100644 index 000000000..f9ba4f281 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolverTest.java @@ -0,0 +1,145 @@ +package com.hubspot.jinjava.el.ext; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.el.JinjavaELContext; +import javax.el.ELContext; +import org.junit.Before; +import org.junit.Test; + +public class JinjavaBeanELResolverTest { + private JinjavaBeanELResolver jinjavaBeanELResolver; + private ELContext elContext; + + @Before + public void setUp() throws Exception { + jinjavaBeanELResolver = new JinjavaBeanELResolver(); + elContext = new JinjavaELContext(); + } + + @Test + public void itInvokesProperStringReplace() { + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + "abcd", + "replace", + null, + new Object[] { "abcd", "efgh" } + ) + ) + .isEqualTo("efgh"); + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + "abcd", + "replace", + null, + new Object[] { 'a', 'e' } + ) + ) + .isEqualTo("ebcd"); + } + + @Test + public void itInvokesBestMethodWithSingleParam() { + class Temp { + + public String getResult(int a) { + return "int"; + } + + public String getResult(String a) { + return "String"; + } + + public String getResult(Object a) { + return "Object"; + } + + public String getResult(CharSequence a) { + return "CharSequence"; + } + } + Temp var = new Temp(); + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + var, + "getResult", + null, + new Object[] { 1 } + ) + ) + .isEqualTo("int"); + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + var, + "getResult", + null, + new Object[] { "1" } + ) + ) + .isEqualTo("String"); + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + var, + "getResult", + null, + new Object[] { new Object() } + ) + ) + .isEqualTo("Object"); + } + + @Test + public void itPrefersPrimitives() { + class Temp { + + public String getResult(int a, Integer b) { + return "int Integer"; + } + + public String getResult(int a, Object b) { + return "int Object"; + } + + public String getResult(Number a, int b) { + return "Number int"; + } + } + Temp var = new Temp(); + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + var, + "getResult", + null, + new Object[] { 1, 2 } + ) + ) + .isEqualTo("int Integer"); + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + var, + "getResult", + null, + new Object[] { 1, Integer.valueOf(2) } + ) + ) + .isEqualTo("int Integer"); // should be "int object", but we can't figure that out + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + var, + "getResult", + null, + new Object[] { Integer.valueOf(1), 2 } + ) + ) + .isEqualTo("int Integer"); // should be "Number int", but we can't figure that out + } +} From 340f04f66d0cff97eaadf04647db7bacf0ca567e Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 23 Jan 2023 14:51:26 -0500 Subject: [PATCH 148/637] Copy Odysseus's BeanELResolver to separate class --- .../jinjava/el/ext/BeanELResolver.java | 719 ++++++++++++++++++ 1 file changed, 719 insertions(+) create mode 100644 src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java diff --git a/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java b/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java new file mode 100644 index 000000000..b005a71d8 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java @@ -0,0 +1,719 @@ +/* + * Copyright 2006-2009 Odysseus Software GmbH + * Modifications Copyright (c) 2023 HubSpot Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hubspot.jinjava.el.ext; + +import java.beans.FeatureDescriptor; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.el.CompositeELResolver; +import javax.el.ELContext; +import javax.el.ELException; +import javax.el.ELResolver; +import javax.el.ExpressionFactory; +import javax.el.MethodNotFoundException; +import javax.el.PropertyNotFoundException; +import javax.el.PropertyNotWritableException; + +/** + * Defines property resolution behavior on objects using the JavaBeans component architecture. This + * resolver handles base objects of any type, as long as the base is not null. It accepts any object + * as a property, and coerces it to a string. That string is then used to find a JavaBeans compliant + * property on the base object. The value is accessed using JavaBeans getters and setters. This + * resolver can be constructed in read-only mode, which means that isReadOnly will always return + * true and {@link #setValue(ELContext, Object, Object, Object)} will always throw + * PropertyNotWritableException. ELResolvers are combined together using {@link CompositeELResolver} + * s, to define rich semantics for evaluating an expression. See the javadocs for {@link ELResolver} + * for details. Because this resolver handles base objects of any type, it should be placed near the + * end of a composite resolver. Otherwise, it will claim to have resolved a property before any + * resolvers that come after it get a chance to test if they can do so as well. + * + * @see CompositeELResolver + * @see ELResolver + */ +public class BeanELResolver extends ELResolver { + + protected static final class BeanProperties { + private final Map map = new HashMap(); + + public BeanProperties(Class baseClass) { + PropertyDescriptor[] descriptors; + try { + descriptors = Introspector.getBeanInfo(baseClass).getPropertyDescriptors(); + } catch (IntrospectionException e) { + throw new ELException(e); + } + for (PropertyDescriptor descriptor : descriptors) { + map.put(descriptor.getName(), new BeanProperty(descriptor)); + } + } + + public BeanProperty getBeanProperty(String property) { + return map.get(property); + } + } + + protected static final class BeanProperty { + private final PropertyDescriptor descriptor; + + private Method readMethod; + private Method writedMethod; + + public BeanProperty(PropertyDescriptor descriptor) { + this.descriptor = descriptor; + } + + public Class getPropertyType() { + return descriptor.getPropertyType(); + } + + public Method getReadMethod() { + if (readMethod == null) { + readMethod = findAccessibleMethod(descriptor.getReadMethod()); + } + return readMethod; + } + + public Method getWriteMethod() { + if (writedMethod == null) { + writedMethod = findAccessibleMethod(descriptor.getWriteMethod()); + } + return writedMethod; + } + + public boolean isReadOnly() { + return getWriteMethod() == null; + } + } + + private static Method findPublicAccessibleMethod(Method method) { + if (method == null || !Modifier.isPublic(method.getModifiers())) { + return null; + } + if ( + method.isAccessible() || + Modifier.isPublic(method.getDeclaringClass().getModifiers()) + ) { + return method; + } + for (Class cls : method.getDeclaringClass().getInterfaces()) { + Method mth = null; + try { + mth = + findPublicAccessibleMethod( + cls.getMethod(method.getName(), method.getParameterTypes()) + ); + if (mth != null) { + return mth; + } + } catch (NoSuchMethodException ignore) { + // do nothing + } + } + Class cls = method.getDeclaringClass().getSuperclass(); + if (cls != null) { + Method mth = null; + try { + mth = + findPublicAccessibleMethod( + cls.getMethod(method.getName(), method.getParameterTypes()) + ); + if (mth != null) { + return mth; + } + } catch (NoSuchMethodException ignore) { + // do nothing + } + } + return null; + } + + private static Method findAccessibleMethod(Method method) { + Method result = findPublicAccessibleMethod(method); + if (result == null && method != null && Modifier.isPublic(method.getModifiers())) { + result = method; + try { + method.setAccessible(true); + } catch (SecurityException e) { + result = null; + } + } + return result; + } + + private final boolean readOnly; + private final ConcurrentHashMap, BeanProperties> cache; + + private ExpressionFactory defaultFactory; + + /** + * Creates a new read/write BeanELResolver. + */ + public BeanELResolver() { + this(false); + } + + /** + * Creates a new BeanELResolver whose read-only status is determined by the given parameter. + */ + public BeanELResolver(boolean readOnly) { + this.readOnly = readOnly; + this.cache = new ConcurrentHashMap, BeanProperties>(); + } + + /** + * If the base object is not null, returns the most general type that this resolver accepts for + * the property argument. Otherwise, returns null. Assuming the base is not null, this method + * will always return Object.class. This is because any object is accepted as a key and is + * coerced into a string. + * + * @param context + * The context of this evaluation. + * @param base + * The bean to analyze. + * @return null if base is null; otherwise Object.class. + */ + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + return isResolvable(base) ? Object.class : null; + } + + /** + * If the base object is not null, returns an Iterator containing the set of JavaBeans + * properties available on the given object. Otherwise, returns null. The Iterator returned must + * contain zero or more instances of java.beans.FeatureDescriptor. Each info object contains + * information about a property in the bean, as obtained by calling the + * BeanInfo.getPropertyDescriptors method. The FeatureDescriptor is initialized using the same + * fields as are present in the PropertyDescriptor, with the additional required named + * attributes "type" and "resolvableAtDesignTime" set as follows: + *
    + *
  • {@link ELResolver#TYPE} - The runtime type of the property, from + * PropertyDescriptor.getPropertyType().
  • + *
  • {@link ELResolver#RESOLVABLE_AT_DESIGN_TIME} - true.
  • + *
+ * + * @param context + * The context of this evaluation. + * @param base + * The bean to analyze. + * @return An Iterator containing zero or more FeatureDescriptor objects, each representing a + * property on this bean, or null if the base object is null. + */ + @Override + public Iterator getFeatureDescriptors( + ELContext context, + Object base + ) { + if (isResolvable(base)) { + final PropertyDescriptor[] properties; + try { + properties = Introspector.getBeanInfo(base.getClass()).getPropertyDescriptors(); + } catch (IntrospectionException e) { + return Collections.emptyList().iterator(); + } + return new Iterator() { + int next = 0; + + public boolean hasNext() { + return properties != null && next < properties.length; + } + + public FeatureDescriptor next() { + PropertyDescriptor property = properties[next++]; + FeatureDescriptor feature = new FeatureDescriptor(); + feature.setDisplayName(property.getDisplayName()); + feature.setName(property.getName()); + feature.setShortDescription(property.getShortDescription()); + feature.setExpert(property.isExpert()); + feature.setHidden(property.isHidden()); + feature.setPreferred(property.isPreferred()); + feature.setValue(TYPE, property.getPropertyType()); + feature.setValue(RESOLVABLE_AT_DESIGN_TIME, true); + return feature; + } + + public void remove() { + throw new UnsupportedOperationException("cannot remove"); + } + }; + } + return null; + } + + /** + * If the base object is not null, returns the most general acceptable type that can be set on + * this bean property. If the base is not null, the propertyResolved property of the ELContext + * object must be set to true by this resolver, before returning. If this property is not true + * after this method is called, the caller should ignore the return value. The provided property + * will first be coerced to a String. If there is a BeanInfoProperty for this property and there + * were no errors retrieving it, the propertyType of the propertyDescriptor is returned. + * Otherwise, a PropertyNotFoundException is thrown. + * + * @param context + * The context of this evaluation. + * @param base + * The bean to analyze. + * @param property + * The name of the property to analyze. Will be coerced to a String. + * @return If the propertyResolved property of ELContext was set to true, then the most general + * acceptable type; otherwise undefined. + * @throws NullPointerException + * if context is null + * @throws PropertyNotFoundException + * if base is not null and the specified property does not exist or is not readable. + * @throws ELException + * if an exception was thrown while performing the property or variable resolution. + * The thrown exception must be included as the cause property of this exception, if + * available. + */ + @Override + public Class getType(ELContext context, Object base, Object property) { + if (context == null) { + throw new NullPointerException(); + } + Class result = null; + if (isResolvable(base)) { + result = toBeanProperty(base, property).getPropertyType(); + context.setPropertyResolved(true); + } + return result; + } + + /** + * If the base object is not null, returns the current value of the given property on this bean. + * If the base is not null, the propertyResolved property of the ELContext object must be set to + * true by this resolver, before returning. If this property is not true after this method is + * called, the caller should ignore the return value. The provided property name will first be + * coerced to a String. If the property is a readable property of the base object, as per the + * JavaBeans specification, then return the result of the getter call. If the getter throws an + * exception, it is propagated to the caller. If the property is not found or is not readable, a + * PropertyNotFoundException is thrown. + * + * @param context + * The context of this evaluation. + * @param base + * The bean to analyze. + * @param property + * The name of the property to analyze. Will be coerced to a String. + * @return If the propertyResolved property of ELContext was set to true, then the value of the + * given property. Otherwise, undefined. + * @throws NullPointerException + * if context is null + * @throws PropertyNotFoundException + * if base is not null and the specified property does not exist or is not readable. + * @throws ELException + * if an exception was thrown while performing the property or variable resolution. + * The thrown exception must be included as the cause property of this exception, if + * available. + */ + @Override + public Object getValue(ELContext context, Object base, Object property) { + if (context == null) { + throw new NullPointerException(); + } + Object result = null; + if (isResolvable(base)) { + Method method = toBeanProperty(base, property).getReadMethod(); + if (method == null) { + throw new PropertyNotFoundException("Cannot read property " + property); + } + try { + result = method.invoke(base); + } catch (InvocationTargetException e) { + throw new ELException(e.getCause()); + } catch (Exception e) { + throw new ELException(e); + } + context.setPropertyResolved(true); + } + return result; + } + + /** + * If the base object is not null, returns whether a call to + * {@link #setValue(ELContext, Object, Object, Object)} will always fail. If the base is not + * null, the propertyResolved property of the ELContext object must be set to true by this + * resolver, before returning. If this property is not true after this method is called, the + * caller can safely assume no value was set. + * + * @param context + * The context of this evaluation. + * @param base + * The bean to analyze. + * @param property + * The name of the property to analyze. Will be coerced to a String. + * @return If the propertyResolved property of ELContext was set to true, then true if calling + * the setValue method will always fail or false if it is possible that such a call may + * succeed; otherwise undefined. + * @throws NullPointerException + * if context is null + * @throws PropertyNotFoundException + * if base is not null and the specified property does not exist or is not readable. + * @throws ELException + * if an exception was thrown while performing the property or variable resolution. + * The thrown exception must be included as the cause property of this exception, if + * available. + */ + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + if (context == null) { + throw new NullPointerException(); + } + boolean result = readOnly; + if (isResolvable(base)) { + result |= toBeanProperty(base, property).isReadOnly(); + context.setPropertyResolved(true); + } + return result; + } + + /** + * If the base object is not null, attempts to set the value of the given property on this bean. + * If the base is not null, the propertyResolved property of the ELContext object must be set to + * true by this resolver, before returning. If this property is not true after this method is + * called, the caller can safely assume no value was set. If this resolver was constructed in + * read-only mode, this method will always throw PropertyNotWritableException. The provided + * property name will first be coerced to a String. If property is a writable property of base + * (as per the JavaBeans Specification), the setter method is called (passing value). If the + * property exists but does not have a setter, then a PropertyNotFoundException is thrown. If + * the property does not exist, a PropertyNotFoundException is thrown. + * + * @param context + * The context of this evaluation. + * @param base + * The bean to analyze. + * @param property + * The name of the property to analyze. Will be coerced to a String. + * @param value + * The value to be associated with the specified key. + * @throws NullPointerException + * if context is null + * @throws PropertyNotFoundException + * if base is not null and the specified property does not exist or is not readable. + * @throws PropertyNotWritableException + * if this resolver was constructed in read-only mode, or if there is no setter for + * the property + * @throws ELException + * if an exception was thrown while performing the property or variable resolution. + * The thrown exception must be included as the cause property of this exception, if + * available. + */ + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + if (context == null) { + throw new NullPointerException(); + } + if (isResolvable(base)) { + if (readOnly) { + throw new PropertyNotWritableException("resolver is read-only"); + } + Method method = toBeanProperty(base, property).getWriteMethod(); + if (method == null) { + throw new PropertyNotWritableException("Cannot write property: " + property); + } + try { + method.invoke(base, value); + } catch (InvocationTargetException e) { + throw new ELException("Cannot write property: " + property, e.getCause()); + } catch (IllegalArgumentException e) { + throw new ELException("Cannot write property: " + property, e); + } catch (IllegalAccessException e) { + throw new PropertyNotWritableException("Cannot write property: " + property, e); + } + context.setPropertyResolved(true); + } + } + + /** + * If the base object is not null, invoke the method, with the given parameters on + * this bean. The return value from the method is returned. + * + *

+ * If the base is not null, the propertyResolved property of the + * ELContext object must be set to true by this resolver, before + * returning. If this property is not true after this method is called, the caller + * should ignore the return value. + *

+ * + *

+ * The provided method object will first be coerced to a String. The methods in the + * bean is then examined and an attempt will be made to select one for invocation. If no + * suitable can be found, a MethodNotFoundException is thrown. + * + * If the given paramTypes is not null, select the method with the given name and + * parameter types. + * + * Else select the method with the given name that has the same number of parameters. If there + * are more than one such method, the method selection process is undefined. + * + * Else select the method with the given name that takes a variable number of arguments. + * + * Note the resolution for overloaded methods will likely be clarified in a future version of + * the spec. + * + * The provided parameters are coerced to the corresponding parameter types of the method, and + * the method is then invoked. + * + * @param context + * The context of this evaluation. + * @param base + * The bean on which to invoke the method + * @param method + * The simple name of the method to invoke. Will be coerced to a String. + * If method is "<init>"or "<clinit>" a MethodNotFoundException is + * thrown. + * @param paramTypes + * An array of Class objects identifying the method's formal parameter types, in + * declared order. Use an empty array if the method has no parameters. Can be + * null, in which case the method's formal parameter types are assumed + * to be unknown. + * @param params + * The parameters to pass to the method, or null if no parameters. + * @return The result of the method invocation (null if the method has a + * void return type). + * @throws MethodNotFoundException + * if no suitable method can be found. + * @throws ELException + * if an exception was thrown while performing (base, method) resolution. The thrown + * exception must be included as the cause property of this exception, if available. + * If the exception thrown is an InvocationTargetException, extract its + * cause and pass it to the ELException constructor. + * @since 2.2 + */ + @Override + public Object invoke( + ELContext context, + Object base, + Object method, + Class[] paramTypes, + Object[] params + ) { + if (context == null) { + throw new NullPointerException(); + } + Object result = null; + if (isResolvable(base)) { + if (params == null) { + params = new Object[0]; + } + String name = method.toString(); + Method target = findMethod(base, name, paramTypes, params.length); + if (target == null) { + throw new MethodNotFoundException( + "Cannot find method " + + name + + " with " + + params.length + + " parameters in " + + base.getClass() + ); + } + try { + result = + target.invoke( + base, + coerceParams(getExpressionFactory(context), target, params) + ); + } catch (InvocationTargetException e) { + throw new ELException(e.getCause()); + } catch (IllegalAccessException e) { + throw new ELException(e); + } + context.setPropertyResolved(true); + } + return result; + } + + private Method findMethod(Object base, String name, Class[] types, int paramCount) { + if (types != null) { + try { + return findAccessibleMethod(base.getClass().getMethod(name, types)); + } catch (NoSuchMethodException e) { + return null; + } + } + Method varArgsMethod = null; + for (Method method : base.getClass().getMethods()) { + if (method.getName().equals(name)) { + int formalParamCount = method.getParameterTypes().length; + if (method.isVarArgs() && paramCount >= formalParamCount - 1) { + varArgsMethod = method; + } else if (paramCount == formalParamCount) { + return findAccessibleMethod(method); + } + } + } + return varArgsMethod == null ? null : findAccessibleMethod(varArgsMethod); + } + + /** + * Lookup an expression factory used to coerce method parameters in context under key + * "javax.el.ExpressionFactory". + * If no expression factory can be found under that key, use a default instance created with + * {@link ExpressionFactory#newInstance()}. + * @param context + * The context of this evaluation. + * @return expression factory instance + */ + protected ExpressionFactory getExpressionFactory(ELContext context) { + Object obj = context.getContext(ExpressionFactory.class); + if (obj instanceof ExpressionFactory) { + return (ExpressionFactory) obj; + } + if (defaultFactory == null) { + defaultFactory = ExpressionFactory.newInstance(); + } + return defaultFactory; + } + + protected Object[] coerceParams( + ExpressionFactory factory, + Method method, + Object[] params + ) { + Class[] types = method.getParameterTypes(); + Object[] args = new Object[types.length]; + if (method.isVarArgs()) { + int varargIndex = types.length - 1; + if (params.length < varargIndex) { + throw new ELException("Bad argument count"); + } + for (int i = 0; i < varargIndex; i++) { + coerceValue(args, i, factory, params[i], types[i]); + } + Class varargType = types[varargIndex].getComponentType(); + int length = params.length - varargIndex; + Object array = null; + if (length == 1) { + Object source = params[varargIndex]; + if (source != null && source.getClass().isArray()) { + if (types[varargIndex].isInstance(source)) { // use source array as is + array = source; + } else { // coerce array elements + length = Array.getLength(source); + array = Array.newInstance(varargType, length); + for (int i = 0; i < length; i++) { + coerceValue(array, i, factory, Array.get(source, i), varargType); + } + } + } else { // single element array + array = Array.newInstance(varargType, 1); + coerceValue(array, 0, factory, source, varargType); + } + } else { + array = Array.newInstance(varargType, length); + for (int i = 0; i < length; i++) { + coerceValue(array, i, factory, params[varargIndex + i], varargType); + } + } + args[varargIndex] = array; + } else { + if (params.length != args.length) { + throw new ELException("Bad argument count"); + } + for (int i = 0; i < args.length; i++) { + coerceValue(args, i, factory, params[i], types[i]); + } + } + return args; + } + + private void coerceValue( + Object array, + int index, + ExpressionFactory factory, + Object value, + Class type + ) { + if (value != null || type.isPrimitive()) { + Array.set(array, index, factory.coerceToType(value, type)); + } + } + + /** + * Test whether the given base should be resolved by this ELResolver. + * + * @param base + * The bean to analyze. + * @param property + * The name of the property to analyze. Will be coerced to a String. + * @return base != null + */ + private final boolean isResolvable(Object base) { + return base != null; + } + + /** + * Lookup BeanProperty for the given (base, property) pair. + * + * @param base + * The bean to analyze. + * @param property + * The name of the property to analyze. Will be coerced to a String. + * @return The BeanProperty representing (base, property). + * @throws PropertyNotFoundException + * if no BeanProperty can be found. + */ + private final BeanProperty toBeanProperty(Object base, Object property) { + BeanProperties beanProperties = cache.get(base.getClass()); + if (beanProperties == null) { + BeanProperties newBeanProperties = new BeanProperties(base.getClass()); + beanProperties = cache.putIfAbsent(base.getClass(), newBeanProperties); + if (beanProperties == null) { // put succeeded, use new value + beanProperties = newBeanProperties; + } + } + BeanProperty beanProperty = property == null + ? null + : beanProperties.getBeanProperty(property.toString()); + if (beanProperty == null) { + throw new PropertyNotFoundException( + "Could not find property " + property + " in " + base.getClass() + ); + } + return beanProperty; + } + + /** + * This method is not part of the API, though it can be used (reflectively) by clients of this + * class to remove entries from the cache when the beans are being unloaded. + * + * Note: this method is present in the reference implementation, so we're adding it here to ease + * migration. + * + * @param classLoader + * The classLoader used to load the beans. + */ + @SuppressWarnings("unused") + private final void purgeBeanClasses(ClassLoader loader) { + Iterator> classes = cache.keySet().iterator(); + while (classes.hasNext()) { + if (loader == classes.next().getClassLoader()) { + classes.remove(); + } + } + } +} From 7cf04c01f61f5e39a2e388452fbebb4cd1a86c67 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 23 Jan 2023 14:51:59 -0500 Subject: [PATCH 149/637] Use in-repo BeanELResolver with minor modifications to perform best method finding --- .../jinjava/el/ext/BeanELResolver.java | 24 +- .../jinjava/el/ext/JinjavaBeanELResolver.java | 224 +----------------- 2 files changed, 26 insertions(+), 222 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java b/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java index b005a71d8..90c9b60d6 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java @@ -151,7 +151,8 @@ private static Method findPublicAccessibleMethod(Method method) { return null; } - private static Method findAccessibleMethod(Method method) { + // Changed modifier to protected + protected static Method findAccessibleMethod(Method method) { Method result = findPublicAccessibleMethod(method); if (result == null && method != null && Modifier.isPublic(method.getModifiers())) { result = method; @@ -520,7 +521,7 @@ public Object invoke( params = new Object[0]; } String name = method.toString(); - Method target = findMethod(base, name, paramTypes, params.length); + Method target = findMethod(base, name, paramTypes, params, params.length); if (target == null) { throw new MethodNotFoundException( "Cannot find method " + @@ -547,7 +548,14 @@ public Object invoke( return result; } - private Method findMethod(Object base, String name, Class[] types, int paramCount) { + // Changed modifier to protected; Added `Object[] params` parameter + protected Method findMethod( + Object base, + String name, + Class[] types, + Object[] params, + int paramCount + ) { if (types != null) { try { return findAccessibleMethod(base.getClass().getMethod(name, types)); @@ -658,11 +666,9 @@ private void coerceValue( * * @param base * The bean to analyze. - * @param property - * The name of the property to analyze. Will be coerced to a String. * @return base != null */ - private final boolean isResolvable(Object base) { + private boolean isResolvable(Object base) { return base != null; } @@ -677,7 +683,7 @@ private final boolean isResolvable(Object base) { * @throws PropertyNotFoundException * if no BeanProperty can be found. */ - private final BeanProperty toBeanProperty(Object base, Object property) { + private BeanProperty toBeanProperty(Object base, Object property) { BeanProperties beanProperties = cache.get(base.getClass()); if (beanProperties == null) { BeanProperties newBeanProperties = new BeanProperties(base.getClass()); @@ -704,11 +710,11 @@ private final BeanProperty toBeanProperty(Object base, Object property) { * Note: this method is present in the reference implementation, so we're adding it here to ease * migration. * - * @param classLoader + * @param loader * The classLoader used to load the beans. */ @SuppressWarnings("unused") - private final void purgeBeanClasses(ClassLoader loader) { + private void purgeBeanClasses(ClassLoader loader) { Iterator> classes = cache.keySet().iterator(); while (classes.hasNext()) { if (loader == classes.next().getClassLoader()) { diff --git a/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java index 6088ded05..cb0cc46a8 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java @@ -6,19 +6,13 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.util.EagerReconstructionUtils; import java.lang.invoke.MethodType; -import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.LinkedList; import java.util.List; import java.util.Set; -import javax.el.BeanELResolver; import javax.el.ELContext; -import javax.el.ELException; -import javax.el.ExpressionFactory; import javax.el.MethodNotFoundException; /** @@ -65,8 +59,6 @@ public class JinjavaBeanELResolver extends BeanELResolver { .add("merge") .build(); - private ExpressionFactory defaultFactory; - /** * Creates a new read/write {@link JinjavaBeanELResolver}. */ @@ -132,12 +124,9 @@ public Object invoke( ) ); } - Object result; - if (paramTypes == null) { - result = invokeBestMatch(context, base, method, params); - } else { - result = super.invoke(context, base, method, paramTypes, params); - } + + Object result = super.invoke(context, base, method, paramTypes, params); + if (isRestrictedClass(result)) { throw new MethodNotFoundException( "Cannot find method '" + method + "' in " + base.getClass() @@ -147,54 +136,17 @@ public Object invoke( return result; } - protected Object invokeBestMatch( - ELContext context, + @Override + protected Method findMethod( Object base, - Object method, - Object[] params + String name, + Class[] types, + Object[] params, + int paramCount ) { - if (context == null) { - throw new NullPointerException(); - } else { - Object result = null; - if (base != null) { - if (params == null) { - params = new Object[0]; - } - - String name = method.toString(); - Method target = this.findMethod(base, name, params, params.length); - if (target == null) { - throw new MethodNotFoundException( - "Cannot find method " + - name + - " with " + - params.length + - " parameters in " + - base.getClass() - ); - } - - try { - result = - target.invoke( - base, - this.coerceParams(this.getExpressionFactory(context), target, params) - ); - } catch (InvocationTargetException var10) { - throw new ELException(var10.getCause()); - } catch (IllegalAccessException var11) { - throw new ELException(var11); - } - - context.setPropertyResolved(true); - } - - return result; + if (types != null) { + return super.findMethod(base, name, types, params, paramCount); } - } - - protected Method findMethod(Object base, String name, Object[] params, int paramCount) { Method varArgsMethod = null; Method[] methods = base.getClass().getMethods(); @@ -256,160 +208,6 @@ private static int pickMoreSpecificMethod(Method methodA, Method methodB) { return 1; } - private static Method findAccessibleMethod(Method method) { - Method result = findPublicAccessibleMethod(method); - if (result == null && method != null && Modifier.isPublic(method.getModifiers())) { - result = method; - - try { - method.setAccessible(true); - } catch (SecurityException var3) { - result = null; - } - } - - return result; - } - - private static Method findPublicAccessibleMethod(Method method) { - if (method != null && Modifier.isPublic(method.getModifiers())) { - if ( - !method.isAccessible() && - !Modifier.isPublic(method.getDeclaringClass().getModifiers()) - ) { - Class[] arr$ = method.getDeclaringClass().getInterfaces(); - int len$ = arr$.length; - - for (int i$ = 0; i$ < len$; ++i$) { - Class cls = arr$[i$]; - Method mth = null; - - try { - mth = - findPublicAccessibleMethod( - cls.getMethod(method.getName(), method.getParameterTypes()) - ); - if (mth != null) { - return mth; - } - } catch (NoSuchMethodException var8) {} - } - - Class cls = method.getDeclaringClass().getSuperclass(); - if (cls != null) { - Method mth = null; - - try { - mth = - findPublicAccessibleMethod( - cls.getMethod(method.getName(), method.getParameterTypes()) - ); - if (mth != null) { - return mth; - } - } catch (NoSuchMethodException var7) {} - } - - return null; - } else { - return method; - } - } else { - return null; - } - } - - private Object[] coerceParams( - ExpressionFactory factory, - Method method, - Object[] params - ) { - Class[] types = method.getParameterTypes(); - Object[] args = new Object[types.length]; - int varargIndex; - if (method.isVarArgs()) { - varargIndex = types.length - 1; - if (params.length < varargIndex) { - throw new ELException("Bad argument count"); - } - - for (int i = 0; i < varargIndex; ++i) { - this.coerceValue(args, i, factory, params[i], types[i]); - } - - Class varargType = types[varargIndex].getComponentType(); - int length = params.length - varargIndex; - Object array = null; - if (length == 1) { - Object source = params[varargIndex]; - if (source != null && source.getClass().isArray()) { - if (types[varargIndex].isInstance(source)) { - array = source; - } else { - length = Array.getLength(source); - array = Array.newInstance(varargType, length); - - for (int i = 0; i < length; ++i) { - this.coerceValue(array, i, factory, Array.get(source, i), varargType); - } - } - } else { - array = Array.newInstance(varargType, 1); - this.coerceValue(array, 0, factory, source, varargType); - } - } else { - array = Array.newInstance(varargType, length); - - for (int i = 0; i < length; ++i) { - this.coerceValue(array, i, factory, params[varargIndex + i], varargType); - } - } - - args[varargIndex] = array; - } else { - if (params.length != args.length) { - throw new ELException("Bad argument count"); - } - - for (varargIndex = 0; varargIndex < args.length; ++varargIndex) { - this.coerceValue( - args, - varargIndex, - factory, - params[varargIndex], - types[varargIndex] - ); - } - } - - return args; - } - - private void coerceValue( - Object array, - int index, - ExpressionFactory factory, - Object value, - Class type - ) { - if (value != null || type.isPrimitive()) { - Array.set(array, index, factory.coerceToType(value, type)); - } - } - - private ExpressionFactory getExpressionFactory(ELContext context) { - Object obj = context.getContext(ExpressionFactory.class); - if (obj instanceof ExpressionFactory) { - return (ExpressionFactory) obj; - } else { - if (this.defaultFactory == null) { - this.defaultFactory = ExpressionFactory.newInstance(); - } - - return this.defaultFactory; - } - } - private String validatePropertyName(Object property) { String propertyName = transformPropertyName(property); From e05332eb053939e9d191389149f84d90b6d17d6f Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 19 Jan 2023 10:47:28 -0500 Subject: [PATCH 150/637] Extend `Writer` to limit the length of the string which can be written to without using reflection. Also adds a new method `writeSelf` to PyishSerializable to allow piecewise writing to the jsonGenerator. --- .../serialization/MapEntrySerializer.java | 37 +++++++--- .../serialization/PyishObjectMapper.java | 44 +++++++++--- .../serialization/PyishSerializable.java | 25 ++++++- .../serialization/PyishSerializer.java | 3 +- .../SizeLimitingJsonProcessingException.java | 10 +++ .../serialization/SizeLimitingWriter.java | 69 +++++++++++++++++++ .../jinjava/util/EagerExpressionResolver.java | 4 +- .../hubspot/jinjava/util/WhitespaceUtils.java | 12 ++++ .../jinjava/lib/filter/EscapeFilterTest.java | 15 +++- .../serialization/PyishObjectMapperTest.java | 27 ++++++++ 10 files changed, 221 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingJsonProcessingException.java create mode 100644 src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingWriter.java diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java index b82d878bc..9e666e14a 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java @@ -2,28 +2,49 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.CharArrayWriter; import java.io.IOException; -import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicInteger; -public class MapEntrySerializer extends JsonSerializer { +public class MapEntrySerializer extends JsonSerializer> { public static final MapEntrySerializer INSTANCE = new MapEntrySerializer(); private MapEntrySerializer() {} @Override public void serialize( - Map.Entry object, + Entry entry, JsonGenerator jsonGenerator, SerializerProvider serializerProvider ) throws IOException { - String key = PyishObjectMapper.PYISH_OBJECT_WRITER.writeValueAsString( - object.getKey() - ); - String value = PyishObjectMapper.PYISH_OBJECT_WRITER.writeValueAsString( - object.getValue() + AtomicInteger remainingLength = (AtomicInteger) serializerProvider.getAttribute( + SizeLimitingWriter.REMAINING_LENGTH_ATTRIBUTE ); + String key; + String value; + if (remainingLength != null) { + ObjectWriter objectWriter = PyishObjectMapper.PYISH_OBJECT_WRITER.withAttribute( + SizeLimitingWriter.REMAINING_LENGTH_ATTRIBUTE, + remainingLength + ); + key = objectWriter.writeValueAsString(entry.getKey()); + SizeLimitingWriter sizeLimitingWriter = new SizeLimitingWriter( + new CharArrayWriter(), + remainingLength + ); + objectWriter.writeValue( + new SizeLimitingWriter(new CharArrayWriter(), remainingLength), + entry.getValue() + ); + value = sizeLimitingWriter.toString(); + } else { + key = PyishObjectMapper.PYISH_OBJECT_WRITER.writeValueAsString(entry.getKey()); + value = PyishObjectMapper.PYISH_OBJECT_WRITER.writeValueAsString(entry.getValue()); + } jsonGenerator.writeRawValue(String.format("fn:map_entry(%s, %s)", key, value)); } } diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 90e9c53e2..d616f7596 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -2,16 +2,20 @@ import com.fasterxml.jackson.core.JsonFactoryBuilder; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.util.WhitespaceUtils; +import java.io.CharArrayWriter; import java.io.IOException; +import java.io.Writer; import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; public class PyishObjectMapper { public static final ObjectWriter PYISH_OBJECT_WRITER; @@ -19,8 +23,10 @@ public class PyishObjectMapper { static { ObjectMapper mapper = new ObjectMapper( new JsonFactoryBuilder().quoteChar('\'').build() - ) - .registerModule( + ); + + mapper = + mapper.registerModule( new SimpleModule() .setSerializerModifier(PyishBeanSerializerModifier.INSTANCE) .addSerializer(PyishSerializable.class, PyishSerializer.INSTANCE) @@ -40,16 +46,36 @@ public static String getAsUnquotedPyishString(Object val) { public static String getAsPyishString(Object val) { try { return getAsPyishStringOrThrow(val); - } catch (JsonProcessingException e) { + } catch (IOException e) { + if (e instanceof SizeLimitingJsonProcessingException) { + throw new DeferredValueException(String.format("%s: %s", e.getMessage(), val)); + } return Objects.toString(val, ""); } } - public static String getAsPyishStringOrThrow(Object val) - throws JsonProcessingException { - String string = PYISH_OBJECT_WRITER.writeValueAsString(val); - JinjavaInterpreter.checkOutputSize(string); - return string; + public static String getAsPyishStringOrThrow(Object val) throws IOException { + ObjectWriter objectWriter = PYISH_OBJECT_WRITER; + Writer writer; + Optional maxOutputSize = JinjavaInterpreter + .getCurrentMaybe() + .map(interpreter -> interpreter.getConfig().getMaxOutputSize()) + .filter(max -> max > 0); + if (maxOutputSize.isPresent()) { + AtomicInteger remainingLength = new AtomicInteger( + (int) Math.min(Integer.MAX_VALUE, maxOutputSize.get()) + ); + objectWriter = + objectWriter.withAttribute( + SizeLimitingWriter.REMAINING_LENGTH_ATTRIBUTE, + remainingLength + ); + writer = new SizeLimitingWriter(new CharArrayWriter(), remainingLength); + } else { + writer = new CharArrayWriter(); + } + objectWriter.writeValue(writer, val); + return writer.toString(); } public static class NullKeySerializer extends JsonSerializer { diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java index 4bab88047..9030cabc4 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java @@ -1,10 +1,13 @@ package com.hubspot.jinjava.objects.serialization; import com.fasterxml.jackson.core.JsonFactoryBuilder; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializerProvider; import com.hubspot.jinjava.objects.PyWrapper; +import java.io.IOException; import java.util.Objects; public interface PyishSerializable extends PyWrapper { @@ -17,13 +20,29 @@ public interface PyishSerializable extends PyWrapper { * Allows for a class to specify a custom string representation in Jinjava. * By default, this will get a json representation of the object, * but this method can be overridden to provide a custom representation. - * This should use double quotes to wrap json keys/values. - * @return A pyish/json string representation of the object + * This method will be used by {@link #writeSelf(JsonGenerator, SerializerProvider)} + * to specify what will be written to the json generator. + * @return A pyish/json CharSequence representation of the object */ - default String toPyishString() { + default CharSequence toPyishString() { return writeValueAsString(this); } + /** + * Allows for a class to specify how its pyish string representation will + * be written to the json generator. + * If the pyish string representation of this object can be very large, it's recommended + * to override this method instead of {@link #toPyishString()} so that jsonGenerator + * can be written to multiple times, allowing multiple limit checks to occur. + */ + default void writeSelf( + JsonGenerator jsonGenerator, + SerializerProvider serializerProvider + ) + throws IOException { + jsonGenerator.writeRawValue(toPyishString().toString()); + } + /** * Utility method to assist implementations of PyishSerializable in * overriding toPyishString(). diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializer.java index 489a742a1..a6707fe26 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializer.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializer.java @@ -12,7 +12,6 @@ public class PyishSerializer extends JsonSerializer { private PyishSerializer() {} - @Override public void serialize( Object object, JsonGenerator jsonGenerator, @@ -27,7 +26,7 @@ public void serialize( .map(interpreter -> interpreter.wrap(object)) .orElse(object); if (wrappedObject instanceof PyishSerializable) { - jsonGenerator.writeRawValue(((PyishSerializable) wrappedObject).toPyishString()); + ((PyishSerializable) wrappedObject).writeSelf(jsonGenerator, serializerProvider); } else if (wrappedObject instanceof Boolean) { jsonGenerator.writeBoolean((Boolean) wrappedObject); } else if (wrappedObject instanceof Number) { diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingJsonProcessingException.java b/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingJsonProcessingException.java new file mode 100644 index 000000000..eb0736ae4 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingJsonProcessingException.java @@ -0,0 +1,10 @@ +package com.hubspot.jinjava.objects.serialization; + +import com.fasterxml.jackson.core.JsonProcessingException; + +public class SizeLimitingJsonProcessingException extends JsonProcessingException { + + protected SizeLimitingJsonProcessingException(int maxSize, int attemptedSize) { + super("Max length of {} chars reached when serializing. {} chars attempted."); + } +} diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingWriter.java b/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingWriter.java new file mode 100644 index 000000000..a37b554a1 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingWriter.java @@ -0,0 +1,69 @@ +package com.hubspot.jinjava.objects.serialization; + +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.concurrent.atomic.AtomicInteger; + +public class SizeLimitingWriter extends Writer { + public static final String REMAINING_LENGTH_ATTRIBUTE = "remainingLength"; + private final CharArrayWriter charArrayWriter; + private final AtomicInteger remainingLength; + private final int startingLength; + + public SizeLimitingWriter( + CharArrayWriter charArrayWriter, + AtomicInteger remainingLength + ) { + this.charArrayWriter = charArrayWriter; + this.remainingLength = remainingLength; + startingLength = remainingLength.get(); + } + + @Override + public void write(int c) throws SizeLimitingJsonProcessingException { + checkMaxSize(1); + charArrayWriter.write(c); + } + + @Override + public void write(char[] c, int off, int len) + throws SizeLimitingJsonProcessingException { + checkMaxSize(len); + charArrayWriter.write(c, off, len); + } + + @Override + public void write(String str, int off, int len) + throws SizeLimitingJsonProcessingException { + checkMaxSize(len); + charArrayWriter.write(str, off, len); + } + + private void checkMaxSize(int extra) throws SizeLimitingJsonProcessingException { + if (remainingLength.addAndGet(extra * -1) < 0) { + throw new SizeLimitingJsonProcessingException( + charArrayWriter.size() + extra, + startingLength + ); + } + } + + public char[] toCharArray() { + return charArrayWriter.toCharArray(); + } + + public int size() { + return charArrayWriter.size(); + } + + public String toString() { + return charArrayWriter.toString(); + } + + @Override + public void flush() throws IOException {} + + @Override + public void close() throws IOException {} +} diff --git a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java index 153dcafba..06c059272 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java @@ -1,6 +1,5 @@ package com.hubspot.jinjava.util; -import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Primitives; import com.hubspot.jinjava.el.ext.DeferredParsingException; @@ -17,6 +16,7 @@ import com.hubspot.jinjava.tree.parse.ExpressionToken; import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult.ResolutionState; +import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -113,7 +113,7 @@ public static String getValueAsJinjavaStringSafe(Object val) { return pyishString; } } - } catch (JsonProcessingException | OutputTooBigException ignored) {} + } catch (IOException | OutputTooBigException ignored) {} throw new DeferredValueException("Can not convert deferred result to string"); } diff --git a/src/main/java/com/hubspot/jinjava/util/WhitespaceUtils.java b/src/main/java/com/hubspot/jinjava/util/WhitespaceUtils.java index 43eb5f85c..5183910c0 100644 --- a/src/main/java/com/hubspot/jinjava/util/WhitespaceUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/WhitespaceUtils.java @@ -2,6 +2,7 @@ import com.google.common.base.Strings; import com.hubspot.jinjava.interpret.InterpretException; +import javax.annotation.Nullable; public final class WhitespaceUtils { private static final char[] QUOTE_CHARS = new char[] { '\'', '"' }; @@ -142,5 +143,16 @@ public static String unwrap(String s, String prefix, String suffix) { return s.substring(start + prefix.length(), end - suffix.length() + 1); } + @Nullable + public static StringBuilder quoteIfNotNull(CharSequence charSequence) { + if (charSequence != null) { + return new StringBuilder(charSequence.length() + 2) + .append('\'') + .append(charSequence) + .append('\''); + } + return null; + } + private WhitespaceUtils() {} } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java index 226f3cfae..7368c49c5 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java @@ -54,7 +54,7 @@ public void testNewStringReplaceIsFaster() { assertThat(newResult).isEqualTo(oldResult); System.out.printf("New: %d Old:%d\n", newTime.toMillis(), oldTime.toMillis()); - int speedUpFactor = 2; // On M1, it is between 50 and 100 times faster. + int speedUpFactor = getVersion() < 17 ? 2 : 1; // On M1, it is between 50 and 100 times faster. Difference is much smaller on java 17 assertThat(newTime.toMillis()).isLessThan(oldTime.toMillis() / speedUpFactor); } @@ -65,4 +65,17 @@ private static String fixture(String name) { throw new RuntimeException(e); } } + + private static int getVersion() { + String version = System.getProperty("java.version"); + if (version.startsWith("1.")) { + version = version.substring(2, 3); + } else { + int dotIndex = version.indexOf("."); + if (dotIndex != -1) { + version = version.substring(0, dotIndex); + } + } + return Integer.parseInt(version); + } } diff --git a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java index 17dc8c5e6..b0d3c000f 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -1,12 +1,14 @@ package com.hubspot.jinjava.objects.serialization; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.Test; @@ -39,4 +41,29 @@ public void itSerializesMapWithNullValues() { assertThat(PyishObjectMapper.getAsPyishString(map)) .isEqualTo("{'foobar': null, 'foo': 'bar'} "); } + + @Test + public void itLimitsDepth() { + final List original = new ArrayList<>(); + List list = original; + for (int i = 0; i < 20; i++) { + List temp = new ArrayList<>(); + list.add("abcdefghijklmnopqrstuvwxyz"); + list.add(temp); + list = temp; + } + list.add("a"); + list.add(original); + try { + Jinjava jinjava = new Jinjava( + JinjavaConfig.newBuilder().withMaxOutputSize(10000).build() + ); + JinjavaInterpreter.pushCurrent(jinjava.newInterpreter()); + assertThatThrownBy(() -> PyishObjectMapper.getAsPyishStringOrThrow(original)) + .as("The string to be serialized is larger than the max output size") + .isInstanceOf(SizeLimitingJsonProcessingException.class); + } finally { + JinjavaInterpreter.popCurrent(); + } + } } From d34bc172166a03f7ba0b22571c658b0966c79b1f Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 25 Jan 2023 10:51:59 -0500 Subject: [PATCH 151/637] Fix map entry wrapping with limiting and adjust exceptions --- .../serialization/MapEntrySerializer.java | 5 +---- .../serialization/PyishObjectMapper.java | 17 +++++++++++++- .../SizeLimitingJsonProcessingException.java | 20 ++++++++++++++++- .../serialization/SizeLimitingWriter.java | 4 ++-- .../lib/tag/eager/EagerTagDecoratorTest.java | 7 +++--- .../serialization/PyishObjectMapperTest.java | 22 +++++++++++++++++++ 6 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java index 9e666e14a..c9baa8508 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java @@ -36,10 +36,7 @@ public void serialize( new CharArrayWriter(), remainingLength ); - objectWriter.writeValue( - new SizeLimitingWriter(new CharArrayWriter(), remainingLength), - entry.getValue() - ); + objectWriter.writeValue(sizeLimitingWriter, entry.getValue()); value = sizeLimitingWriter.toString(); } else { key = PyishObjectMapper.PYISH_OBJECT_WRITER.writeValueAsString(entry.getKey()); diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index d616f7596..dd72770a4 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.util.WhitespaceUtils; import java.io.CharArrayWriter; import java.io.IOException; @@ -48,7 +49,21 @@ public static String getAsPyishString(Object val) { return getAsPyishStringOrThrow(val); } catch (IOException e) { if (e instanceof SizeLimitingJsonProcessingException) { - throw new DeferredValueException(String.format("%s: %s", e.getMessage(), val)); + if ( + JinjavaInterpreter + .getCurrentMaybe() + .map( + interpreter -> interpreter.getConfig().getExecutionMode().useEagerParser() + ) + .orElse(false) + ) { + throw new DeferredValueException(String.format("%s: %s", e.getMessage(), val)); + } else { + throw new OutputTooBigException( + ((SizeLimitingJsonProcessingException) e).getMaxSize(), + ((SizeLimitingJsonProcessingException) e).getAttemptedSize() + ); + } } return Objects.toString(val, ""); } diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingJsonProcessingException.java b/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingJsonProcessingException.java index eb0736ae4..063737f57 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingJsonProcessingException.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingJsonProcessingException.java @@ -3,8 +3,26 @@ import com.fasterxml.jackson.core.JsonProcessingException; public class SizeLimitingJsonProcessingException extends JsonProcessingException { + private final int maxSize; + private final int attemptedSize; protected SizeLimitingJsonProcessingException(int maxSize, int attemptedSize) { - super("Max length of {} chars reached when serializing. {} chars attempted."); + super( + String.format( + "Max length of %d chars reached when serializing. %d chars attempted.", + maxSize, + attemptedSize + ) + ); + this.maxSize = maxSize; + this.attemptedSize = attemptedSize; + } + + public int getAttemptedSize() { + return attemptedSize; + } + + public int getMaxSize() { + return maxSize; } } diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingWriter.java b/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingWriter.java index a37b554a1..68b0d3ef8 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingWriter.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingWriter.java @@ -43,8 +43,8 @@ public void write(String str, int off, int len) private void checkMaxSize(int extra) throws SizeLimitingJsonProcessingException { if (remainingLength.addAndGet(extra * -1) < 0) { throw new SizeLimitingJsonProcessingException( - charArrayWriter.size() + extra, - startingLength + startingLength, + charArrayWriter.size() + extra ); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecoratorTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecoratorTest.java index 74424a156..1ace159a3 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecoratorTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecoratorTest.java @@ -2,7 +2,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.hubspot.jinjava.BaseInterpretingTest; import com.hubspot.jinjava.JinjavaConfig; @@ -10,7 +12,6 @@ import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.OutputTooBigException; -import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; import com.hubspot.jinjava.lib.fn.ELFunctionDefinition; import com.hubspot.jinjava.lib.tag.Tag; import com.hubspot.jinjava.mode.EagerExecutionMode; @@ -124,7 +125,7 @@ public void itLimitsTagLength() { () -> eagerTagDecorator.getEagerTagImage((TagToken) tagNode.getMaster(), interpreter) ) - .isInstanceOf(OutputTooBigException.class); + .isInstanceOf(DeferredValueException.class); } @Test diff --git a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java index b0d3c000f..dcb80204d 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -5,6 +5,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; import java.util.ArrayList; import java.util.HashMap; @@ -33,6 +36,25 @@ public void itSerializesMapEntrySet() throws JsonProcessingException { .isEqualTo("[fn:map_entry('bar', {'foobar': []} ), fn:map_entry('foo', 'bar')]"); } + @Test + public void itSerializesMapEntrySetWithLimit() throws JsonProcessingException { + SizeLimitingPyMap map = new SizeLimitingPyMap(new HashMap<>(), 10); + map.put("foo", "bar"); + map.put("bar", ImmutableMap.of("foobar", new ArrayList<>())); + + Jinjava jinjava = new Jinjava( + JinjavaConfig.newBuilder().withMaxOutputSize(10000).build() + ); + try { + JinjavaInterpreter.pushCurrent(jinjava.newInterpreter()); + String result = PyishObjectMapper.getAsPyishString(map.items()); + assertThat(result) + .isEqualTo("[fn:map_entry('bar', {'foobar': []} ), fn:map_entry('foo', 'bar')]"); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + @Test public void itSerializesMapWithNullValues() { Map map = new SizeLimitingPyMap(new HashMap<>(), 10); From 4424f3d6ae612b177408d77df0f3de7116c33023 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 25 Jan 2023 11:21:18 -0500 Subject: [PATCH 152/637] Allow Pyish serialization to be more efficient by passing along a string builder to new method appendPyishString(sb) --- .../hubspot/jinjava/el/ext/NamedParameter.java | 7 +++++-- .../jinjava/interpret/LazyReference.java | 2 +- .../com/hubspot/jinjava/objects/Namespace.java | 4 ++-- .../hubspot/jinjava/objects/date/PyishDate.java | 13 +++++++------ .../serialization/PyishSerializable.java | 17 ++++++++++++++++- .../lib/filter/RejectAttrFilterTest.java | 2 +- 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java b/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java index 39170e65e..5875a4978 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java @@ -26,7 +26,10 @@ public String toString() { } @Override - public String toPyishString() { - return name + "=" + PyishSerializable.writeValueAsString(value); + public StringBuilder appendPyishString(StringBuilder sb) { + return sb + .append(name) + .append('=') + .append(PyishSerializable.writeValueAsString(value)); } } diff --git a/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java b/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java index 6a40714b5..383171264 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java +++ b/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java @@ -24,7 +24,7 @@ public void setReferenceKey(String referenceKey) { } @Override - public String toPyishString() { + public CharSequence toPyishString() { return getReferenceKey(); } } diff --git a/src/main/java/com/hubspot/jinjava/objects/Namespace.java b/src/main/java/com/hubspot/jinjava/objects/Namespace.java index da3a42350..3d3c5fe3a 100644 --- a/src/main/java/com/hubspot/jinjava/objects/Namespace.java +++ b/src/main/java/com/hubspot/jinjava/objects/Namespace.java @@ -20,7 +20,7 @@ public Namespace(Map map, int maxSize) { } @Override - public String toPyishString() { - return String.format("namespace(%s)", PyishSerializable.super.toPyishString()); + public StringBuilder appendPyishString(StringBuilder sb) { + return PyishSerializable.super.appendPyishString(sb.append("namespace(")).append(')'); } } 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 9ffd07e5e..2087dc37f 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java @@ -158,11 +158,12 @@ public boolean equals(Object obj) { } @Override - public String toPyishString() { - return String.format( - "'%s'|strtotime(%s)", - strftime(FULL_DATE_FORMAT), - PyishObjectMapper.getAsPyishString(FULL_DATE_FORMAT) - ); + public StringBuilder appendPyishString(StringBuilder sb) { + return sb + .append('\'') + .append(strftime(FULL_DATE_FORMAT)) + .append("'|strtotime(") + .append(PyishObjectMapper.getAsPyishString(FULL_DATE_FORMAT)) + .append(')'); } } diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java index 9030cabc4..e6642b402 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java @@ -28,9 +28,24 @@ default CharSequence toPyishString() { return writeValueAsString(this); } + /** + * Allows for a class to append the custom string representation in Jinjava. + * This method can be overridden to append a custom representation. + * This method will be used by {@link #writeSelf(JsonGenerator, SerializerProvider)} + * to specify what will be written to the json generator. + * If the pyish string representation of this object is composed of several strings, + * it's recommended to override this method instead of {@link #toPyishString()} + * @param sb StringBuilder to append the pyish string representation to. + * @return The same StringBuilder sb with an appended result + */ + default StringBuilder appendPyishString(StringBuilder sb) { + return sb.append(toPyishString()); + } + /** * Allows for a class to specify how its pyish string representation will * be written to the json generator. + * * If the pyish string representation of this object can be very large, it's recommended * to override this method instead of {@link #toPyishString()} so that jsonGenerator * can be written to multiple times, allowing multiple limit checks to occur. @@ -40,7 +55,7 @@ default void writeSelf( SerializerProvider serializerProvider ) throws IOException { - jsonGenerator.writeRawValue(toPyishString().toString()); + jsonGenerator.writeRawValue(appendPyishString(new StringBuilder()).toString()); } /** diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/RejectAttrFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/RejectAttrFilterTest.java index b4f9ca7e7..9ab84b89a 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/RejectAttrFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/RejectAttrFilterTest.java @@ -112,7 +112,7 @@ public String toString() { } @Override - public String toPyishString() { + public CharSequence toPyishString() { return toString(); } } From 805a4acab3d7a68d1e0dbeb4dfff4fe27662f368 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 25 Jan 2023 11:27:31 -0500 Subject: [PATCH 153/637] Update PyishSerializable method comments --- .../objects/serialization/PyishObjectMapper.java | 6 ++---- .../objects/serialization/PyishSerializable.java | 13 +++++++++---- .../jinjava/util/EagerExpressionResolverTest.java | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index dd72770a4..451408214 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -24,10 +24,8 @@ public class PyishObjectMapper { static { ObjectMapper mapper = new ObjectMapper( new JsonFactoryBuilder().quoteChar('\'').build() - ); - - mapper = - mapper.registerModule( + ) + .registerModule( new SimpleModule() .setSerializerModifier(PyishBeanSerializerModifier.INSTANCE) .addSerializer(PyishSerializable.class, PyishSerializer.INSTANCE) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java index e6642b402..d72afffd7 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java @@ -16,23 +16,26 @@ public interface PyishSerializable extends PyWrapper { ) .writer(PyishPrettyPrinter.INSTANCE) .with(PyishCharacterEscapes.INSTANCE); + /** * Allows for a class to specify a custom string representation in Jinjava. * By default, this will get a json representation of the object, * but this method can be overridden to provide a custom representation. - * This method will be used by {@link #writeSelf(JsonGenerator, SerializerProvider)} - * to specify what will be written to the json generator. + * This should no longer be called directly, + * {@link #writeSelf(JsonGenerator, SerializerProvider)} or + * {@link #appendPyishString(StringBuilder)} should instead be used. * @return A pyish/json CharSequence representation of the object */ + @Deprecated default CharSequence toPyishString() { return writeValueAsString(this); } /** * Allows for a class to append the custom string representation in Jinjava. - * This method can be overridden to append a custom representation. * This method will be used by {@link #writeSelf(JsonGenerator, SerializerProvider)} * to specify what will be written to the json generator. + *

* If the pyish string representation of this object is composed of several strings, * it's recommended to override this method instead of {@link #toPyishString()} * @param sb StringBuilder to append the pyish string representation to. @@ -45,10 +48,12 @@ default StringBuilder appendPyishString(StringBuilder sb) { /** * Allows for a class to specify how its pyish string representation will * be written to the json generator. - * + *

* If the pyish string representation of this object can be very large, it's recommended * to override this method instead of {@link #toPyishString()} so that jsonGenerator * can be written to multiple times, allowing multiple limit checks to occur. + * @param jsonGenerator The JsonGenerator to write to. + * @param serializerProvider Provides default value serialization and attributes stored on the ObjectWriter if needed. */ default void writeSelf( JsonGenerator jsonGenerator, diff --git a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java index eb3c63c2a..aac2a08dd 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java @@ -311,7 +311,7 @@ public void itSerializesDateProperly() { EagerExpressionResult eagerExpressionResult = eagerResolveExpression("date"); assertThat(WhitespaceUtils.unquoteAndUnescape(eagerExpressionResult.toString())) - .isEqualTo(date.toPyishString()); + .isEqualTo(date.appendPyishString(new StringBuilder()).toString()); interpreter.render( "{% set foo = " + PyishObjectMapper.getAsPyishString(ImmutableMap.of("a", date)) + From 41dbdedf04cae56012c8660843a02f042547a3c7 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 25 Jan 2023 13:18:42 -0500 Subject: [PATCH 154/637] Make LengthLimitingStringBuilder implement Appendable and use it in default writeSelf method --- .../jinjava/el/ext/NamedParameter.java | 7 +++- .../hubspot/jinjava/objects/Namespace.java | 9 ++++- .../jinjava/objects/date/PyishDate.java | 7 +++- ...engthLimitingJsonProcessingException.java} | 4 +- ...gWriter.java => LengthLimitingWriter.java} | 14 +++---- .../serialization/MapEntrySerializer.java | 10 ++--- .../serialization/PyishObjectMapper.java | 10 ++--- .../serialization/PyishSerializable.java | 26 +++++++++--- .../util/LengthLimitingStringBuilder.java | 40 ++++++++++++++++--- .../serialization/PyishObjectMapperTest.java | 2 +- .../util/EagerExpressionResolverTest.java | 3 +- .../util/LengthLimitingStringBuilderTest.java | 5 ++- 12 files changed, 97 insertions(+), 40 deletions(-) rename src/main/java/com/hubspot/jinjava/objects/serialization/{SizeLimitingJsonProcessingException.java => LengthLimitingJsonProcessingException.java} (75%) rename src/main/java/com/hubspot/jinjava/objects/serialization/{SizeLimitingWriter.java => LengthLimitingWriter.java} (77%) diff --git a/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java b/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java index 5875a4978..9cf11269b 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.el.ext; import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; import java.util.Objects; public class NamedParameter implements PyishSerializable { @@ -26,8 +27,10 @@ public String toString() { } @Override - public StringBuilder appendPyishString(StringBuilder sb) { - return sb + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable .append(name) .append('=') .append(PyishSerializable.writeValueAsString(value)); diff --git a/src/main/java/com/hubspot/jinjava/objects/Namespace.java b/src/main/java/com/hubspot/jinjava/objects/Namespace.java index 3d3c5fe3a..67e8b8125 100644 --- a/src/main/java/com/hubspot/jinjava/objects/Namespace.java +++ b/src/main/java/com/hubspot/jinjava/objects/Namespace.java @@ -2,6 +2,7 @@ import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -20,7 +21,11 @@ public Namespace(Map map, int maxSize) { } @Override - public StringBuilder appendPyishString(StringBuilder sb) { - return PyishSerializable.super.appendPyishString(sb.append("namespace(")).append(')'); + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) PyishSerializable + .super.appendPyishString((T) appendable.append("namespace(")) + .append(')'); } } 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 2087dc37f..4822bcb7b 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java @@ -4,6 +4,7 @@ import com.hubspot.jinjava.objects.PyWrapper; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; import java.io.Serializable; import java.time.Instant; import java.time.ZoneOffset; @@ -158,8 +159,10 @@ public boolean equals(Object obj) { } @Override - public StringBuilder appendPyishString(StringBuilder sb) { - return sb + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable .append('\'') .append(strftime(FULL_DATE_FORMAT)) .append("'|strtotime(") diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingJsonProcessingException.java b/src/main/java/com/hubspot/jinjava/objects/serialization/LengthLimitingJsonProcessingException.java similarity index 75% rename from src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingJsonProcessingException.java rename to src/main/java/com/hubspot/jinjava/objects/serialization/LengthLimitingJsonProcessingException.java index 063737f57..06d6bb014 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingJsonProcessingException.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/LengthLimitingJsonProcessingException.java @@ -2,11 +2,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; -public class SizeLimitingJsonProcessingException extends JsonProcessingException { +public class LengthLimitingJsonProcessingException extends JsonProcessingException { private final int maxSize; private final int attemptedSize; - protected SizeLimitingJsonProcessingException(int maxSize, int attemptedSize) { + protected LengthLimitingJsonProcessingException(int maxSize, int attemptedSize) { super( String.format( "Max length of %d chars reached when serializing. %d chars attempted.", diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingWriter.java b/src/main/java/com/hubspot/jinjava/objects/serialization/LengthLimitingWriter.java similarity index 77% rename from src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingWriter.java rename to src/main/java/com/hubspot/jinjava/objects/serialization/LengthLimitingWriter.java index 68b0d3ef8..3d7b28352 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/SizeLimitingWriter.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/LengthLimitingWriter.java @@ -5,13 +5,13 @@ import java.io.Writer; import java.util.concurrent.atomic.AtomicInteger; -public class SizeLimitingWriter extends Writer { +public class LengthLimitingWriter extends Writer { public static final String REMAINING_LENGTH_ATTRIBUTE = "remainingLength"; private final CharArrayWriter charArrayWriter; private final AtomicInteger remainingLength; private final int startingLength; - public SizeLimitingWriter( + public LengthLimitingWriter( CharArrayWriter charArrayWriter, AtomicInteger remainingLength ) { @@ -21,28 +21,28 @@ public SizeLimitingWriter( } @Override - public void write(int c) throws SizeLimitingJsonProcessingException { + public void write(int c) throws LengthLimitingJsonProcessingException { checkMaxSize(1); charArrayWriter.write(c); } @Override public void write(char[] c, int off, int len) - throws SizeLimitingJsonProcessingException { + throws LengthLimitingJsonProcessingException { checkMaxSize(len); charArrayWriter.write(c, off, len); } @Override public void write(String str, int off, int len) - throws SizeLimitingJsonProcessingException { + throws LengthLimitingJsonProcessingException { checkMaxSize(len); charArrayWriter.write(str, off, len); } - private void checkMaxSize(int extra) throws SizeLimitingJsonProcessingException { + private void checkMaxSize(int extra) throws LengthLimitingJsonProcessingException { if (remainingLength.addAndGet(extra * -1) < 0) { - throw new SizeLimitingJsonProcessingException( + throw new LengthLimitingJsonProcessingException( startingLength, charArrayWriter.size() + extra ); diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java index c9baa8508..dd354bb46 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java @@ -22,22 +22,22 @@ public void serialize( ) throws IOException { AtomicInteger remainingLength = (AtomicInteger) serializerProvider.getAttribute( - SizeLimitingWriter.REMAINING_LENGTH_ATTRIBUTE + LengthLimitingWriter.REMAINING_LENGTH_ATTRIBUTE ); String key; String value; if (remainingLength != null) { ObjectWriter objectWriter = PyishObjectMapper.PYISH_OBJECT_WRITER.withAttribute( - SizeLimitingWriter.REMAINING_LENGTH_ATTRIBUTE, + LengthLimitingWriter.REMAINING_LENGTH_ATTRIBUTE, remainingLength ); key = objectWriter.writeValueAsString(entry.getKey()); - SizeLimitingWriter sizeLimitingWriter = new SizeLimitingWriter( + LengthLimitingWriter lengthLimitingWriter = new LengthLimitingWriter( new CharArrayWriter(), remainingLength ); - objectWriter.writeValue(sizeLimitingWriter, entry.getValue()); - value = sizeLimitingWriter.toString(); + objectWriter.writeValue(lengthLimitingWriter, entry.getValue()); + value = lengthLimitingWriter.toString(); } else { key = PyishObjectMapper.PYISH_OBJECT_WRITER.writeValueAsString(entry.getKey()); value = PyishObjectMapper.PYISH_OBJECT_WRITER.writeValueAsString(entry.getValue()); diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 451408214..6222f3f43 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -46,7 +46,7 @@ public static String getAsPyishString(Object val) { try { return getAsPyishStringOrThrow(val); } catch (IOException e) { - if (e instanceof SizeLimitingJsonProcessingException) { + if (e instanceof LengthLimitingJsonProcessingException) { if ( JinjavaInterpreter .getCurrentMaybe() @@ -58,8 +58,8 @@ public static String getAsPyishString(Object val) { throw new DeferredValueException(String.format("%s: %s", e.getMessage(), val)); } else { throw new OutputTooBigException( - ((SizeLimitingJsonProcessingException) e).getMaxSize(), - ((SizeLimitingJsonProcessingException) e).getAttemptedSize() + ((LengthLimitingJsonProcessingException) e).getMaxSize(), + ((LengthLimitingJsonProcessingException) e).getAttemptedSize() ); } } @@ -80,10 +80,10 @@ public static String getAsPyishStringOrThrow(Object val) throws IOException { ); objectWriter = objectWriter.withAttribute( - SizeLimitingWriter.REMAINING_LENGTH_ATTRIBUTE, + LengthLimitingWriter.REMAINING_LENGTH_ATTRIBUTE, remainingLength ); - writer = new SizeLimitingWriter(new CharArrayWriter(), remainingLength); + writer = new LengthLimitingWriter(new CharArrayWriter(), remainingLength); } else { writer = new CharArrayWriter(); } diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java index d72afffd7..b0a2150ab 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java @@ -7,8 +7,10 @@ import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializerProvider; import com.hubspot.jinjava.objects.PyWrapper; +import com.hubspot.jinjava.util.LengthLimitingStringBuilder; import java.io.IOException; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; public interface PyishSerializable extends PyWrapper { ObjectWriter SELF_WRITER = new ObjectMapper( @@ -23,7 +25,7 @@ public interface PyishSerializable extends PyWrapper { * but this method can be overridden to provide a custom representation. * This should no longer be called directly, * {@link #writeSelf(JsonGenerator, SerializerProvider)} or - * {@link #appendPyishString(StringBuilder)} should instead be used. + * {@link #appendPyishString(Appendable)} should instead be used. * @return A pyish/json CharSequence representation of the object */ @Deprecated @@ -38,11 +40,13 @@ default CharSequence toPyishString() { *

* If the pyish string representation of this object is composed of several strings, * it's recommended to override this method instead of {@link #toPyishString()} - * @param sb StringBuilder to append the pyish string representation to. - * @return The same StringBuilder sb with an appended result + * @param appendable Appendable to append the pyish string representation to. + * @return The same appendable with an appended result */ - default StringBuilder appendPyishString(StringBuilder sb) { - return sb.append(toPyishString()); + @SuppressWarnings("unchecked") + default T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(toPyishString()); } /** @@ -60,7 +64,17 @@ default void writeSelf( SerializerProvider serializerProvider ) throws IOException { - jsonGenerator.writeRawValue(appendPyishString(new StringBuilder()).toString()); + AtomicInteger remainingLength = (AtomicInteger) serializerProvider.getAttribute( + LengthLimitingWriter.REMAINING_LENGTH_ATTRIBUTE + ); + jsonGenerator.writeRawValue( + appendPyishString( + remainingLength != null + ? new LengthLimitingStringBuilder(remainingLength.get()) + : new StringBuilder() + ) + .toString() + ); } /** diff --git a/src/main/java/com/hubspot/jinjava/util/LengthLimitingStringBuilder.java b/src/main/java/com/hubspot/jinjava/util/LengthLimitingStringBuilder.java index 11b3cc3d0..c8fb929fb 100644 --- a/src/main/java/com/hubspot/jinjava/util/LengthLimitingStringBuilder.java +++ b/src/main/java/com/hubspot/jinjava/util/LengthLimitingStringBuilder.java @@ -4,7 +4,8 @@ import java.io.Serializable; import java.util.stream.IntStream; -public class LengthLimitingStringBuilder implements Serializable, CharSequence { +public class LengthLimitingStringBuilder + implements Serializable, CharSequence, Appendable { private static final long serialVersionUID = -1891922886257965755L; private final StringBuilder builder; @@ -50,14 +51,41 @@ public void append(Object obj) { append(String.valueOf(obj)); } - public void append(String str) { - if (str == null) { - return; + @Override + public LengthLimitingStringBuilder append(CharSequence csq) { + int csqLength = 4; // null + if (csq != null) { + csqLength = csq.length(); + } + length += csqLength; + checkLength(); + builder.append(csq); + return this; + } + + @Override + public Appendable append(CharSequence csq, int start, int end) { + int csqLength = 4; // null + if (csq != null) { + csqLength = end - start; } - length += str.length(); + length += csqLength; + checkLength(); + builder.append(csq, start, end); + return this; + } + + @Override + public Appendable append(char c) { + length++; + checkLength(); + builder.append(c); + return this; + } + + private void checkLength() { if (maxLength > 0 && length > maxLength) { throw new OutputTooBigException(maxLength, length); } - builder.append(str); } } diff --git a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java index dcb80204d..cadcb8be9 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -83,7 +83,7 @@ public void itLimitsDepth() { JinjavaInterpreter.pushCurrent(jinjava.newInterpreter()); assertThatThrownBy(() -> PyishObjectMapper.getAsPyishStringOrThrow(original)) .as("The string to be serialized is larger than the max output size") - .isInstanceOf(SizeLimitingJsonProcessingException.class); + .isInstanceOf(LengthLimitingJsonProcessingException.class); } finally { JinjavaInterpreter.popCurrent(); } diff --git a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java index aac2a08dd..58f876363 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java @@ -24,6 +24,7 @@ import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; +import java.io.IOException; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -303,7 +304,7 @@ public void itEvaluatesDict() { } @Test - public void itSerializesDateProperly() { + public void itSerializesDateProperly() throws IOException { PyishDate date = new PyishDate( ZonedDateTime.ofInstant(Instant.ofEpochMilli(1234567890L), ZoneId.systemDefault()) ); diff --git a/src/test/java/com/hubspot/jinjava/util/LengthLimitingStringBuilderTest.java b/src/test/java/com/hubspot/jinjava/util/LengthLimitingStringBuilderTest.java index f57a4f753..0a2bcf59c 100644 --- a/src/test/java/com/hubspot/jinjava/util/LengthLimitingStringBuilderTest.java +++ b/src/test/java/com/hubspot/jinjava/util/LengthLimitingStringBuilderTest.java @@ -25,6 +25,9 @@ public void itDoesNotLimitWithZeroLength() { public void itHandlesNullStrings() { LengthLimitingStringBuilder sb = new LengthLimitingStringBuilder(10); sb.append(null); - assertThat(sb.length()).isEqualTo(0); + assertThat(sb.toString()).isEqualTo("null"); + LengthLimitingStringBuilder sbLimited = new LengthLimitingStringBuilder(3); + assertThatThrownBy(() -> sbLimited.append(null)) + .isInstanceOf(OutputTooBigException.class); } } From fa21618e95021d8b904805051a5e6a399b5bd69d Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 25 Jan 2023 16:09:17 -0500 Subject: [PATCH 155/637] Change method name to writePyishSelf --- .../jinjava/objects/serialization/PyishSerializable.java | 6 +++--- .../jinjava/objects/serialization/PyishSerializer.java | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java index b0a2150ab..80f090687 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java @@ -24,7 +24,7 @@ public interface PyishSerializable extends PyWrapper { * By default, this will get a json representation of the object, * but this method can be overridden to provide a custom representation. * This should no longer be called directly, - * {@link #writeSelf(JsonGenerator, SerializerProvider)} or + * {@link #writePyishSelf(JsonGenerator, SerializerProvider)} or * {@link #appendPyishString(Appendable)} should instead be used. * @return A pyish/json CharSequence representation of the object */ @@ -35,7 +35,7 @@ default CharSequence toPyishString() { /** * Allows for a class to append the custom string representation in Jinjava. - * This method will be used by {@link #writeSelf(JsonGenerator, SerializerProvider)} + * This method will be used by {@link #writePyishSelf(JsonGenerator, SerializerProvider)} * to specify what will be written to the json generator. *

* If the pyish string representation of this object is composed of several strings, @@ -59,7 +59,7 @@ default T appendPyishString(T appendable) * @param jsonGenerator The JsonGenerator to write to. * @param serializerProvider Provides default value serialization and attributes stored on the ObjectWriter if needed. */ - default void writeSelf( + default void writePyishSelf( JsonGenerator jsonGenerator, SerializerProvider serializerProvider ) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializer.java index a6707fe26..094776282 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializer.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializer.java @@ -26,7 +26,10 @@ public void serialize( .map(interpreter -> interpreter.wrap(object)) .orElse(object); if (wrappedObject instanceof PyishSerializable) { - ((PyishSerializable) wrappedObject).writeSelf(jsonGenerator, serializerProvider); + ((PyishSerializable) wrappedObject).writePyishSelf( + jsonGenerator, + serializerProvider + ); } else if (wrappedObject instanceof Boolean) { jsonGenerator.writeBoolean((Boolean) wrappedObject); } else if (wrappedObject instanceof Number) { From d58e01dc4a1a9edc406a95857400fa6eb964d341 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 25 Jan 2023 16:41:02 -0500 Subject: [PATCH 156/637] Remove toPyishString() method entirely --- .../jinjava/interpret/JinjavaInterpreter.java | 6 +++-- .../jinjava/interpret/LazyReference.java | 7 ++++-- .../serialization/PyishSerializable.java | 24 ++++--------------- .../lib/filter/RejectAttrFilterTest.java | 7 ++++-- .../lib/filter/SelectAttrFilterTest.java | 13 ++++++---- .../jinjava/lib/filter/SortFilterTest.java | 13 ++++++---- .../jinjava/lib/filter/UniqueFilterTest.java | 7 ++++-- .../util/EagerExpressionResolverTest.java | 4 +++- 8 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index a3a392019..553f4eaf7 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -882,7 +882,9 @@ private String getWrappedErrorMessage( } @Override - public String toPyishString() { - return ExtendedParser.INTERPRETER; + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(ExtendedParser.INTERPRETER); } } diff --git a/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java b/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java index 383171264..18cbecb16 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java +++ b/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; public class LazyReference extends LazyExpression implements PyishSerializable { private String referenceKey; @@ -24,7 +25,9 @@ public void setReferenceKey(String referenceKey) { } @Override - public CharSequence toPyishString() { - return getReferenceKey(); + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(getReferenceKey()); } } diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java index 80f090687..b53efcbd3 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java @@ -19,43 +19,27 @@ public interface PyishSerializable extends PyWrapper { .writer(PyishPrettyPrinter.INSTANCE) .with(PyishCharacterEscapes.INSTANCE); - /** - * Allows for a class to specify a custom string representation in Jinjava. - * By default, this will get a json representation of the object, - * but this method can be overridden to provide a custom representation. - * This should no longer be called directly, - * {@link #writePyishSelf(JsonGenerator, SerializerProvider)} or - * {@link #appendPyishString(Appendable)} should instead be used. - * @return A pyish/json CharSequence representation of the object - */ - @Deprecated - default CharSequence toPyishString() { - return writeValueAsString(this); - } - /** * Allows for a class to append the custom string representation in Jinjava. * This method will be used by {@link #writePyishSelf(JsonGenerator, SerializerProvider)} * to specify what will be written to the json generator. *

- * If the pyish string representation of this object is composed of several strings, - * it's recommended to override this method instead of {@link #toPyishString()} * @param appendable Appendable to append the pyish string representation to. * @return The same appendable with an appended result */ @SuppressWarnings("unchecked") default T appendPyishString(T appendable) throws IOException { - return (T) appendable.append(toPyishString()); + return (T) appendable.append(writeValueAsString(this)); } /** * Allows for a class to specify how its pyish string representation will * be written to the json generator. *

- * If the pyish string representation of this object can be very large, it's recommended - * to override this method instead of {@link #toPyishString()} so that jsonGenerator - * can be written to multiple times, allowing multiple limit checks to occur. + * If the object's serialization can be broken up into multiple jsonGenerator writes, + * then this method can be overridden to do so instead of a single call to + * {@link JsonGenerator#writeRawValue(String)}. * @param jsonGenerator The JsonGenerator to write to. * @param serializerProvider Provides default value serialization and attributes stored on the ObjectWriter if needed. */ diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/RejectAttrFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/RejectAttrFilterTest.java index 9ab84b89a..cd4366ab5 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/RejectAttrFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/RejectAttrFilterTest.java @@ -5,6 +5,7 @@ import com.google.common.collect.Lists; import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; import java.util.HashMap; import org.junit.Before; import org.junit.Test; @@ -112,8 +113,10 @@ public String toString() { } @Override - public CharSequence toPyishString() { - return toString(); + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(toString()); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/SelectAttrFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/SelectAttrFilterTest.java index 8b59c671a..1ca5ddca4 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/SelectAttrFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/SelectAttrFilterTest.java @@ -6,6 +6,7 @@ import com.google.common.collect.Lists; import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; import java.util.HashMap; import org.junit.Before; import org.junit.Test; @@ -135,8 +136,10 @@ public String toString() { } @Override - public String toPyishString() { - return toString(); + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(toString()); } } @@ -164,8 +167,10 @@ public String toString() { } @Override - public String toPyishString() { - return toString(); + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(toString()); } } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/SortFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/SortFilterTest.java index c44f276f1..180ec8b75 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/SortFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/SortFilterTest.java @@ -6,6 +6,7 @@ import com.hubspot.jinjava.interpret.RenderResult; import com.hubspot.jinjava.interpret.TemplateError.ErrorType; import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -153,8 +154,10 @@ public String toString() { } @Override - public String toPyishString() { - return toString(); + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(toString()); } } @@ -175,8 +178,10 @@ public String toString() { } @Override - public String toPyishString() { - return toString(); + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(toString()); } } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/UniqueFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/UniqueFilterTest.java index 0a9e6966c..0aaf16bbd 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/UniqueFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/UniqueFilterTest.java @@ -4,6 +4,7 @@ import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.apache.commons.lang3.StringUtils; @@ -71,8 +72,10 @@ public String toString() { } @Override - public String toPyishString() { - return toString(); + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(toString()); } } } diff --git a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java index 58f876363..0d308f5c9 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java @@ -908,7 +908,9 @@ public String getName() { } @Override - public String toPyishString() { + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { throw new DeferredValueException("Can't serialize"); } } From 16a76893752dceda52f6dfefe9acee1add37c33a Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 26 Jan 2023 13:47:25 -0500 Subject: [PATCH 157/637] Just wrap LengthLimitingJsonProcessingException as OutputTooBigException --- .../jinjava/objects/date/PyishDate.java | 2 +- .../serialization/PyishObjectMapper.java | 20 ++++--------------- .../lib/tag/eager/EagerTagDecoratorTest.java | 2 +- 3 files changed, 6 insertions(+), 18 deletions(-) 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 4822bcb7b..42f8fa082 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java @@ -166,7 +166,7 @@ public T appendPyishString(T appendable) .append('\'') .append(strftime(FULL_DATE_FORMAT)) .append("'|strtotime(") - .append(PyishObjectMapper.getAsPyishString(FULL_DATE_FORMAT)) + .append(PyishObjectMapper.getAsPyishStringOrThrow(FULL_DATE_FORMAT)) .append(')'); } } diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 6222f3f43..14da54606 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; -import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.util.WhitespaceUtils; @@ -47,21 +46,10 @@ public static String getAsPyishString(Object val) { return getAsPyishStringOrThrow(val); } catch (IOException e) { if (e instanceof LengthLimitingJsonProcessingException) { - if ( - JinjavaInterpreter - .getCurrentMaybe() - .map( - interpreter -> interpreter.getConfig().getExecutionMode().useEagerParser() - ) - .orElse(false) - ) { - throw new DeferredValueException(String.format("%s: %s", e.getMessage(), val)); - } else { - throw new OutputTooBigException( - ((LengthLimitingJsonProcessingException) e).getMaxSize(), - ((LengthLimitingJsonProcessingException) e).getAttemptedSize() - ); - } + throw new OutputTooBigException( + ((LengthLimitingJsonProcessingException) e).getMaxSize(), + ((LengthLimitingJsonProcessingException) e).getAttemptedSize() + ); } return Objects.toString(val, ""); } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecoratorTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecoratorTest.java index 1ace159a3..7825fdeaf 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecoratorTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagDecoratorTest.java @@ -125,7 +125,7 @@ public void itLimitsTagLength() { () -> eagerTagDecorator.getEagerTagImage((TagToken) tagNode.getMaster(), interpreter) ) - .isInstanceOf(DeferredValueException.class); + .isInstanceOf(OutputTooBigException.class); } @Test From 15f49a4edca3e1702f35b292eb85457519c96f66 Mon Sep 17 00:00:00 2001 From: Julia Uy Date: Fri, 27 Jan 2023 12:02:51 -0600 Subject: [PATCH 158/637] Create format_number filter --- .../jinjava/lib/filter/NumberFilter.java | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/main/java/com/hubspot/jinjava/lib/filter/NumberFilter.java diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/NumberFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/NumberFilter.java new file mode 100644 index 000000000..051d45e04 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/NumberFilter.java @@ -0,0 +1,110 @@ +package com.hubspot.jinjava.lib.filter; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; +import com.hubspot.jinjava.doc.annotations.JinjavaParam; +import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; +import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; +import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; +import com.hubspot.jinjava.interpret.TemplateError.ErrorType; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; +import java.util.Objects; + +@JinjavaDoc( + value = "Formats a given number based on the locale passed in as a parameter.", + input = @JinjavaParam( + value = "value", + desc = "The number to be formatted based on locale", + required = true + ), + params = { + @JinjavaParam( + value = "locale", + desc = "Locale in which to format the number. The default is the page's locale." + ), + @JinjavaParam( + value = "decimal precision number", + type = "number", + desc = "A number input that determines the decimal precision of the formatted value. If the number of decimal digits from the input value is less than the decimal precision number, use the number of decimal digits from the input value. Otherwise, use the decimal precision number. The default is the number of decimal digits from the input value." + ) + }, + snippets = { + @JinjavaSnippet(code = "{{ number|format_number }}"), + @JinjavaSnippet(code = "{{ number|format_number(\"en-US\") }}"), + @JinjavaSnippet(code = "{{ number|format_number(\"en-US\", 3) }}") + } +) +public class NumberFilter implements Filter { + private static final String FORMAT_NUMBER_FILTER_NAME = "format_number"; + + @Override + public String getName() { + return FORMAT_NUMBER_FILTER_NAME; + } + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + Locale locale = args.length > 0 && !Strings.isNullOrEmpty(args[0]) + ? Locale.forLanguageTag(args[0]) + : interpreter.getConfig().getLocale(); + + BigDecimal number; + try { + number = parseInput(var); + } catch (Exception e) { + if (interpreter.getContext().isValidationMode()) { + return ""; + } + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.INVALID_INPUT, + ErrorItem.FILTER, + "Input value '" + var + "' could not be parsed.", + null, + interpreter.getLineNumber(), + e, + null, + ImmutableMap.of("value", Objects.toString(var)) + ) + ); + return var; + } + + int noOfDecimalPlacesInInput = Math.max(0, number.scale()); + int decimalPrecisionNumber = args.length > 1 + ? Integer.parseInt(args[1]) + : noOfDecimalPlacesInInput; + + return formatNumber(locale, number, noOfDecimalPlacesInInput, decimalPrecisionNumber); + } + + private BigDecimal parseInput(Object input) throws Exception { + DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(); + df.setParseBigDecimal(true); + + return (BigDecimal) df.parseObject(Objects.toString(input)); + } + + private String formatNumber( + Locale locale, + BigDecimal number, + int noOfDecimalPlacesInInput, + int decimalPrecisionNumber + ) { + NumberFormat numberFormat = NumberFormat.getNumberInstance(locale); + + numberFormat.setMinimumFractionDigits(noOfDecimalPlacesInInput); + numberFormat.setMaximumFractionDigits( + Math.min(noOfDecimalPlacesInInput, decimalPrecisionNumber) + ); + + return numberFormat.format(number); + } +} From f3ef0f1ec68ddccc564648249c733d3e7b8cd86c Mon Sep 17 00:00:00 2001 From: Julia Uy Date: Fri, 27 Jan 2023 13:43:37 -0600 Subject: [PATCH 159/637] Rename to NumberFormatFilter --- .../lib/filter/{NumberFilter.java => NumberFormatFilter.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/com/hubspot/jinjava/lib/filter/{NumberFilter.java => NumberFormatFilter.java} (98%) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/NumberFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/NumberFormatFilter.java similarity index 98% rename from src/main/java/com/hubspot/jinjava/lib/filter/NumberFilter.java rename to src/main/java/com/hubspot/jinjava/lib/filter/NumberFormatFilter.java index 051d45e04..0b50f4593 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/NumberFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/NumberFormatFilter.java @@ -40,7 +40,7 @@ @JinjavaSnippet(code = "{{ number|format_number(\"en-US\", 3) }}") } ) -public class NumberFilter implements Filter { +public class NumberFormatFilter implements Filter { private static final String FORMAT_NUMBER_FILTER_NAME = "format_number"; @Override From 834fcd4a0a8af03fb5150ff9303938188c31124e Mon Sep 17 00:00:00 2001 From: Julia Uy Date: Fri, 27 Jan 2023 13:54:19 -0600 Subject: [PATCH 160/637] Add filter to FilterLibrary --- src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java index 544230d9c..2bcb27080 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java @@ -82,6 +82,7 @@ protected void registerDefaults() { Md5Filter.class, MinusTimeFilter.class, MultiplyFilter.class, + NumberFormatFilter.class, PlusTimeFilter.class, PrettyPrintFilter.class, RandomFilter.class, From 40d44c34cf153289cad80a127496b5b0728744d8 Mon Sep 17 00:00:00 2001 From: Julia Uy Date: Fri, 27 Jan 2023 13:55:01 -0600 Subject: [PATCH 161/637] Add tests for filter using different locales --- .../lib/filter/NumberFormatFilterTest.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/test/java/com/hubspot/jinjava/lib/filter/NumberFormatFilterTest.java diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/NumberFormatFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/NumberFormatFilterTest.java new file mode 100644 index 000000000..7b1880b39 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/NumberFormatFilterTest.java @@ -0,0 +1,74 @@ +package com.hubspot.jinjava.lib.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseJinjavaTest; +import java.util.HashMap; +import org.junit.Before; +import org.junit.Test; + +public class NumberFormatFilterTest extends BaseJinjavaTest { + + @Before + public void setup() {} + + @Test + public void testNumberFormatFilter() { + assertThat( + jinjava.render("{{1000|format_number('en-US')}}", new HashMap()) + ) + .isEqualTo("1,000"); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('en-US') }}", + new HashMap() + ) + ) + .isEqualTo("1,000.333"); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('en-US', 2) }}", + new HashMap() + ) + ) + .isEqualTo("1,000.33"); + + assertThat( + jinjava.render("{{ 1000|format_number('fr') }}", new HashMap()) + ) + .isEqualTo("1\u00a0000"); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('fr') }}", + new HashMap() + ) + ) + .isEqualTo("1\u00a0000,333"); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('fr', 2) }}", + new HashMap() + ) + ) + .isEqualTo("1\u00a0000,33"); + + assertThat( + jinjava.render("{{ 1000|format_number('es') }}", new HashMap()) + ) + .isEqualTo("1.000"); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('es') }}", + new HashMap() + ) + ) + .isEqualTo("1.000,333"); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('es', 2) }}", + new HashMap() + ) + ) + .isEqualTo("1.000,33"); + } +} From 75fa23313d5506d8c2e686fe16d18fcc936ca61d Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 1 Feb 2023 09:58:39 -0500 Subject: [PATCH 162/637] Flip ternary expression --- .../jinjava/objects/serialization/PyishSerializable.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java index b53efcbd3..54c2e7df4 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java @@ -53,9 +53,9 @@ default void writePyishSelf( ); jsonGenerator.writeRawValue( appendPyishString( - remainingLength != null - ? new LengthLimitingStringBuilder(remainingLength.get()) - : new StringBuilder() + remainingLength == null + ? new StringBuilder() + : new LengthLimitingStringBuilder(remainingLength.get()) ) .toString() ); From faa31612fe456412597a82fad6c310a2cf102fcc Mon Sep 17 00:00:00 2001 From: Julia Uy Date: Mon, 6 Feb 2023 14:23:33 -0600 Subject: [PATCH 163/637] Update variable names, class name and method parameter --- .../jinjava/lib/filter/FilterLibrary.java | 2 +- ...matFilter.java => FormatNumberFilter.java} | 26 +++++++++++-------- ...rTest.java => FormatNumberFilterTest.java} | 4 +-- 3 files changed, 18 insertions(+), 14 deletions(-) rename src/main/java/com/hubspot/jinjava/lib/filter/{NumberFormatFilter.java => FormatNumberFilter.java} (84%) rename src/test/java/com/hubspot/jinjava/lib/filter/{NumberFormatFilterTest.java => FormatNumberFilterTest.java} (94%) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java index 2bcb27080..57990d84a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java @@ -82,7 +82,7 @@ protected void registerDefaults() { Md5Filter.class, MinusTimeFilter.class, MultiplyFilter.class, - NumberFormatFilter.class, + FormatNumberFilter.class, PlusTimeFilter.class, PrettyPrintFilter.class, RandomFilter.class, diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/NumberFormatFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/FormatNumberFilter.java similarity index 84% rename from src/main/java/com/hubspot/jinjava/lib/filter/NumberFormatFilter.java rename to src/main/java/com/hubspot/jinjava/lib/filter/FormatNumberFilter.java index 0b50f4593..924e8e325 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/NumberFormatFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FormatNumberFilter.java @@ -15,6 +15,7 @@ import java.text.NumberFormat; import java.util.Locale; import java.util.Objects; +import java.util.Optional; @JinjavaDoc( value = "Formats a given number based on the locale passed in as a parameter.", @@ -29,7 +30,7 @@ desc = "Locale in which to format the number. The default is the page's locale." ), @JinjavaParam( - value = "decimal precision number", + value = "max decimal precision", type = "number", desc = "A number input that determines the decimal precision of the formatted value. If the number of decimal digits from the input value is less than the decimal precision number, use the number of decimal digits from the input value. Otherwise, use the decimal precision number. The default is the number of decimal digits from the input value." ) @@ -40,7 +41,7 @@ @JinjavaSnippet(code = "{{ number|format_number(\"en-US\", 3) }}") } ) -public class NumberFormatFilter implements Filter { +public class FormatNumberFilter implements Filter { private static final String FORMAT_NUMBER_FILTER_NAME = "format_number"; @Override @@ -77,12 +78,11 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) return var; } - int noOfDecimalPlacesInInput = Math.max(0, number.scale()); - int decimalPrecisionNumber = args.length > 1 - ? Integer.parseInt(args[1]) - : noOfDecimalPlacesInInput; + Optional maxDecimalPrecision = args.length > 1 + ? Optional.of(Integer.parseInt(args[1])) + : Optional.empty(); - return formatNumber(locale, number, noOfDecimalPlacesInInput, decimalPrecisionNumber); + return formatNumber(locale, number, maxDecimalPrecision); } private BigDecimal parseInput(Object input) throws Exception { @@ -95,14 +95,18 @@ private BigDecimal parseInput(Object input) throws Exception { private String formatNumber( Locale locale, BigDecimal number, - int noOfDecimalPlacesInInput, - int decimalPrecisionNumber + Optional maxDecimalPrecision ) { NumberFormat numberFormat = NumberFormat.getNumberInstance(locale); + int numDecimalPlacesInInput = Math.max(0, number.scale()); - numberFormat.setMinimumFractionDigits(noOfDecimalPlacesInInput); numberFormat.setMaximumFractionDigits( - Math.min(noOfDecimalPlacesInInput, decimalPrecisionNumber) + Math.min( + numDecimalPlacesInInput, + maxDecimalPrecision.isPresent() + ? maxDecimalPrecision.get() + : numDecimalPlacesInInput + ) ); return numberFormat.format(number); diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/NumberFormatFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java similarity index 94% rename from src/test/java/com/hubspot/jinjava/lib/filter/NumberFormatFilterTest.java rename to src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java index 7b1880b39..25b715a0a 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/NumberFormatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java @@ -7,13 +7,13 @@ import org.junit.Before; import org.junit.Test; -public class NumberFormatFilterTest extends BaseJinjavaTest { +public class FormatNumberFilterTest extends BaseJinjavaTest { @Before public void setup() {} @Test - public void testNumberFormatFilter() { + public void testFormatNumberFilter() { assertThat( jinjava.render("{{1000|format_number('en-US')}}", new HashMap()) ) From a80a4ed011d28bfff3fa5f593afd6d643290a1d9 Mon Sep 17 00:00:00 2001 From: Julia Uy Date: Mon, 6 Feb 2023 14:55:58 -0600 Subject: [PATCH 164/637] Move class in library for alphabetical order --- src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java index 57990d84a..216a1d2bd 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java @@ -62,6 +62,7 @@ protected void registerDefaults() { FormatFilter.class, FormatDateFilter.class, FormatDatetimeFilter.class, + FormatNumberFilter.class, FormatTimeFilter.class, FromJsonFilter.class, FromYamlFilter.class, @@ -82,7 +83,6 @@ protected void registerDefaults() { Md5Filter.class, MinusTimeFilter.class, MultiplyFilter.class, - FormatNumberFilter.class, PlusTimeFilter.class, PrettyPrintFilter.class, RandomFilter.class, From 72bd6f96d2bcf11f2b214de447177827cd5d0a43 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 9 Feb 2023 11:11:34 -0500 Subject: [PATCH 165/637] We should be setting the dateFormat to what it originally is in PyishDate. This PR adds a new method which we can use when reconstructing to put the date format back to what it originally was --- .../jinjava/objects/date/PyishDate.java | 9 ++++++++- .../jinjava/objects/date/PyishDateTest.java | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) 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 42f8fa082..55969b006 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java @@ -112,6 +112,11 @@ public void setDateFormat(String dateFormat) { this.dateFormat = dateFormat; } + public PyishDate withDateFormat(String dateFormat) { + setDateFormat(dateFormat); + return this; + } + public Date toDate() { return Date.from(date.toInstant()); } @@ -163,10 +168,12 @@ public boolean equals(Object obj) { public T appendPyishString(T appendable) throws IOException { return (T) appendable - .append('\'') + .append("('") .append(strftime(FULL_DATE_FORMAT)) .append("'|strtotime(") .append(PyishObjectMapper.getAsPyishStringOrThrow(FULL_DATE_FORMAT)) + .append(")).withDateFormat(") + .append(PyishObjectMapper.getAsPyishStringOrThrow(dateFormat)) .append(')'); } } diff --git a/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java b/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java index 0c25fc12f..7adda3452 100644 --- a/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java @@ -67,6 +67,26 @@ public void itPyishSerializes() { assertThat(d1).isEqualTo(interpreter.getContext().get("foo")); } + @Test + public void itPyishSerializesWithCustomDateFormat() { + PyishDate d1 = new PyishDate(ZonedDateTime.parse("2013-11-12T14:15:16.170+02:00")); + d1.setDateFormat("yyyy-MM-dd"); + JinjavaInterpreter interpreter = new Jinjava().newInterpreter(); + interpreter.render("{% set foo = " + PyishObjectMapper.getAsPyishString(d1) + "%}"); + PyishDate reconstructed = (PyishDate) interpreter.getContext().get("foo"); + assertThat(reconstructed.toString()).isEqualTo("2013-11-12"); + } + + @Test + public void itDoesntLoseSecondsOnReconstruction() { + PyishDate d1 = new PyishDate(ZonedDateTime.parse("2013-11-12T14:15:16.170+02:00")); + d1.setDateFormat("yyyy-MM-dd"); + JinjavaInterpreter interpreter = new Jinjava().newInterpreter(); + interpreter.render("{% set foo = " + PyishObjectMapper.getAsPyishString(d1) + "%}"); + PyishDate reconstructed = (PyishDate) interpreter.getContext().get("foo"); + assertThat(reconstructed.getSecond()).isEqualTo(16); + } + @Test public void testPyishDateToStringWithCustomDateFormatter() { PyishDate d1 = new PyishDate(ZonedDateTime.parse("2013-11-12T14:15:16.170+02:00")); From 73970396c181d2aa97c7aa5bf1ef64e37f237ddf Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 9 Feb 2023 16:04:20 -0500 Subject: [PATCH 166/637] Add option to EscapeJinjavaFilter to only escape curly braces when part of a token --- .../lib/filter/EscapeJinjavaFilter.java | 18 ++++++++++++++++++ .../lib/filter/EscapeJinjavaFilterTest.java | 10 ++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java index d94c1e804..21d2f0498 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java @@ -27,6 +27,14 @@ "Use this filter if you need to display text that might contain such characters in Jinjava. " + "Marks return value as markup string.", input = @JinjavaParam(value = "s", desc = "String to escape", required = true), + params = { + @JinjavaParam( + value = "all_braces", + type = "boolean", + desc = "Whether to only escape all curly braces or just when there are default expression, tag, or comment marks", + defaultValue = "true" + ) + }, snippets = { @JinjavaSnippet( code = "{% set escape_string = \"{{This markup is printed as text}}\" %}\n" + @@ -47,8 +55,18 @@ public static String escapeJinjavaEntities(String input) { return StringUtils.replaceEach(input, TO_REPLACE, REPLACE_WITH); } + public static String escapeFullJinjavaEntities(String input) { + return input + .replace("{{", BLBRACE + BLBRACE) + .replaceAll("\\{([{%#])", BLBRACE + "$1") + .replaceAll("([}%#])}", "$1" + BRBRACE); + } + @Override public Object filter(Object object, JinjavaInterpreter interpreter, String... arg) { + if (arg.length > 0 && "false".equals(arg[0])) { + return escapeFullJinjavaEntities(Objects.toString(object, "")); + } return escapeJinjavaEntities(Objects.toString(object, "")); } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilterTest.java index b5b8b1118..8f01bf2ae 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilterTest.java @@ -30,4 +30,14 @@ public void testSafeStringCanBeEscaped() { assertThat(f.filter(new SafeString("{{ me & you }}"), interpreter)) .isInstanceOf(SafeString.class); } + + @Test + public void testDoesNotEscapeJson() { + assertThat( + f.filter("{'foo': 'bar', '{{{ foo }}}': '{% bar %}'}", interpreter, "false") + ) + .isEqualTo( + "{'foo': 'bar', '{{{ foo }}}': '{% bar %}'}" + ); + } } From 9c3762761942bf305a297b115f55c81d595cda09 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 10 Feb 2023 09:04:13 -0500 Subject: [PATCH 167/637] Add interface to define pre-process actions for a node --- .../com/hubspot/jinjava/JinjavaConfig.java | 14 +++++++++++ .../jinjava/el/JinjavaNodePreProcessor.java | 24 +++++++++++++++++++ .../hubspot/jinjava/el/NodePreProcessor.java | 8 +++++++ .../hubspot/jinjava/tree/ExpressionNode.java | 1 - .../java/com/hubspot/jinjava/tree/Node.java | 14 +---------- .../com/hubspot/jinjava/tree/TagNode.java | 1 - 6 files changed, 47 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/hubspot/jinjava/el/JinjavaNodePreProcessor.java create mode 100644 src/main/java/com/hubspot/jinjava/el/NodePreProcessor.java diff --git a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java index ea0abea84..8172557d7 100644 --- a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java +++ b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java @@ -19,7 +19,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.hubspot.jinjava.el.JinjavaInterpreterResolver; +import com.hubspot.jinjava.el.JinjavaNodePreProcessor; import com.hubspot.jinjava.el.JinjavaObjectUnwrapper; +import com.hubspot.jinjava.el.NodePreProcessor; import com.hubspot.jinjava.el.ObjectUnwrapper; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.Context.Library; @@ -72,6 +74,7 @@ public class JinjavaConfig { private final ObjectMapper objectMapper; private final ObjectUnwrapper objectUnwrapper; + private final NodePreProcessor nodePreProcessor; public static Builder newBuilder() { return new Builder(); @@ -128,6 +131,7 @@ private JinjavaConfig(Builder builder) { enablePreciseDivideFilter = builder.enablePreciseDivideFilter; objectMapper = builder.objectMapper; objectUnwrapper = builder.objectUnwrapper; + nodePreProcessor = builder.nodePreProcessor; } public Charset getCharset() { @@ -230,6 +234,10 @@ public ObjectUnwrapper getObjectUnwrapper() { return objectUnwrapper; } + public NodePreProcessor getNodePreProcessor() { + return nodePreProcessor; + } + /** * @deprecated Replaced by {@link LegacyOverrides#isIterateOverMapKeys()} */ @@ -282,6 +290,7 @@ public static class Builder { private ObjectMapper objectMapper = new ObjectMapper(); private ObjectUnwrapper objectUnwrapper = new JinjavaObjectUnwrapper(); + private NodePreProcessor nodePreProcessor = new JinjavaNodePreProcessor(); private Builder() {} @@ -443,6 +452,11 @@ public Builder withObjectUnwrapper(ObjectUnwrapper objectUnwrapper) { return this; } + public Builder withNodePreProcessor(NodePreProcessor nodePreProcessor) { + this.nodePreProcessor = nodePreProcessor; + return this; + } + public JinjavaConfig build() { return new JinjavaConfig(this); } diff --git a/src/main/java/com/hubspot/jinjava/el/JinjavaNodePreProcessor.java b/src/main/java/com/hubspot/jinjava/el/JinjavaNodePreProcessor.java new file mode 100644 index 000000000..401e413e3 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/JinjavaNodePreProcessor.java @@ -0,0 +1,24 @@ +package com.hubspot.jinjava.el; + +import com.hubspot.jinjava.interpret.InterpretException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.tree.Node; + +public class JinjavaNodePreProcessor implements NodePreProcessor { + + @Override + public void preProcess(Node node, JinjavaInterpreter interpreter) { + interpreter.getContext().setCurrentNode(node); + checkForInterrupt(node); + } + + protected void checkForInterrupt(Node node) { + if (Thread.currentThread().isInterrupted()) { + throw new InterpretException( + "Interrupt rendering " + getClass(), + node.getMaster().getLineNumber(), + node.getMaster().getStartPosition() + ); + } + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/NodePreProcessor.java b/src/main/java/com/hubspot/jinjava/el/NodePreProcessor.java new file mode 100644 index 000000000..85ef0c8f8 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/NodePreProcessor.java @@ -0,0 +1,8 @@ +package com.hubspot.jinjava.el; + +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.tree.Node; + +public interface NodePreProcessor { + void preProcess(Node node, JinjavaInterpreter interpreter); +} diff --git a/src/main/java/com/hubspot/jinjava/tree/ExpressionNode.java b/src/main/java/com/hubspot/jinjava/tree/ExpressionNode.java index aac897c4c..5693a2baf 100644 --- a/src/main/java/com/hubspot/jinjava/tree/ExpressionNode.java +++ b/src/main/java/com/hubspot/jinjava/tree/ExpressionNode.java @@ -47,7 +47,6 @@ public OutputNode render(JinjavaInterpreter interpreter) { try { return expressionStrategy.interpretOutput(master, interpreter); } catch (DeferredValueException e) { - checkForInterrupt(); interpreter.getContext().handleDeferredNode(this); return new RenderedOutputNode(master.getImage()); } diff --git a/src/main/java/com/hubspot/jinjava/tree/Node.java b/src/main/java/com/hubspot/jinjava/tree/Node.java index da39f4852..836d0ea31 100644 --- a/src/main/java/com/hubspot/jinjava/tree/Node.java +++ b/src/main/java/com/hubspot/jinjava/tree/Node.java @@ -15,7 +15,6 @@ **********************************************************************/ package com.hubspot.jinjava.tree; -import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.tree.output.OutputNode; import com.hubspot.jinjava.tree.parse.Token; @@ -100,17 +99,6 @@ public String toTreeString(int level) { } public void preProcess(JinjavaInterpreter interpreter) { - interpreter.getContext().setCurrentNode(this); - checkForInterrupt(); - } - - public final void checkForInterrupt() { - if (Thread.currentThread().isInterrupted()) { - throw new InterpretException( - "Interrupt rendering " + getClass(), - master.getLineNumber(), - master.getStartPosition() - ); - } + interpreter.getConfig().getNodePreProcessor().preProcess(this, interpreter); } } diff --git a/src/main/java/com/hubspot/jinjava/tree/TagNode.java b/src/main/java/com/hubspot/jinjava/tree/TagNode.java index 4402bfe38..1218631d2 100644 --- a/src/main/java/com/hubspot/jinjava/tree/TagNode.java +++ b/src/main/java/com/hubspot/jinjava/tree/TagNode.java @@ -56,7 +56,6 @@ public OutputNode render(JinjavaInterpreter interpreter) { } return tag.interpretOutput(this, interpreter); } catch (DeferredValueException e) { - checkForInterrupt(); interpreter.getContext().handleDeferredNode(this); return new RenderedOutputNode(reconstructImage()); } catch ( From 7fb1dc7a172640ee200862d13ed4df06042e55ea Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 10 Feb 2023 13:36:21 -0500 Subject: [PATCH 168/637] Implement BiConsumer instead --- .../java/com/hubspot/jinjava/JinjavaConfig.java | 14 +++++++++----- .../jinjava/el/JinjavaNodePreProcessor.java | 5 +++-- .../com/hubspot/jinjava/el/NodePreProcessor.java | 8 -------- src/main/java/com/hubspot/jinjava/tree/Node.java | 2 +- 4 files changed, 13 insertions(+), 16 deletions(-) delete mode 100644 src/main/java/com/hubspot/jinjava/el/NodePreProcessor.java diff --git a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java index 8172557d7..0d9f27451 100644 --- a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java +++ b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java @@ -21,15 +21,16 @@ import com.hubspot.jinjava.el.JinjavaInterpreterResolver; import com.hubspot.jinjava.el.JinjavaNodePreProcessor; import com.hubspot.jinjava.el.JinjavaObjectUnwrapper; -import com.hubspot.jinjava.el.NodePreProcessor; import com.hubspot.jinjava.el.ObjectUnwrapper; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.Context.Library; import com.hubspot.jinjava.interpret.InterpreterFactory; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.JinjavaInterpreterFactory; import com.hubspot.jinjava.mode.DefaultExecutionMode; import com.hubspot.jinjava.mode.ExecutionMode; import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy; +import com.hubspot.jinjava.tree.Node; import com.hubspot.jinjava.tree.parse.DefaultTokenScannerSymbols; import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; import java.nio.charset.Charset; @@ -40,6 +41,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; import javax.el.ELResolver; public class JinjavaConfig { @@ -74,7 +76,7 @@ public class JinjavaConfig { private final ObjectMapper objectMapper; private final ObjectUnwrapper objectUnwrapper; - private final NodePreProcessor nodePreProcessor; + private final BiConsumer nodePreProcessor; public static Builder newBuilder() { return new Builder(); @@ -234,7 +236,7 @@ public ObjectUnwrapper getObjectUnwrapper() { return objectUnwrapper; } - public NodePreProcessor getNodePreProcessor() { + public BiConsumer getNodePreProcessor() { return nodePreProcessor; } @@ -290,7 +292,7 @@ public static class Builder { private ObjectMapper objectMapper = new ObjectMapper(); private ObjectUnwrapper objectUnwrapper = new JinjavaObjectUnwrapper(); - private NodePreProcessor nodePreProcessor = new JinjavaNodePreProcessor(); + private BiConsumer nodePreProcessor = new JinjavaNodePreProcessor(); private Builder() {} @@ -452,7 +454,9 @@ public Builder withObjectUnwrapper(ObjectUnwrapper objectUnwrapper) { return this; } - public Builder withNodePreProcessor(NodePreProcessor nodePreProcessor) { + public Builder withNodePreProcessor( + BiConsumer nodePreProcessor + ) { this.nodePreProcessor = nodePreProcessor; return this; } diff --git a/src/main/java/com/hubspot/jinjava/el/JinjavaNodePreProcessor.java b/src/main/java/com/hubspot/jinjava/el/JinjavaNodePreProcessor.java index 401e413e3..881594909 100644 --- a/src/main/java/com/hubspot/jinjava/el/JinjavaNodePreProcessor.java +++ b/src/main/java/com/hubspot/jinjava/el/JinjavaNodePreProcessor.java @@ -3,11 +3,12 @@ import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.tree.Node; +import java.util.function.BiConsumer; -public class JinjavaNodePreProcessor implements NodePreProcessor { +public class JinjavaNodePreProcessor implements BiConsumer { @Override - public void preProcess(Node node, JinjavaInterpreter interpreter) { + public void accept(Node node, JinjavaInterpreter interpreter) { interpreter.getContext().setCurrentNode(node); checkForInterrupt(node); } diff --git a/src/main/java/com/hubspot/jinjava/el/NodePreProcessor.java b/src/main/java/com/hubspot/jinjava/el/NodePreProcessor.java deleted file mode 100644 index 85ef0c8f8..000000000 --- a/src/main/java/com/hubspot/jinjava/el/NodePreProcessor.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.hubspot.jinjava.el; - -import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.tree.Node; - -public interface NodePreProcessor { - void preProcess(Node node, JinjavaInterpreter interpreter); -} diff --git a/src/main/java/com/hubspot/jinjava/tree/Node.java b/src/main/java/com/hubspot/jinjava/tree/Node.java index 836d0ea31..b107c8eab 100644 --- a/src/main/java/com/hubspot/jinjava/tree/Node.java +++ b/src/main/java/com/hubspot/jinjava/tree/Node.java @@ -99,6 +99,6 @@ public String toTreeString(int level) { } public void preProcess(JinjavaInterpreter interpreter) { - interpreter.getConfig().getNodePreProcessor().preProcess(this, interpreter); + interpreter.getConfig().getNodePreProcessor().accept(this, interpreter); } } From 1e2ac9213a54ddf46fc162ddacc9038107c26256 Mon Sep 17 00:00:00 2001 From: Jeff Boulter Date: Mon, 13 Feb 2023 16:15:12 -0500 Subject: [PATCH 169/637] add provider --- .../java/com/hubspot/jinjava/JinjavaConfig.java | 14 ++++++++++++++ .../jinjava/interpret/JinjavaInterpreter.java | 4 ++-- .../objects/date/CurrentDateTimeProvider.java | 9 +++++++++ .../jinjava/objects/date/DateTimeProvider.java | 5 +++++ .../objects/date/FixedDateTimeProvider.java | 14 ++++++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/hubspot/jinjava/objects/date/CurrentDateTimeProvider.java create mode 100644 src/main/java/com/hubspot/jinjava/objects/date/DateTimeProvider.java create mode 100644 src/main/java/com/hubspot/jinjava/objects/date/FixedDateTimeProvider.java diff --git a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java index ad527458c..d854d21e7 100644 --- a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java +++ b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java @@ -25,6 +25,8 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreterFactory; import com.hubspot.jinjava.mode.DefaultExecutionMode; import com.hubspot.jinjava.mode.ExecutionMode; +import com.hubspot.jinjava.objects.date.CurrentDateTimeProvider; +import com.hubspot.jinjava.objects.date.DateTimeProvider; import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy; import com.hubspot.jinjava.tree.parse.DefaultTokenScannerSymbols; import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; @@ -62,6 +64,7 @@ public class JinjavaConfig { private final int rangeLimit; private final int maxNumDeferredTokens; private final InterpreterFactory interpreterFactory; + private final DateTimeProvider dateTimeProvider; private TokenScannerSymbols tokenScannerSymbols; private final ELResolver elResolver; private final ExecutionMode executionMode; @@ -121,6 +124,7 @@ private JinjavaConfig(Builder builder) { elResolver = builder.elResolver; executionMode = builder.executionMode; legacyOverrides = builder.legacyOverrides; + dateTimeProvider = builder.dateTimeProvider; enablePreciseDivideFilter = builder.enablePreciseDivideFilter; objectMapper = builder.objectMapper; } @@ -241,6 +245,10 @@ public boolean getEnablePreciseDivideFilter() { return enablePreciseDivideFilter; } + public DateTimeProvider getDateTimeProvider() { + return dateTimeProvider; + } + public static class Builder { private Charset charset = StandardCharsets.UTF_8; private Locale locale = Locale.ENGLISH; @@ -258,6 +266,7 @@ public static class Builder { private boolean nestedInterpretationEnabled = true; private RandomNumberGeneratorStrategy randomNumberGeneratorStrategy = RandomNumberGeneratorStrategy.THREAD_LOCAL; + private DateTimeProvider dateTimeProvider = new CurrentDateTimeProvider(); private boolean validationMode = false; private long maxStringLength = 0; private int rangeLimit = DEFAULT_RANGE_LIMIT; @@ -306,6 +315,11 @@ public Builder withRandomNumberGeneratorStrategy( return this; } + public Builder withDateTimeProvider(DateTimeProvider dateTimeProvider) { + this.dateTimeProvider = dateTimeProvider; + return this; + } + public Builder withTrimBlocks(boolean trimBlocks) { this.trimBlocks = trimBlocks; return this; diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index a3a392019..7ede5f016 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -156,9 +156,9 @@ public void addBlock(String name, BlockInfo blockInfo) { /** * Creates a new variable scope, extending from the current scope. Allows you to create a nested * contextual scope which can override variables from higher levels. - * + *

* Should be used in a try/finally context, similar to lock-use patterns: - * + *

* * interpreter.enterScope(); * try (interpreter.enterScope()) { diff --git a/src/main/java/com/hubspot/jinjava/objects/date/CurrentDateTimeProvider.java b/src/main/java/com/hubspot/jinjava/objects/date/CurrentDateTimeProvider.java new file mode 100644 index 000000000..937d4d00a --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/date/CurrentDateTimeProvider.java @@ -0,0 +1,9 @@ +package com.hubspot.jinjava.objects.date; + +public class CurrentDateTimeProvider implements DateTimeProvider { + + @Override + public long getCurrentTimeMillis() { + return System.currentTimeMillis(); + } +} diff --git a/src/main/java/com/hubspot/jinjava/objects/date/DateTimeProvider.java b/src/main/java/com/hubspot/jinjava/objects/date/DateTimeProvider.java new file mode 100644 index 000000000..ca6c53c9e --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/date/DateTimeProvider.java @@ -0,0 +1,5 @@ +package com.hubspot.jinjava.objects.date; + +public interface DateTimeProvider { + long getCurrentTimeMillis(); +} diff --git a/src/main/java/com/hubspot/jinjava/objects/date/FixedDateTimeProvider.java b/src/main/java/com/hubspot/jinjava/objects/date/FixedDateTimeProvider.java new file mode 100644 index 000000000..75bc71ce8 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/date/FixedDateTimeProvider.java @@ -0,0 +1,14 @@ +package com.hubspot.jinjava.objects.date; + +public class FixedDateTimeProvider implements DateTimeProvider { + private long currentTimeMillis; + + public FixedDateTimeProvider(long currentTimeMillis) { + this.currentTimeMillis = currentTimeMillis; + } + + @Override + public long getCurrentTimeMillis() { + return currentTimeMillis; + } +} From a65e215bc28d76f265679a8d048bb301ff548533 Mon Sep 17 00:00:00 2001 From: Jeff Boulter Date: Mon, 13 Feb 2023 16:47:24 -0500 Subject: [PATCH 170/637] use datetimeprovider in methods that get the current time --- .../com/hubspot/jinjava/lib/fn/Functions.java | 10 +++++- .../jinjava/objects/date/PyishDate.java | 13 +++++++- .../lib/filter/UnixTimestampFilterTest.java | 4 +-- .../jinjava/lib/fn/TodayFunctionTest.java | 31 +++++++++++++++++-- .../lib/fn/UnixTimestampFunctionTest.java | 31 ++++++++++++++----- .../jinjava/objects/date/PyishDateTest.java | 25 +++++++++++++++ 6 files changed, 100 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java b/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java index 94c2b2c07..e620d2988 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java @@ -13,6 +13,7 @@ import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.mode.ExecutionMode; import com.hubspot.jinjava.objects.Namespace; +import com.hubspot.jinjava.objects.date.DateTimeProvider; import com.hubspot.jinjava.objects.date.PyishDate; import com.hubspot.jinjava.objects.date.StrftimeFormatter; import com.hubspot.jinjava.tree.Node; @@ -245,7 +246,14 @@ public static ZonedDateTime getDateTimeArg(Object var, ZoneId zoneOffset) { JinjavaInterpreter.getCurrent().getPosition() ); } - d = ZonedDateTime.now(zoneOffset); + long currentMillis = JinjavaInterpreter + .getCurrentMaybe() + .map(JinjavaInterpreter::getConfig) + .map(JinjavaConfig::getDateTimeProvider) + .map(DateTimeProvider::getCurrentTimeMillis) + .orElse(System.currentTimeMillis()); + + d = ZonedDateTime.ofInstant(Instant.ofEpochMilli(currentMillis), zoneOffset); } else if (var instanceof Number) { d = ZonedDateTime.ofInstant( 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 9ffd07e5e..85443413d 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.objects.date; +import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.objects.PyWrapper; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; @@ -50,7 +51,17 @@ public PyishDate(Long epochMillis) { this( ZonedDateTime.ofInstant( Instant.ofEpochMilli( - Optional.ofNullable(epochMillis).orElseGet(System::currentTimeMillis) + Optional + .ofNullable(epochMillis) + .orElseGet( + () -> + JinjavaInterpreter + .getCurrentMaybe() + .map(JinjavaInterpreter::getConfig) + .map(JinjavaConfig::getDateTimeProvider) + .map(DateTimeProvider::getCurrentTimeMillis) + .orElse(System.currentTimeMillis()) + ) ), ZoneOffset.UTC ) diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilterTest.java index fc28df5e9..e52cea3de 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilterTest.java @@ -20,12 +20,12 @@ public void setup() { } @After - public void tearDown() throws Exception { + public void tearDown() { assertThat(interpreter.getErrorsCopy()).isEmpty(); } @Test - public void itRendersFromDate() throws Exception { + public void itRendersFromDate() { assertThat(interpreter.renderFlat("{{ d|unixtimestamp }}")).isEqualTo(timestamp); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/fn/TodayFunctionTest.java b/src/test/java/com/hubspot/jinjava/lib/fn/TodayFunctionTest.java index abb3830c4..63cbd9fcb 100644 --- a/src/test/java/com/hubspot/jinjava/lib/fn/TodayFunctionTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/fn/TodayFunctionTest.java @@ -10,12 +10,15 @@ import com.hubspot.jinjava.interpret.InvalidArgumentException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.mode.EagerExecutionMode; +import com.hubspot.jinjava.objects.date.FixedDateTimeProvider; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import org.junit.Test; public class TodayFunctionTest extends BaseInterpretingTest { + private static final String ZONE_NAME = "America/New_York"; + private static final ZoneId ZONE_ID = ZoneId.of(ZONE_NAME); @Test public void itDefaultsToUtcTimezone() { @@ -23,10 +26,32 @@ public void itDefaultsToUtcTimezone() { assertThat(zonedDateTime.getZone()).isEqualTo(ZoneOffset.UTC); } + @Test + public void itUsesFixedDateTimeProvider() { + long ts = 1233333414223L; + + JinjavaInterpreter.pushCurrent( + new JinjavaInterpreter( + new Jinjava(), + new Context(), + JinjavaConfig + .newBuilder() + .withDateTimeProvider(new FixedDateTimeProvider(ts)) + .build() + ) + ); + try { + assertThat(Functions.today(ZONE_NAME)) + .isEqualTo(ZonedDateTime.of(2009, 1, 30, 0, 0, 0, 0, ZONE_ID)); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + @Test public void itParsesTimezones() { - ZonedDateTime zonedDateTime = Functions.today("America/New_York"); - assertThat(zonedDateTime.getZone()).isEqualTo(ZoneId.of("America/New_York")); + ZonedDateTime zonedDateTime = Functions.today(ZONE_NAME); + assertThat(zonedDateTime.getZone()).isEqualTo(ZONE_ID); } @Test(expected = InvalidArgumentException.class) @@ -52,7 +77,7 @@ public void itDefersWhenExecutingEagerly() { ) ); try { - Functions.today("America/New_York"); + Functions.today(ZONE_NAME); } finally { JinjavaInterpreter.popCurrent(); } diff --git a/src/test/java/com/hubspot/jinjava/lib/fn/UnixTimestampFunctionTest.java b/src/test/java/com/hubspot/jinjava/lib/fn/UnixTimestampFunctionTest.java index e7cb1e3b9..de494fd85 100644 --- a/src/test/java/com/hubspot/jinjava/lib/fn/UnixTimestampFunctionTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/fn/UnixTimestampFunctionTest.java @@ -8,7 +8,9 @@ import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.mode.EagerExecutionMode; +import com.hubspot.jinjava.objects.date.FixedDateTimeProvider; import java.time.ZonedDateTime; +import org.assertj.core.data.Offset; import org.junit.Test; public class UnixTimestampFunctionTest { @@ -24,14 +26,29 @@ public void itGetsUnixTimestamps() { .isLessThanOrEqualTo(System.currentTimeMillis()); assertThat(Functions.unixtimestamp(epochMilliseconds)).isEqualTo(epochMilliseconds); assertThat(Functions.unixtimestamp(d)).isEqualTo(epochMilliseconds); - assertThat( - Math.abs( - Functions.unixtimestamp((Object) null) - - ZonedDateTime.now().toEpochSecond() * - 1000 - ) + assertThat(Functions.unixtimestamp((Object) null)) + .isCloseTo(System.currentTimeMillis(), Offset.offset(1000L)); + } + + @Test + public void itUsesFixedDateTimeProvider() { + long ts = 1233333414223L; + + JinjavaInterpreter.pushCurrent( + new JinjavaInterpreter( + new Jinjava(), + new Context(), + JinjavaConfig + .newBuilder() + .withDateTimeProvider(new FixedDateTimeProvider(ts)) + .build() ) - .isLessThan(1000); + ); + try { + assertThat(Functions.unixtimestamp((Object) null)).isEqualTo(ts); + } finally { + JinjavaInterpreter.popCurrent(); + } } @Test(expected = DeferredValueException.class) diff --git a/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java b/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java index 0c25fc12f..849c5636c 100644 --- a/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java @@ -3,8 +3,11 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; +import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Date; @@ -18,6 +21,28 @@ public void itUsesCurrentTimeWhenNoneProvided() { assertThat(d.toDate()).isCloseTo(new Date(), 10000); } + @Test + public void itUsesDateTimeProviderWhenNoTimeProvided() { + long ts = 123345414223L; + + JinjavaInterpreter.pushCurrent( + new JinjavaInterpreter( + new Jinjava(), + new Context(), + JinjavaConfig + .newBuilder() + .withDateTimeProvider(new FixedDateTimeProvider(ts)) + .build() + ) + ); + try { + PyishDate d = new PyishDate((Long) null); + assertThat(d.toDate()).isEqualTo(Date.from(Instant.ofEpochMilli(ts))); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + @Test public void testStrfmt() { PyishDate d = new PyishDate(ZonedDateTime.parse("2013-11-12T14:15:00+00:00")); From d733fdf38e7818daab5b9d8ccca480da5bed252d Mon Sep 17 00:00:00 2001 From: David Byron Date: Mon, 13 Feb 2023 15:51:04 -0800 Subject: [PATCH 171/637] fix(default): change default(null) to return null instead of "null" fixes https://github.com/HubSpot/jinjava/issues/429 --- .../jinjava/lib/filter/DefaultFilter.java | 2 +- .../jinjava/lib/filter/DefaultFilterTest.java | 22 +++++++++++++++++++ .../jinjava/lib/fn/TypeFunctionTest.java | 5 +++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/DefaultFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/DefaultFilter.java index 00766619b..c23548428 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/DefaultFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/DefaultFilter.java @@ -86,7 +86,7 @@ public Object filter( return object; } - return defaultValue instanceof PyWrapper + return (defaultValue instanceof PyWrapper) || (defaultValue == null) ? defaultValue : Objects.toString(defaultValue); } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/DefaultFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/DefaultFilterTest.java index 9893267cf..ca9977c53 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/DefaultFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DefaultFilterTest.java @@ -79,4 +79,26 @@ public void itIgnoresBadTruthyValue() { ) .isEqualTo("Type = str"); } + + @Test + public void itDefaultsNullToNull() { + assertThat( + jinjava.render( + "{% set d=d | default(null) %}{% if (d == null) %}default yields real null{% else %}default yields something other than null{% endif %}", + new HashMap<>() + ) + ) + .isEqualTo("default yields real null"); + } + + @Test + public void itDefaultsNullToNullWithTruthyParam() { + assertThat( + jinjava.render( + "{% set d=d | default(null, true) %}{% if (d == null) %}default with truthy yields real null{% else %}default with truthy yields something other than null{% endif %}", + new HashMap<>() + ) + ) + .isEqualTo("default with truthy yields real null"); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/fn/TypeFunctionTest.java b/src/test/java/com/hubspot/jinjava/lib/fn/TypeFunctionTest.java index ad01927e6..b237fe76c 100644 --- a/src/test/java/com/hubspot/jinjava/lib/fn/TypeFunctionTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/fn/TypeFunctionTest.java @@ -57,4 +57,9 @@ public void testBool() { public void testSafeString() { assertThat(TypeFunction.type(new SafeString("foo"))).isEqualTo("str"); } + + @Test + public void testNull() { + assertThat(TypeFunction.type(null)).isEqualTo("null"); + } } From d8ef52bb7fc7ebbb24990b79407447b88e73d8a3 Mon Sep 17 00:00:00 2001 From: Jeff Boulter Date: Tue, 14 Feb 2023 16:14:42 -0500 Subject: [PATCH 172/637] Update src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java Co-authored-by: Jack Smith <72623970+jasmith-hs@users.noreply.github.com> --- src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 85443413d..ee5722e45 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java @@ -60,7 +60,7 @@ public PyishDate(Long epochMillis) { .map(JinjavaInterpreter::getConfig) .map(JinjavaConfig::getDateTimeProvider) .map(DateTimeProvider::getCurrentTimeMillis) - .orElse(System.currentTimeMillis()) + .orElseGet(System::currentTimeMillis) ) ), ZoneOffset.UTC From d05b0d475026016f1bfc3a0761eee6422ea2cd6c Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 21 Feb 2023 17:09:03 -0500 Subject: [PATCH 173/637] Macro functions don't need to start not in deferred execution mode. This is dead code and causes problems if the macro function modifies variables --- src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java | 1 - .../jinjava/lib/expression/EagerExpressionStrategyTest.java | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java index c63f9453b..b2efa44fd 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java @@ -77,7 +77,6 @@ public Object doEvaluate( JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); Optional importFile = getImportFile(interpreter); try (InterpreterScopeClosable c = interpreter.enterScope()) { - interpreter.getContext().setDeferredExecutionMode(false); String result = getEvaluationResult(argMap, kwargMap, varArgs, interpreter); if ( diff --git a/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java b/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java index c3c457397..9eae2ff80 100644 --- a/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java @@ -133,13 +133,13 @@ public void itGoesIntoDeferredExecutionMode() { } @Test - public void itDoesNotGoIntoDeferredExecutionModeWithMacro() { + public void itGoesIntoDeferredExecutionModeWithMacro() { assertExpectedOutput( "{% macro def() %}{{ is_deferred_execution_mode() }}{% endmacro %}" + "{{ def() }}" + "{% if deferred %}{{ def() }}{% endif %}" + "{{ def() }}", - "false{% if deferred %}false{% endif %}false" + "false{% if deferred %}true{% endif %}false" ); } From 159bdb8922cafd28b00e7f89b6abeb35a6101efd Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Tue, 21 Feb 2023 17:21:18 -0500 Subject: [PATCH 174/637] Add additional test to demonstrate why macro functions shouldn't start off by turning deferred execution mode off --- src/test/java/com/hubspot/jinjava/EagerTest.java | 7 +++++++ ...unction-in-deferred-execution-mode.expected.jinja | 10 ++++++++++ ...s-macro-function-in-deferred-execution-mode.jinja | 12 ++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 src/test/resources/eager/runs-macro-function-in-deferred-execution-mode.expected.jinja create mode 100644 src/test/resources/eager/runs-macro-function-in-deferred-execution-mode.jinja diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 04544fa0f..9ef57f9ac 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1266,4 +1266,11 @@ public void itOnlyDefersLoopItemOnCurrentContext() { "only-defers-loop-item-on-current-context" ); } + + @Test + public void itRunsMacroFunctionInDeferredExecutionMode() { + expectedTemplateInterpreter.assertExpectedOutput( + "runs-macro-function-in-deferred-execution-mode" + ); + } } diff --git a/src/test/resources/eager/runs-macro-function-in-deferred-execution-mode.expected.jinja b/src/test/resources/eager/runs-macro-function-in-deferred-execution-mode.expected.jinja new file mode 100644 index 000000000..a269291c7 --- /dev/null +++ b/src/test/resources/eager/runs-macro-function-in-deferred-execution-mode.expected.jinja @@ -0,0 +1,10 @@ +{% set holder = {'val': -1} %}{% for i in range(deferred) %} +{% set __macro_modify_615470226_temp_variable_1__ %} +{% do holder.update({'val': holder.val + 1}) %} +{% endset %}{% do __macro_modify_615470226_temp_variable_1__ %} +{% if holder.val >= 1 %} +{{ i }} +{% endif %} +{% endfor %} + +{{ holder.val }} \ No newline at end of file diff --git a/src/test/resources/eager/runs-macro-function-in-deferred-execution-mode.jinja b/src/test/resources/eager/runs-macro-function-in-deferred-execution-mode.jinja new file mode 100644 index 000000000..9670c58de --- /dev/null +++ b/src/test/resources/eager/runs-macro-function-in-deferred-execution-mode.jinja @@ -0,0 +1,12 @@ +{% macro modify(val) %} +{% do holder.update({'val': holder.val + 1}) %} +{% endmacro %} +{% set holder = {'val': -1} %} +{% for i in range(deferred) %} +{% do modify(1) %} +{% if holder.val >= 1 %} +{{ i }} +{% endif %} +{% endfor %} + +{{ holder.val }} From 9c77b0d6315a783b43bb57c8897b08cf1d7ea099 Mon Sep 17 00:00:00 2001 From: offa Date: Thu, 8 Dec 2022 20:29:57 +0100 Subject: [PATCH 175/637] Add Github Actions CI --- .github/workflows/ci.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..9223f659c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: ci + +on: [ push, pull_request ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + java: [ 11, 17 ] + name: jdk-${{ matrix.java }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: ${{ matrix.java }} + cache: "maven" + - name: "Build" + run: mvn --batch-mode -no-transfer-progress -V verify + From 0d9f8f6e4cd4b08210773251c63e12c87fc2e693 Mon Sep 17 00:00:00 2001 From: offa Date: Thu, 8 Dec 2022 20:48:39 +0100 Subject: [PATCH 176/637] Remove Travis CI config --- .travis.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 699ebab4f..000000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -sudo: false -language: java -jdk: - - oraclejdk11 - -# https://travis-ci.community/t/error-installing-oraclejdk8-expected-feature-release-number-in-range-of-9-to-14-but-got-8/3766 -dist: trusty - -cache: - directories: - - $HOME/.m2 - -after_success: - - travis_wait mvn validate -e - - codecov - From 76976893c697fe65bbff892d2c7a4ce6c9a59e1c Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 27 Feb 2023 12:04:10 -0500 Subject: [PATCH 177/637] Don't hardcode french grouping separator. It is a different unicode value in java 17 --- .../lib/filter/FormatNumberFilterTest.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java index 25b715a0a..d0134165d 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java @@ -3,7 +3,9 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hubspot.jinjava.BaseJinjavaTest; +import java.text.DecimalFormatSymbols; import java.util.HashMap; +import java.util.Locale; import org.junit.Before; import org.junit.Test; @@ -36,21 +38,36 @@ public void testFormatNumberFilter() { assertThat( jinjava.render("{{ 1000|format_number('fr') }}", new HashMap()) ) - .isEqualTo("1\u00a0000"); + .isEqualTo( + String.format( + "1%s000", + DecimalFormatSymbols.getInstance(Locale.FRENCH).getGroupingSeparator() + ) + ); assertThat( jinjava.render( "{{ 1000.333|format_number('fr') }}", new HashMap() ) ) - .isEqualTo("1\u00a0000,333"); + .isEqualTo( + String.format( + "1%s000,333", + DecimalFormatSymbols.getInstance(Locale.FRENCH).getGroupingSeparator() + ) + ); assertThat( jinjava.render( "{{ 1000.333|format_number('fr', 2) }}", new HashMap() ) ) - .isEqualTo("1\u00a0000,33"); + .isEqualTo( + String.format( + "1%s000,33", + DecimalFormatSymbols.getInstance(Locale.FRENCH).getGroupingSeparator() + ) + ); assertThat( jinjava.render("{{ 1000|format_number('es') }}", new HashMap()) From 31ad89107e485b12c56c190270d89da126401ae6 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 27 Feb 2023 12:04:37 -0500 Subject: [PATCH 178/637] Add open to allow reflection for InjectedContextFunctionProxyTest --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9223f659c..64d1afa0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,5 +19,6 @@ jobs: java-version: ${{ matrix.java }} cache: "maven" - name: "Build" - run: mvn --batch-mode -no-transfer-progress -V verify - + run: mvn --batch-mode -no-transfer-progress -V verify + env: + JDK_JAVA_OPTIONS: --add-opens=java.base/java.lang=ALL-UNNAMED From 717a2e2b826776f37a1c4840331d126326770133 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 27 Feb 2023 12:17:55 -0500 Subject: [PATCH 179/637] Reduce speedup factor as running on Java 11 Temurin through GitHub Actions is barely twice as fast with the new replace method --- .../com/hubspot/jinjava/lib/filter/EscapeFilterTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java index 7368c49c5..af3a07f7d 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java @@ -54,8 +54,9 @@ public void testNewStringReplaceIsFaster() { assertThat(newResult).isEqualTo(oldResult); System.out.printf("New: %d Old:%d\n", newTime.toMillis(), oldTime.toMillis()); - int speedUpFactor = getVersion() < 17 ? 2 : 1; // On M1, it is between 50 and 100 times faster. Difference is much smaller on java 17 - assertThat(newTime.toMillis()).isLessThan(oldTime.toMillis() / speedUpFactor); + double speedUpFactor = getVersion() < 17 ? 1.5d : 1; // On M1, it is between 50 and 100 times faster. Difference is much smaller on java 17 + assertThat(newTime.toMillis()) + .isLessThan((long) (oldTime.toMillis() / speedUpFactor)); } private static String fixture(String name) { From 65e1b72e6ceae96198732f8ee64e8c25e50a0287 Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Thu, 2 Mar 2023 15:07:01 -0500 Subject: [PATCH 180/637] Create context method for checking if in for loop --- .../hubspot/jinjava/interpret/Context.java | 5 ++++ .../com/hubspot/jinjava/lib/tag/ForTag.java | 3 ++- .../hubspot/jinjava/lib/tag/ForTagTest.java | 24 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/interpret/Context.java b/src/main/java/com/hubspot/jinjava/interpret/Context.java index 8e4d2d30d..c515948a1 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/Context.java +++ b/src/main/java/com/hubspot/jinjava/interpret/Context.java @@ -29,6 +29,7 @@ import com.hubspot.jinjava.lib.fn.ELFunctionDefinition; import com.hubspot.jinjava.lib.fn.FunctionLibrary; import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.lib.tag.ForTag; import com.hubspot.jinjava.lib.tag.Tag; import com.hubspot.jinjava.lib.tag.TagLibrary; import com.hubspot.jinjava.lib.tag.eager.DeferredToken; @@ -799,4 +800,8 @@ public Node getCurrentNode() { public void setCurrentNode(final Node currentNode) { this.currentNode = currentNode; } + + public boolean isInForLoop() { + return get(ForTag.LOOP) != null; + } } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java index 34631a01a..49cadd0d1 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java @@ -95,8 +95,9 @@ public class ForTag implements Tag { public static final String TAG_NAME = "for"; + public static final String LOOP = "loop"; + private static final long serialVersionUID = 6175143875754966497L; - private static final String LOOP = "loop"; private static final Pattern IN_PATTERN = Pattern.compile("\\sin\\s"); public static final String TOO_LARGE_EXCEPTION_MESSAGE = "Loop too large"; diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java index aed122698..f42c45872 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java @@ -16,6 +16,7 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.RenderResult; import com.hubspot.jinjava.interpret.TemplateError; +import com.hubspot.jinjava.lib.fn.ELFunctionDefinition; import com.hubspot.jinjava.objects.date.PyishDate; import com.hubspot.jinjava.tree.Node; import com.hubspot.jinjava.tree.TagNode; @@ -349,4 +350,27 @@ public void itCatchesConcurrentModificationInLoop() { assertThat(rendered.getErrors().get(0).getMessage()) .contains("Cannot modify collection in 'for' loop"); } + + @Test + public void itAllowsCheckingOfWithinForLoop() throws NoSuchMethodException { + Map context = Maps.newHashMap(); + String template = + "{% set test = [1, 2] %}{{ in_for_loop() }} {% for i in test %}{{ in_for_loop() }} {% endfor %}{{ in_for_loop() }}"; + + jinjava.registerFunction( + new ELFunctionDefinition( + "", + "in_for_loop", + this.getClass().getDeclaredMethod("inForLoop") + ) + ); + + RenderResult rendered = jinjava.renderForResult(template, context); + assertThat(rendered.getOutput()).isEqualTo("false true true false"); + } + + public static boolean inForLoop() { + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); + return interpreter.getContext().isInForLoop(); + } } From 75cedd9569dfe1774595cb11f3fef27b833cff24 Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Thu, 2 Mar 2023 15:10:59 -0500 Subject: [PATCH 181/637] setup and teardown --- .../hubspot/jinjava/lib/tag/ForTagTest.java | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java index f42c45872..2fb118cd6 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java @@ -29,6 +29,7 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; +import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -36,8 +37,26 @@ public class ForTagTest extends BaseInterpretingTest { public Tag tag; @Before - public void setup() { + public void setup() throws Exception { tag = new ForTag(); + + jinjava + .getGlobalContext() + .registerFunction( + new ELFunctionDefinition( + "", + "in_for_loop", + this.getClass().getDeclaredMethod("inForLoop") + ) + ); + interpreter = + new JinjavaInterpreter(jinjava, context, JinjavaConfig.newBuilder().build()); + JinjavaInterpreter.pushCurrent(interpreter); + } + + @After + public void teardown() { + JinjavaInterpreter.popCurrent(); } @Test @@ -357,14 +376,6 @@ public void itAllowsCheckingOfWithinForLoop() throws NoSuchMethodException { String template = "{% set test = [1, 2] %}{{ in_for_loop() }} {% for i in test %}{{ in_for_loop() }} {% endfor %}{{ in_for_loop() }}"; - jinjava.registerFunction( - new ELFunctionDefinition( - "", - "in_for_loop", - this.getClass().getDeclaredMethod("inForLoop") - ) - ); - RenderResult rendered = jinjava.renderForResult(template, context); assertThat(rendered.getOutput()).isEqualTo("false true true false"); } From 7b0bf13289ec48fe81899988085bb4ff232b3da5 Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Thu, 2 Mar 2023 15:13:04 -0500 Subject: [PATCH 182/637] move some stuff around again --- .../hubspot/jinjava/lib/tag/ForTagTest.java | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java index 2fb118cd6..9b5af78f6 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java @@ -29,7 +29,6 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; -import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -37,26 +36,24 @@ public class ForTagTest extends BaseInterpretingTest { public Tag tag; @Before - public void setup() throws Exception { + @Override + public void baseSetup() { + super.baseSetup(); tag = new ForTag(); - jinjava - .getGlobalContext() - .registerFunction( - new ELFunctionDefinition( - "", - "in_for_loop", - this.getClass().getDeclaredMethod("inForLoop") - ) - ); - interpreter = - new JinjavaInterpreter(jinjava, context, JinjavaConfig.newBuilder().build()); - JinjavaInterpreter.pushCurrent(interpreter); - } - - @After - public void teardown() { - JinjavaInterpreter.popCurrent(); + try { + jinjava + .getGlobalContext() + .registerFunction( + new ELFunctionDefinition( + "", + "in_for_loop", + this.getClass().getDeclaredMethod("inForLoop") + ) + ); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } } @Test From d2aa98b38f3555832abb84b6fcad485efc99b72e Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Thu, 2 Mar 2023 15:15:28 -0500 Subject: [PATCH 183/637] fix extended test --- .../com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java index 827a6003e..4a7dd16fe 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerForTagTest.java @@ -236,4 +236,9 @@ public void itDoesNotSwallowDeferredValueException() { interpreter.render(input); assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); } + + public static boolean inForLoop() { + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); + return interpreter.getContext().isInForLoop(); + } } From a76ba0cc512298a0377097a7ec733b9df4da9739 Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Thu, 2 Mar 2023 16:46:13 -0500 Subject: [PATCH 184/637] Filter duplicate template errors --- .../jinjava/interpret/JinjavaInterpreter.java | 14 +++++-- .../jinjava/interpret/TemplateError.java | 3 +- .../interpret/JinjavaInterpreterTest.java | 42 +++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index 76ff3d3d2..876c8a732 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -54,6 +54,7 @@ import com.hubspot.jinjava.util.WhitespaceUtils; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -87,7 +88,9 @@ public class JinjavaInterpreter implements PyishSerializable { private int position = 0; private int scopeDepth = 1; private BlockInfo currentBlock; - private final List errors = new LinkedList<>(); + private final List errors = new ArrayList<>(); + private final Set errorSet = new HashSet<>(); + private static final int MAX_ERROR_SIZE = 100; public JinjavaInterpreter( @@ -738,9 +741,14 @@ public void addError(TemplateError templateError) { templateError.setLineno(context.getCurrentPathStack().getTopLineNumber()); } - // Limit the number of error. + // Limit the number of errors and filter duplicates if (errors.size() < MAX_ERROR_SIZE) { - this.errors.add(templateError.withScopeDepth(scopeDepth)); + templateError = templateError.withScopeDepth(scopeDepth); + int errorCode = templateError.hashCode(); + if (!errorSet.contains(errorCode)) { + this.errors.add(templateError); + this.errorSet.add(errorCode); + } } } diff --git a/src/main/java/com/hubspot/jinjava/interpret/TemplateError.java b/src/main/java/com/hubspot/jinjava/interpret/TemplateError.java index ff143aadb..8d4f63285 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/TemplateError.java +++ b/src/main/java/com/hubspot/jinjava/interpret/TemplateError.java @@ -540,7 +540,8 @@ public int hashCode() { startPosition, category, categoryErrors, - scopeDepth + scopeDepth, + sourceTemplate ); } } diff --git a/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java b/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java index 9233e6685..6b6b7733b 100644 --- a/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java +++ b/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java @@ -461,4 +461,46 @@ public void itDefaultsToUnitedStatesOnEmptyLocaleInFormattedDate() { StrftimeFormatter.format(date, "medium", Locale.forLanguageTag("en-US")) ); } + + @Test + public void itFiltersDuplicateErrors() { + TemplateError error1 = new TemplateError( + TemplateError.ErrorType.WARNING, + TemplateError.ErrorReason.OTHER, + TemplateError.ErrorItem.FILTER, + "the first error", + "list", + interpreter.getLineNumber(), + interpreter.getPosition(), + null + ); + + TemplateError copiedError1 = new TemplateError( + TemplateError.ErrorType.WARNING, + TemplateError.ErrorReason.OTHER, + TemplateError.ErrorItem.FILTER, + "the first error", + "list", + interpreter.getLineNumber(), + interpreter.getPosition(), + null + ); + + TemplateError error2 = new TemplateError( + TemplateError.ErrorType.WARNING, + TemplateError.ErrorReason.OTHER, + TemplateError.ErrorItem.FILTER, + "the second error", + "list", + interpreter.getLineNumber(), + interpreter.getPosition(), + null + ); + + interpreter.addError(error1); + interpreter.addError(error2); + interpreter.addError(copiedError1); + + assertThat(interpreter.getErrors()).containsExactly(error1, error2); + } } From 878c69cd1a581d6d92b814e993263ec098ea1280 Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Thu, 2 Mar 2023 16:49:45 -0500 Subject: [PATCH 185/637] cover remove case --- .../java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index 876c8a732..91dcfc453 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -754,7 +754,8 @@ public void addError(TemplateError templateError) { public void removeLastError() { if (!errors.isEmpty()) { - errors.remove(errors.size() - 1); + TemplateError error = errors.remove(errors.size() - 1); + errorSet.remove(error.hashCode()); } } From 6bda1d5664c6dc3acc372d2ac3a908f2c43fb74e Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Thu, 2 Mar 2023 16:50:52 -0500 Subject: [PATCH 186/637] fix tests --- .../java/com/hubspot/jinjava/lib/tag/ValidationModeTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/ValidationModeTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/ValidationModeTest.java index 0d8c9b6a9..27cbadf9e 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/ValidationModeTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/ValidationModeTest.java @@ -114,7 +114,7 @@ public void itResolvesAllForExpressionsInValidationMode() { "{{ badCode( }}" + "{% for i in [1, 2, 3] %}" + " {{ badCode( }}" + "{% endfor %}" ); - assertThat(validatingInterpreter.getErrors().size()).isEqualTo(4); + assertThat(validatingInterpreter.getErrors().size()).isEqualTo(2); } @Test @@ -130,7 +130,7 @@ public void itResolvesNestedForExpressionsInValidationMode() { "done" ); - assertThat(validatingInterpreter.getErrors().size()).isEqualTo(4); + assertThat(validatingInterpreter.getErrors().size()).isEqualTo(2); assertThat(output.trim()).isEqualTo("done"); } From 5f952e0f8e8bed16c76885fa21fae34227c82536 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 2 Mar 2023 17:37:09 -0500 Subject: [PATCH 187/637] Put deferred macro output into its own scope so that set tags don't modify variables in the wrong scope when doing recursive evaluation --- .../hubspot/jinjava/lib/fn/MacroFunction.java | 34 ++++++++++--------- .../java/com/hubspot/jinjava/EagerTest.java | 15 ++++++++ ...fications-in-scope.expected.expected.jinja | 7 ++++ ...acro-modifications-in-scope.expected.jinja | 10 ++++++ .../keeps-macro-modifications-in-scope.jinja | 15 ++++++++ 5 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 src/test/resources/eager/keeps-macro-modifications-in-scope.expected.expected.jinja create mode 100644 src/test/resources/eager/keeps-macro-modifications-in-scope.expected.jinja create mode 100644 src/test/resources/eager/keeps-macro-modifications-in-scope.jinja diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java index b2efa44fd..8faf0a585 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java @@ -9,6 +9,7 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; import com.hubspot.jinjava.tree.Node; +import com.hubspot.jinjava.util.EagerReconstructionUtils; import com.hubspot.jinjava.util.LengthLimitingStringBuilder; import java.util.HashMap; import java.util.LinkedHashMap; @@ -78,24 +79,25 @@ public Object doEvaluate( Optional importFile = getImportFile(interpreter); try (InterpreterScopeClosable c = interpreter.enterScope()) { String result = getEvaluationResult(argMap, kwargMap, varArgs, interpreter); - if ( - !interpreter.getContext().isPartialMacroEvaluation() && - ( - !interpreter.getContext().getDeferredNodes().isEmpty() || - !interpreter.getContext().getDeferredTokens().isEmpty() - ) + !interpreter.getContext().getDeferredNodes().isEmpty() || + !interpreter.getContext().getDeferredTokens().isEmpty() ) { - String tempVarName = MacroFunctionTempVariable.getVarName( - getName(), - hashCode(), - currentCallCount - ); - interpreter - .getContext() - .getParent() - .put(tempVarName, new MacroFunctionTempVariable(result)); - throw new DeferredParsingException(this, tempVarName); + if (!interpreter.getContext().isPartialMacroEvaluation()) { + String tempVarName = MacroFunctionTempVariable.getVarName( + getName(), + hashCode(), + currentCallCount + ); + interpreter + .getContext() + .getParent() + .put(tempVarName, new MacroFunctionTempVariable(result)); + throw new DeferredParsingException(this, tempVarName); + } + if (interpreter.getContext().isDeferredExecutionMode()) { + return EagerReconstructionUtils.wrapInChildScope(result, interpreter); + } } return result; diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 9ef57f9ac..6f8e95035 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1273,4 +1273,19 @@ public void itRunsMacroFunctionInDeferredExecutionMode() { "runs-macro-function-in-deferred-execution-mode" ); } + + @Test + public void itKeepsMacroModificationsInScope() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "keeps-macro-modifications-in-scope" + ); + } + + @Test + public void itKeepsMacroModificationsInScopeSecondPass() { + interpreter.getContext().put("deferred", true); + expectedTemplateInterpreter.assertExpectedOutput( + "keeps-macro-modifications-in-scope.expected" + ); + } } diff --git a/src/test/resources/eager/keeps-macro-modifications-in-scope.expected.expected.jinja b/src/test/resources/eager/keeps-macro-modifications-in-scope.expected.expected.jinja new file mode 100644 index 000000000..23d81650a --- /dev/null +++ b/src/test/resources/eager/keeps-macro-modifications-in-scope.expected.expected.jinja @@ -0,0 +1,7 @@ +1 +2 +3 +3 +2 +3 +3 diff --git a/src/test/resources/eager/keeps-macro-modifications-in-scope.expected.jinja b/src/test/resources/eager/keeps-macro-modifications-in-scope.expected.jinja new file mode 100644 index 000000000..5b50f72a3 --- /dev/null +++ b/src/test/resources/eager/keeps-macro-modifications-in-scope.expected.jinja @@ -0,0 +1,10 @@ +{% set list = [] %}{% if deferred %} + +{% set list = [] %}{% for __ignored__ in [0] %}{% do list.append(1) %}1{% set depth = 2 %} +{% for __ignored__ in [0] %}{% do list.append(2) %}2{% set depth = 3 %} +{% for __ignored__ in [0] %}{% do list.append(3) %}3{% endfor %} +{% for __ignored__ in [0] %}{% do list.append(3) %}3{% endfor %}{% endfor %} +{% for __ignored__ in [0] %}{% do list.append(2) %}2{% set depth = 3 %} +{% for __ignored__ in [0] %}{% do list.append(3) %}3{% endfor %} +{% for __ignored__ in [0] %}{% do list.append(3) %}3{% endfor %}{% endfor %}{% endfor %} +{% endif %} diff --git a/src/test/resources/eager/keeps-macro-modifications-in-scope.jinja b/src/test/resources/eager/keeps-macro-modifications-in-scope.jinja new file mode 100644 index 000000000..89423a419 --- /dev/null +++ b/src/test/resources/eager/keeps-macro-modifications-in-scope.jinja @@ -0,0 +1,15 @@ +{%- set list = [] %} +{%- if deferred %} +{%- macro inc(val, depth) %} +{%- do list.append(depth) -%} +{{ depth }} +{%- if depth < 3 %} +{%- set depth = depth + 1 %} +{{ inc(val, depth) }} +{{ inc(val, depth) }} +{%- endif %} +{%- endmacro %} + +{{ inc('a', 1) }} +{% endif %} + From 453818a7856c61baa6982050489c68fed9631e06 Mon Sep 17 00:00:00 2001 From: Tyler Kindy Date: Fri, 3 Mar 2023 09:19:37 -0500 Subject: [PATCH 188/637] Delete benchmark (#1018) This PR deletes the benchmark "module" which uses Jinja and Liquid templates for performance testing. We never actually run this benchmark and, since it isn't an actual Maven submodule in this project, it leads to build errors on our internal build system. --- benchmark/README.md | 9 - benchmark/jinja2 | 1 - benchmark/liquid | 1 - benchmark/pom.xml | 175 ----------- benchmark/resources/jinja/simple.jinja | 27 -- benchmark/resources/logback.xml | 18 -- .../jinjava/benchmarks/jinja2/Article.java | 63 ---- .../benchmarks/jinja2/Jinja2Benchmark.java | 110 ------- .../jinjava/benchmarks/jinja2/User.java | 21 -- .../jinjava/benchmarks/liquid/Filters.java | 291 ------------------ .../benchmarks/liquid/LiquidBenchmark.java | 154 --------- .../jinjava/benchmarks/liquid/Tags.java | 84 ----- 12 files changed, 954 deletions(-) delete mode 100644 benchmark/README.md delete mode 160000 benchmark/jinja2 delete mode 160000 benchmark/liquid delete mode 100644 benchmark/pom.xml delete mode 100644 benchmark/resources/jinja/simple.jinja delete mode 100644 benchmark/resources/logback.xml delete mode 100644 benchmark/src/main/java/com/hubspot/jinjava/benchmarks/jinja2/Article.java delete mode 100644 benchmark/src/main/java/com/hubspot/jinjava/benchmarks/jinja2/Jinja2Benchmark.java delete mode 100644 benchmark/src/main/java/com/hubspot/jinjava/benchmarks/jinja2/User.java delete mode 100644 benchmark/src/main/java/com/hubspot/jinjava/benchmarks/liquid/Filters.java delete mode 100644 benchmark/src/main/java/com/hubspot/jinjava/benchmarks/liquid/LiquidBenchmark.java delete mode 100644 benchmark/src/main/java/com/hubspot/jinjava/benchmarks/liquid/Tags.java diff --git a/benchmark/README.md b/benchmark/README.md deleted file mode 100644 index 1ae12810f..000000000 --- a/benchmark/README.md +++ /dev/null @@ -1,9 +0,0 @@ -Jinjava Benchmarks -================== - -To Run: - - mvn clean package - java -jar target/benchmarks.jar - - diff --git a/benchmark/jinja2 b/benchmark/jinja2 deleted file mode 160000 index 85820fceb..000000000 --- a/benchmark/jinja2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 85820fceb83569df62fa5e6b9b0f2f76b7c6a3cf diff --git a/benchmark/liquid b/benchmark/liquid deleted file mode 160000 index e2f8b28f5..000000000 --- a/benchmark/liquid +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e2f8b28f56296ec767eb225be0ddf36f01016613 diff --git a/benchmark/pom.xml b/benchmark/pom.xml deleted file mode 100644 index 740b1f2df..000000000 --- a/benchmark/pom.xml +++ /dev/null @@ -1,175 +0,0 @@ - - - - 4.0.0 - - com.hubspot.content - jinjava-benchmark - 1.0-SNAPSHOT - - JMH benchmark sample: Java - - - - - - com.hubspot.jinjava - jinjava - 2.4.1-SNAPSHOT - - - commons-io - commons-io - 2.7 - - - org.slf4j - slf4j-api - 1.7.25 - - - ch.qos.logback - logback-classic - 1.2.0 - - - org.yaml - snakeyaml - 1.31 - - - de.sven-jacobs - loremipsum - 1.0 - - - - org.openjdk.jmh - jmh-core - ${jmh.version} - - - org.openjdk.jmh - jmh-generator-annprocess - ${jmh.version} - provided - - - - - UTF-8 - 1.21 - 1.8 - benchmarks - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - ${javac.target} - ${javac.target} - ${javac.target} - - - - org.apache.maven.plugins - maven-shade-plugin - 2.2 - - - package - - shade - - - ${uberjar.name} - - - org.openjdk.jmh.Main - - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - - - - - - maven-clean-plugin - 2.5 - - - maven-deploy-plugin - 2.8.1 - - - maven-install-plugin - 2.5.1 - - - maven-jar-plugin - 2.4 - - - maven-javadoc-plugin - 2.9.1 - - - maven-resources-plugin - 2.6 - - - maven-site-plugin - 3.3 - - - maven-source-plugin - 2.2.1 - - - maven-surefire-plugin - 2.17 - - - - - - diff --git a/benchmark/resources/jinja/simple.jinja b/benchmark/resources/jinja/simple.jinja deleted file mode 100644 index d60f09782..000000000 --- a/benchmark/resources/jinja/simple.jinja +++ /dev/null @@ -1,27 +0,0 @@ - - - - {{page_title|e}} - - -

-

{{page_title|e}}

-
- -
- - {% for row in table %} - - {% for cell in row %} - - {% endfor %} - - {% endfor %} -
{{cell}}
-
- - diff --git a/benchmark/resources/logback.xml b/benchmark/resources/logback.xml deleted file mode 100644 index 859cb540e..000000000 --- a/benchmark/resources/logback.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - - diff --git a/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/jinja2/Article.java b/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/jinja2/Article.java deleted file mode 100644 index acd85f369..000000000 --- a/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/jinja2/Article.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.hubspot.jinjava.benchmarks.jinja2; - -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Date; - -import de.svenjacobs.loremipsum.LoremIpsum; - -public class Article { - - private int id; - private String href; - private String title; - private User user; - private String body; - private Date pubDate; - private boolean published; - - public Article(int id, User user) throws NoSuchAlgorithmException { - this.id = id; - this.href = "/article/" + id; - - LoremIpsum ipsum = new LoremIpsum(); - SecureRandom rnd = SecureRandom.getInstanceStrong(); - - this.title = ipsum.getWords(10); - this.user = user; - this.body = ipsum.getParagraphs(); - this.pubDate = Date.from(LocalDateTime.now().minusHours(rnd.nextInt(128)).toInstant(ZoneOffset.UTC)); - this.published = true; - } - - public int getId() { - return id; - } - - public String getHref() { - return href; - } - - public String getTitle() { - return title; - } - - public User getUser() { - return user; - } - - public String getBody() { - return body; - } - - public Date getPubDate() { - return pubDate; - } - - public boolean isPublished() { - return published; - } - -} diff --git a/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/jinja2/Jinja2Benchmark.java b/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/jinja2/Jinja2Benchmark.java deleted file mode 100644 index acadd8c37..000000000 --- a/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/jinja2/Jinja2Benchmark.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.hubspot.jinjava.benchmarks.jinja2; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.slf4j.LoggerFactory; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import com.hubspot.jinjava.Jinjava; -import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.loader.FileLocator; -import com.hubspot.jinjava.loader.ResourceLocator; -import com.hubspot.jinjava.tree.Node; - -import ch.qos.logback.classic.Level; - -@State(Scope.Benchmark) -public class Jinja2Benchmark { - - public String complexTemplate; - public Map complexBindings; - - public Jinjava jinjava; - - public JinjavaInterpreter interpreter; - public Node precompiledTemplate; - - @SuppressWarnings("unchecked") - @Setup - public void setup() throws IOException, NoSuchAlgorithmException { - ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME); - logger.setLevel(Level.WARN); - - jinjava = new Jinjava(); - interpreter = jinjava.newInterpreter(); - - FileLocator locator = new FileLocator(new File("jinja2/examples/rwbench/jinja")); - final String helpersTemplate = locator.getString("helpers.html", StandardCharsets.UTF_8, interpreter); - final String indexTemplate = locator.getString("index.html", StandardCharsets.UTF_8, interpreter); - final String layoutTemplate = locator.getString("layout.html", StandardCharsets.UTF_8, interpreter); - - jinjava.setResourceLocator(new ResourceLocator() { - @Override - public String getString(String fullName, Charset encoding, JinjavaInterpreter interpreter) throws IOException { - switch (fullName) { - case "helpers.html": - return helpersTemplate; - case "layout.html": - return layoutTemplate; - case "index.html": - return indexTemplate; - } - return null; - } - }); - - complexTemplate = indexTemplate; - // for tag doesn't support postfix conditional filtering - complexTemplate = complexTemplate.replaceAll(" if article.published", ""); - - List users = Lists.newArrayList(new User("John Doe"), new User("Jane Doe"), new User("Peter Somewhat")); - SecureRandom rnd = SecureRandom.getInstanceStrong(); - List
articles = new ArrayList<>(); - for (int i = 0; i < 20; i++) { - articles.add(new Article(i, users.get(rnd.nextInt(users.size())))); - } - List> navigation = Lists.newArrayList( - Lists.newArrayList("index", "Index"), - Lists.newArrayList("about", "About"), - Lists.newArrayList("foo?bar=1", "Foo with Bar"), - Lists.newArrayList("foo?bar=2&s=x", "Foo with X"), - Lists.newArrayList("blah", "Blub Blah"), - Lists.newArrayList("hehe", "Haha")); - - complexBindings = ImmutableMap.of("users", users, "articles", articles, "navigation", navigation); - - precompiledTemplate = interpreter.parse(complexTemplate); - } - - @Benchmark - public String realWorldishBenchmark() { - return jinjava.render(complexTemplate, complexBindings); - } - - @Benchmark - public String precompiledBenchmark() { - return interpreter.render(precompiledTemplate, true); - } - - public static void main(String[] args) throws Exception { - Jinja2Benchmark b = new Jinja2Benchmark(); - b.setup(); - System.out.println(b.realWorldishBenchmark()); - System.out.println(b.precompiledBenchmark()); - System.out.println(b.precompiledBenchmark()); - } - -} diff --git a/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/jinja2/User.java b/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/jinja2/User.java deleted file mode 100644 index df0641b95..000000000 --- a/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/jinja2/User.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.hubspot.jinjava.benchmarks.jinja2; - -public class User { - - private String href; - private String username; - - public User(String username) { - this.href = "/user/" + username; - this.username = username; - } - - public String getHref() { - return href; - } - - public String getUsername() { - return username; - } - -} diff --git a/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/liquid/Filters.java b/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/liquid/Filters.java deleted file mode 100644 index 492543196..000000000 --- a/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/liquid/Filters.java +++ /dev/null @@ -1,291 +0,0 @@ -package com.hubspot.jinjava.benchmarks.liquid; - -import static org.apache.commons.lang3.math.NumberUtils.toDouble; - -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Objects; -import java.util.Set; -import java.util.TreeSet; - -import org.apache.commons.lang3.StringUtils; - -import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.lib.filter.DatetimeFilter; -import com.hubspot.jinjava.lib.filter.Filter; -import com.hubspot.jinjava.lib.fn.Functions; - -/** - * Liquid::Template.register_filter JsonFilter - * Liquid::Template.register_filter MoneyFilter - * Liquid::Template.register_filter WeightFilter - * Liquid::Template.register_filter ShopFilter - * Liquid::Template.register_filter TagFilter - * - * @author jstehler - * - */ -public class Filters { - - /** - * override date filter to match liquid functionality - */ - public static class OverrideDateFilter extends DatetimeFilter { - @Override - public Object filter(Object object, JinjavaInterpreter interpreter, String... arg) { - return Functions.dateTimeFormat(ZonedDateTime.now(), arg); - } - } - - public static class JsonFilter implements Filter { - @Override - public String getName() { - return "json"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - return null; - } - } - - public static class MoneyFilter implements Filter { - @Override - public String getName() { - return "money"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - if(var == null) { - return ""; - } - - double money = toDouble(Objects.toString(var)); - return String.format("$ %.2f", money / 100.0); - } - } - - public static class MoneyWithCurrencyFilter extends MoneyFilter { - @Override - public String getName() { - return "money_with_currency"; - } - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - Object val = super.filter(var, interpreter, args); - if(val.toString().length() == 0) { - return ""; - } - return val + " USD"; - } - } - - public static class WeightFilter implements Filter { - @Override - public String getName() { - return "weight"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - double grams = toDouble(Objects.toString(var)); - return String.format("%.2f", grams / 1000); - } - } - - public static class WeightWithUnitFilter extends WeightFilter { - @Override - public String getName() { - return "weight_with_unit"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - return super.filter(var, interpreter, args) + " kg"; - } - } - - public static class ShopAssetUrlFilter implements Filter { - @Override - public String getName() { - return "asset_url"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - return "/files/1/[shop_id]/[shop_id]/assets/" + var; - } - } - - public static class ShopGlobalAssetUrl implements Filter { - @Override - public String getName() { - return "global_asset_url"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - return "/global/" + var; - } - } - - public static class ShopShopifyAssetUrl implements Filter { - @Override - public String getName() { - return "global_asset_url"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - return "/shopify/" + var; - } - } - - public static class ShopScriptTag implements Filter { - @Override - public String getName() { - return "script_tag"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - return String.format("", var); - } - } - - public static class ShopStylesheetTag implements Filter { - @Override - public String getName() { - return "stylesheet_tag"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - String media = "all"; - if(args.length > 0) { - media = args[0]; - } - return String.format("", var, media); - } - } - - public static class ShopLinkTo implements Filter { - @Override - public String getName() { - return "link_to"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - String url = args[0]; - String title = ""; - if(args.length > 1) { - title = args[1]; - } - return String.format("%s", url, title, var); - } - } - - public static class ShopImgTagFilter implements Filter { - @Override - public String getName() { - return "img_tag"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - String alt = ""; - if(args.length > 0) { - alt = args[0]; - } - - return String.format("\"%s\"", var, alt); - } - } - - public static class LinkToTagFilter implements Filter { - @Override - public String getName() { - return "link_to_tag"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - String label = Objects.toString(var); - String tag = args[0]; - - return String.format("%s", tag, interpreter.getContext().get("handle"), tag, label); - } - } - - public static class HighlightActiveTagFilter implements Filter { - @Override - public String getName() { - return "highlight_active_tag"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - String tag = Objects.toString(var); - String cssClass = "active"; - if(args.length > 0) { - cssClass = args[0]; - } - - Collection currentTags = getCurrentTags(interpreter); - if(currentTags.contains(tag)) { - return String.format("%s", cssClass, tag); - } - else { - return tag; - } - } - } - - public static class LinkToAddTagFilter implements Filter { - @Override - public String getName() { - return "link_to_add_tag"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - String label = Objects.toString(var); - String tag = args[0]; - - Set tags = new TreeSet(getCurrentTags(interpreter)); - tags.add(tag); - - return String.format("%s", - tag, interpreter.getContext().get("handle"), StringUtils.join(tags, '+'), label); - } - } - - public static class LinkToRemoveTagFilter implements Filter { - @Override - public String getName() { - return "link_to_remove_tag"; - } - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - String label = Objects.toString(var); - String tag = args[0]; - - Set tags = new TreeSet(getCurrentTags(interpreter)); - tags.remove(tag); - - return String.format("%s", - tag, interpreter.getContext().get("handle"), StringUtils.join(tags, '+'), label); - } - } - - @SuppressWarnings("unchecked") - private static Collection getCurrentTags(JinjavaInterpreter interpreter) { - Collection currentTags = (Collection) interpreter.getContext().get("current_tags", new ArrayList()); - return currentTags; - } - -} diff --git a/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/liquid/LiquidBenchmark.java b/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/liquid/LiquidBenchmark.java deleted file mode 100644 index 825fef535..000000000 --- a/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/liquid/LiquidBenchmark.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.hubspot.jinjava.benchmarks.liquid; - -import static org.apache.commons.io.FileUtils.listFiles; -import static org.apache.commons.io.FileUtils.readFileToString; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.infra.Blackhole; -import org.slf4j.LoggerFactory; -import org.yaml.snakeyaml.Yaml; - -import com.hubspot.jinjava.Jinjava; -import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.tree.Node; - -import ch.qos.logback.classic.Level; - -@State(Scope.Benchmark) -public class LiquidBenchmark { - - public List templates; - public Map bindings; - - public Jinjava jinjava; - - @SuppressWarnings("unchecked") - @Setup - public void setup() throws IOException { - ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME); - logger.setLevel(Level.WARN); - - jinjava = new Jinjava(); - - jinjava.getGlobalContext().registerClasses( - Filters.OverrideDateFilter.class, - - Filters.JsonFilter.class, - Filters.LinkToAddTagFilter.class, - Filters.LinkToRemoveTagFilter.class, - Filters.LinkToTagFilter.class, - Filters.HighlightActiveTagFilter.class, - Filters.MoneyFilter.class, - Filters.MoneyWithCurrencyFilter.class, - Filters.ShopAssetUrlFilter.class, - Filters.ShopGlobalAssetUrl.class, - Filters.ShopImgTagFilter.class, - Filters.ShopLinkTo.class, - Filters.ShopScriptTag.class, - Filters.ShopShopifyAssetUrl.class, - Filters.ShopStylesheetTag.class, - Filters.WeightFilter.class, - Filters.WeightWithUnitFilter.class, - - Tags.AssignTag.class, - Tags.CommentFormTag.class, - Tags.PaginateTag.class, - Tags.TableRowTag.class); - - templates = new ArrayList<>(); - - Map db = (Map) new Yaml().load(readFileToString(new File("liquid/performance/shopify/vision.database.yml"), StandardCharsets.UTF_8)); - bindings = new HashMap<>(initDb(db)); - - File baseDir = new File("liquid/performance/tests"); - for (File tmpl : listFiles(baseDir, new String[] { "liquid" }, true)) { - - String template = readFileToString(tmpl, StandardCharsets.UTF_8); - // convert filter syntax from ':' to '()' - template = template.replaceAll("\\| ([\\w_]+): (.*?)(\\||})", "| $1($2)$3"); - // jinjava doesn't have the '?' postfix binary operator - template = template.replaceAll("if (.*?)\\?", "if $1"); - // no support for offset:n - template = template.replaceAll("offset:\\s*\\d*", ""); - // no support for limit:n - template = template.replaceAll("limit:\\s*\\d*", ""); - // no support for cols:n - template = template.replaceAll("cols:\\s*\\d*", ""); - // no support for for reversal - template = template.replaceAll(" reversed", ""); - - // System.out.println("Adding template: " + tmpl.getAbsolutePath()); - // System.out.println(template); - - templates.add(template); - } - } - - @SuppressWarnings("unchecked") - private Map initDb(Map db) { - for (Map.Entry entry : db.entrySet()) { - if (entry.getValue() instanceof List) { - List values = (List) entry.getValue(); - - if (values.size() > 0 && values.get(0) instanceof Map) { - Map byHandle = new HashMap<>(); - - for (Map val : (List>) values) { - String handle = val.getOrDefault("handle", "").toString(); - - if (handle.trim().length() > 0) { - byHandle.put(handle, val); - } - } - - if (byHandle.size() > 0) { - db.put(entry.getKey(), byHandle); - } - } - } - } - - return db; - } - - @Benchmark - public void parse(Blackhole blackhole) { - JinjavaInterpreter interpreter = jinjava.newInterpreter(); - - for (String template : templates) { - Node parsed = interpreter.parse(template); - if (blackhole != null) { - blackhole.consume(parsed); - } - } - } - - @Benchmark - public void parseAndRender(Blackhole blackhole) { - for (String template : templates) { - String result = jinjava.render(template, bindings); - if (blackhole != null) { - blackhole.consume(result); - } - } - } - - public static void main(String[] args) throws Exception { - LiquidBenchmark b = new LiquidBenchmark(); - b.setup(); - b.parse(null); - b.parseAndRender(null); - } - -} diff --git a/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/liquid/Tags.java b/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/liquid/Tags.java deleted file mode 100644 index 088c455f2..000000000 --- a/benchmark/src/main/java/com/hubspot/jinjava/benchmarks/liquid/Tags.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.hubspot.jinjava.benchmarks.liquid; - -import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.lib.tag.SetTag; -import com.hubspot.jinjava.lib.tag.Tag; -import com.hubspot.jinjava.tree.TagNode; - -/** - * Liquid::Template.register_tag 'paginate', Paginate - * Liquid::Template.register_tag 'form', CommentForm - * - * @author jstehler - * - */ -public class Tags { - - public static class PaginateTag implements Tag { - private static final long serialVersionUID = -4143036883302838710L; - - @Override - public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { - return null; - } - - @Override - public String getName() { - return "paginate"; - } - - @Override - public String getEndTagName() { - return "endpaginate"; - } - } - - public static class CommentFormTag implements Tag { - private static final long serialVersionUID = 4740110980519195813L; - - @Override - public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { - - return null; - } - - @Override - public String getName() { - return "form"; - } - - @Override - public String getEndTagName() { - return "endform"; - } - } - - public static class AssignTag extends SetTag { - private static final long serialVersionUID = -8045822376271136191L; - - @Override - public String getName() { - return "assign"; - } - } - - public static class TableRowTag implements Tag { - private static final long serialVersionUID = 7058892410901688159L; - - @Override - public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { - return ""; - } - - @Override - public String getName() { - return "tablerow"; - } - - @Override - public String getEndTagName() { - return "endtablerow"; - } - } - -} From 41a4d02932f34984485c9dde637287be7e81a470 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 3 Mar 2023 10:03:43 -0500 Subject: [PATCH 189/637] Update changelog for new release 2.7.0 --- CHANGES.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index e9b5bc5ac..289f54a05 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,32 @@ # Jinjava Releases # +### 2023-03-03 Version 2.7.0 ([Maven Central](https://search.maven.org/artifact/com.hubspot.jinjava/jinjava/2.7.0/jar)) ### +* [Use number operations for multiply and divide filters](https://github.com/HubSpot/jinjava/pull/766) +* [Add config to require whitespace in tokens](https://github.com/HubSpot/jinjava/pull/773) +* [Make reject filter the inverse of select filter](https://github.com/HubSpot/jinjava/pull/790) +* [Make ObjectMapper configurable via JinjavaConfig](https://github.com/HubSpot/jinjava/pull/815) +* [Limit rendering cycle detection to expression nodes](https://github.com/HubSpot/jinjava/pull/817) +* [Add URL decode filter](https://github.com/HubSpot/jinjava/pull/840) +* [Fix truthiness of numbers between 0 and 1](https://github.com/HubSpot/jinjava/pull/857) +* [Fix macro function scoping inside of another macro function](https://github.com/HubSpot/jinjava/pull/869) +* [Handle thread interrupts by throwing an InterpretException](https://github.com/HubSpot/jinjava/pull/870) +* [Fix right-side inline whitespace trimming](https://github.com/HubSpot/jinjava/pull/885) +* [Fix Jinjava functionality for duplicate macro functions and call tags](https://github.com/HubSpot/jinjava/pull/889) +* [Fix custom operator precedence](https://github.com/HubSpot/jinjava/pull/902) +* [Parse leading negatives in expression nodes](https://github.com/HubSpot/jinjava/pull/896) +* [add keys function to dictionary](https://github.com/HubSpot/jinjava/pull/936) +* [Update title filter to ignore special characters](https://github.com/HubSpot/jinjava/pull/945) +* [add unescape_html filter](https://github.com/HubSpot/jinjava/pull/967) +* [Move object unwrap behavior to config object](https://github.com/HubSpot/jinjava/pull/983) +* [Get best invoke method based on parameters](https://github.com/HubSpot/jinjava/pull/996) +* [Create format_number filter](https://github.com/HubSpot/jinjava/pull/999) +* [Get current date and time from a provider](https://github.com/HubSpot/jinjava/pull/1007) +* [Create context method for checking if in for loop](https://github.com/HubSpot/jinjava/pull/1015) +* [Filter duplicate template errors](https://github.com/HubSpot/jinjava/pull/1016) +* Fix various NullPointerExceptions in filters and functions +* Various changes to reduce non-deterministic behavior +* Various changes to improve datetime formatting and exception handling +* Various PRs for eager execution to support two-phase rendering. + ### 2021-10-29 Version 2.6.0 ([Maven Central](https://search.maven.org/artifact/com.hubspot.jinjava/jinjava/2.6.0/jar)) ### * [Create interface for object truth values](https://github.com/HubSpot/jinjava/pull/747) * [Catch concurrent modification in for loop](https://github.com/HubSpot/jinjava/pull/750) From b7b92970ff8d200efebdb1cef7bec7cbe74e9476 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 3 Mar 2023 10:04:03 -0500 Subject: [PATCH 190/637] Change snapshot to 2.7.0 As there've been nearly 1.5 years of developmental work without any maintenance releases and there are several new features --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e6e1e6d4b..4328ba874 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.hubspot.jinjava jinjava - 2.6.1-SNAPSHOT + 2.7.0-SNAPSHOT Jinja templating engine implemented in Java From 1239b20421054e6e26aef3ed76bde42af8d09a99 Mon Sep 17 00:00:00 2001 From: egyedt <81372484+egyedt@users.noreply.github.com> Date: Mon, 6 Mar 2023 16:57:50 +0100 Subject: [PATCH 191/637] Update list filter to handle arrays properly (#1014) #1013 --- .../jinjava/lib/filter/ListFilter.java | 30 +++++ .../jinjava/lib/filter/ListFilterTest.java | 111 ++++++++++++++++++ 2 files changed, 141 insertions(+) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/ListFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/ListFilter.java index ba3af0497..950c74058 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/ListFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/ListFilter.java @@ -6,8 +6,10 @@ import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import java.util.Arrays; import java.util.Collection; import java.util.List; +import org.apache.commons.lang3.ArrayUtils; @JinjavaDoc( value = "Convert the value into a list. If it was a string the returned list will be a list of characters.", @@ -45,6 +47,34 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) result = Chars.asList(((String) var).toCharArray()); } else if (Collection.class.isAssignableFrom(var.getClass())) { result = Lists.newArrayList((Collection) var); + } else if (var.getClass().isArray()) { + if (var instanceof boolean[]) { + Boolean[] outputBoxed = ArrayUtils.toObject((boolean[]) var); + result = Arrays.asList(outputBoxed); + } else if (var instanceof byte[]) { + Byte[] outputBoxed = ArrayUtils.toObject((byte[]) var); + result = Arrays.asList(outputBoxed); + } else if (var instanceof char[]) { + Character[] outputBoxed = ArrayUtils.toObject((char[]) var); + result = Arrays.asList(outputBoxed); + } else if (var instanceof short[]) { + Short[] outputBoxed = ArrayUtils.toObject((short[]) var); + result = Arrays.asList(outputBoxed); + } else if (var instanceof int[]) { + Integer[] outputBoxed = ArrayUtils.toObject((int[]) var); + result = Arrays.asList(outputBoxed); + } else if (var instanceof long[]) { + Long[] outputBoxed = ArrayUtils.toObject((long[]) var); + result = Arrays.asList(outputBoxed); + } else if (var instanceof float[]) { + Float[] outputBoxed = ArrayUtils.toObject((float[]) var); + result = Arrays.asList(outputBoxed); + } else if (var instanceof double[]) { + Double[] outputBoxed = ArrayUtils.toObject((double[]) var); + result = Arrays.asList(outputBoxed); + } else { + result = Arrays.asList((Object[]) var); + } } else { result = Lists.newArrayList(var); } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/ListFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/ListFilterTest.java index 5cf191f5f..691638fdf 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/ListFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/ListFilterTest.java @@ -16,6 +16,10 @@ package com.hubspot.jinjava.lib.filter; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import com.google.common.collect.Lists; import com.google.common.collect.Sets; @@ -60,4 +64,111 @@ public void itHandlesNullListParams() { List o = (List) filter.filter(null, null); assertThat(o).isNull(); } + + @Test + public void itHandlesBoolean() { + boolean[] array = {true, false, true}; + + Object result = filter.filter(array, null); + + doAssertions(result, Boolean.class, array[0], array[1], array[2]); + } + + @Test + public void itHandlesByte() { + byte[] array = {1, 2, 3}; + + Object result = filter.filter(array, null); + + doAssertions(result, Byte.class, array[0], array[1], array[2]); + } + + @Test + public void itHandlesChar() { + char[] array = {'a', 'b', 'c'}; + + Object result = filter.filter(array, null); + + doAssertions(result, Character.class, array[0], array[1], array[2]); + } + + @Test + public void itHandlesShort() { + short[] array = {1, 2, 3}; + + Object result = filter.filter(array, null); + + doAssertions(result, Short.class, array[0], array[1], array[2]); + } + + @Test + public void itHandlesInt() { + int[] array = {1, 2, 3}; + + Object result = filter.filter(array, null); + + doAssertions(result, Integer.class, array[0], array[1], array[2]); + } + + @Test + public void itHandlesLong() { + long[] array = {1L, 2L, 3L}; + + Object result = filter.filter(array, null); + + doAssertions(result, Long.class, array[0], array[1], array[2]); + } + + @Test + public void itHandlesFloat() { + float[] array = {1, 2, 3}; + + Object result = filter.filter(array, null); + + doAssertions(result, Float.class, array[0], array[1], array[2]); + } + + @Test + public void itHandlesDouble() { + double[] array = {1.0, 2.0, 3.0}; + + Object result = filter.filter(array, null); + + doAssertions(result, Double.class, array[0], array[1], array[2]); + } + + @Test + public void itHandlesString() { + String[] array = {"word", "word2", "word3"}; + + Object result = filter.filter(array, null); + + doAssertions(result, String.class, array[0], array[1], array[2]); + } + + @Test + public void itHandlesInputNull() { + Object result = filter.filter(null, null); + + assertNull(result); + } + + @Test + public void itHandlesGetName() { + String name = filter.getName(); + + assertEquals("list", name); + } + + private void doAssertions(Object result, Class classOfElements, Object... elements) { + assertNotNull(result); + assertTrue(result instanceof List); + List resultList = (List) result; + assertEquals(elements.length, resultList.size()); + for (int i = 0; i < elements.length; i++) { + assertEquals(elements[i], resultList.get(i)); + assertEquals(classOfElements, resultList.get(i).getClass()); + } + } + } From 413f9f218663eb93746c058bd47b1b1145678728 Mon Sep 17 00:00:00 2001 From: Tyler Kindy Date: Mon, 6 Mar 2023 10:59:14 -0500 Subject: [PATCH 192/637] Format --- .../jinjava/lib/filter/ListFilterTest.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/ListFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/ListFilterTest.java index 691638fdf..451adc584 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/ListFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/ListFilterTest.java @@ -67,7 +67,7 @@ public void itHandlesNullListParams() { @Test public void itHandlesBoolean() { - boolean[] array = {true, false, true}; + boolean[] array = { true, false, true }; Object result = filter.filter(array, null); @@ -76,7 +76,7 @@ public void itHandlesBoolean() { @Test public void itHandlesByte() { - byte[] array = {1, 2, 3}; + byte[] array = { 1, 2, 3 }; Object result = filter.filter(array, null); @@ -85,7 +85,7 @@ public void itHandlesByte() { @Test public void itHandlesChar() { - char[] array = {'a', 'b', 'c'}; + char[] array = { 'a', 'b', 'c' }; Object result = filter.filter(array, null); @@ -94,7 +94,7 @@ public void itHandlesChar() { @Test public void itHandlesShort() { - short[] array = {1, 2, 3}; + short[] array = { 1, 2, 3 }; Object result = filter.filter(array, null); @@ -103,7 +103,7 @@ public void itHandlesShort() { @Test public void itHandlesInt() { - int[] array = {1, 2, 3}; + int[] array = { 1, 2, 3 }; Object result = filter.filter(array, null); @@ -112,7 +112,7 @@ public void itHandlesInt() { @Test public void itHandlesLong() { - long[] array = {1L, 2L, 3L}; + long[] array = { 1L, 2L, 3L }; Object result = filter.filter(array, null); @@ -121,7 +121,7 @@ public void itHandlesLong() { @Test public void itHandlesFloat() { - float[] array = {1, 2, 3}; + float[] array = { 1, 2, 3 }; Object result = filter.filter(array, null); @@ -130,7 +130,7 @@ public void itHandlesFloat() { @Test public void itHandlesDouble() { - double[] array = {1.0, 2.0, 3.0}; + double[] array = { 1.0, 2.0, 3.0 }; Object result = filter.filter(array, null); @@ -139,7 +139,7 @@ public void itHandlesDouble() { @Test public void itHandlesString() { - String[] array = {"word", "word2", "word3"}; + String[] array = { "word", "word2", "word3" }; Object result = filter.filter(array, null); @@ -170,5 +170,4 @@ private void doAssertions(Object result, Class classOfElements, Object... elemen assertEquals(classOfElements, resultList.get(i).getClass()); } } - } From 225148ced0d6d45bc7e5b7441e5b9ee0f6fdc098 Mon Sep 17 00:00:00 2001 From: Jonathan Haber Date: Mon, 6 Mar 2023 12:09:34 -0500 Subject: [PATCH 193/637] Disable sortpom during release process --- pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pom.xml b/pom.xml index 4328ba874..787bcdd90 100644 --- a/pom.xml +++ b/pom.xml @@ -280,4 +280,12 @@ git@github.com:HubSpot/jinjava.git HEAD + + + + basepom.oss-release + + true + + From c83025a12bdcc99282149712e8b0600b7be5e609 Mon Sep 17 00:00:00 2001 From: Jonathan Haber Date: Mon, 6 Mar 2023 12:11:09 -0500 Subject: [PATCH 194/637] derp --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 787bcdd90..38e114cc4 100644 --- a/pom.xml +++ b/pom.xml @@ -287,5 +287,6 @@ true + From 682d30f019b1565764d56e8d9c2badf4518f4ab2 Mon Sep 17 00:00:00 2001 From: Matthew Coley Date: Mon, 6 Mar 2023 13:43:42 -0500 Subject: [PATCH 195/637] Filter out null errors --- .../com/hubspot/jinjava/interpret/JinjavaInterpreter.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index 91dcfc453..ff7a3fc69 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -710,6 +710,10 @@ public BlockInfo getCurrentBlock() { } public void addError(TemplateError templateError) { + if (templateError == null) { + return; + } + if (context.getThrowInterpreterErrors()) { if (templateError.getSeverity() == ErrorType.FATAL) { // Throw fatal errors when locating deferred words. From d6cef9ab584af264dad9b2cafd20bdd216a5f512 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Mon, 6 Mar 2023 16:57:54 -0500 Subject: [PATCH 196/637] Skip dependency management during release process --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 38e114cc4..dc769f842 100644 --- a/pom.xml +++ b/pom.xml @@ -286,6 +286,7 @@ basepom.oss-release true + true From f54540f64f27eeacc69e739674665d36923ef7fa Mon Sep 17 00:00:00 2001 From: HubSpot PaaS Team Date: Mon, 6 Mar 2023 22:12:24 +0000 Subject: [PATCH 197/637] [maven-release-plugin] prepare release jinjava-2.7.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index dc769f842..65e15d685 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.hubspot.jinjava jinjava - 2.7.0-SNAPSHOT + 2.7.0 Jinja templating engine implemented in Java @@ -278,7 +278,7 @@ scm:git:git@github.com:HubSpot/jinjava.git scm:git:git@github.com:HubSpot/jinjava.git git@github.com:HubSpot/jinjava.git - HEAD + jinjava-2.7.0 From c631120c3e4f4acbe8c820132cab4752d3979bca Mon Sep 17 00:00:00 2001 From: HubSpot PaaS Team Date: Mon, 6 Mar 2023 22:12:24 +0000 Subject: [PATCH 198/637] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 65e15d685..ce8b483da 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.hubspot.jinjava jinjava - 2.7.0 + 2.7.1-SNAPSHOT Jinja templating engine implemented in Java @@ -278,7 +278,7 @@ scm:git:git@github.com:HubSpot/jinjava.git scm:git:git@github.com:HubSpot/jinjava.git git@github.com:HubSpot/jinjava.git - jinjava-2.7.0 + HEAD From 29a7c0996904f60f9c032e3ac888eb3e4c265b97 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 10 Mar 2023 09:44:39 -0500 Subject: [PATCH 199/637] In EscapeJinjavaFilter, escape both double closing braces to not have a mismatch --- .../com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java | 1 + .../com/hubspot/jinjava/lib/filter/EscapeJinjavaFilterTest.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java index 21d2f0498..45dc8f0a9 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java @@ -58,6 +58,7 @@ public static String escapeJinjavaEntities(String input) { public static String escapeFullJinjavaEntities(String input) { return input .replace("{{", BLBRACE + BLBRACE) + .replace("}}", BRBRACE + BRBRACE) .replaceAll("\\{([{%#])", BLBRACE + "$1") .replaceAll("([}%#])}", "$1" + BRBRACE); } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilterTest.java index 8f01bf2ae..0fdd75835 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilterTest.java @@ -37,7 +37,7 @@ public void testDoesNotEscapeJson() { f.filter("{'foo': 'bar', '{{{ foo }}}': '{% bar %}'}", interpreter, "false") ) .isEqualTo( - "{'foo': 'bar', '{{{ foo }}}': '{% bar %}'}" + "{'foo': 'bar', '{{{ foo }}}': '{% bar %}'}" ); } } From 7ebb6d8e978a5082314eb8415707af2f10697844 Mon Sep 17 00:00:00 2001 From: Brian Krainer Date: Mon, 13 Mar 2023 09:52:40 -0300 Subject: [PATCH 200/637] Add protected getReadMethod --- .../java/com/hubspot/jinjava/el/ext/BeanELResolver.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java b/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java index 90c9b60d6..c9cd2157f 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java @@ -337,7 +337,7 @@ public Object getValue(ELContext context, Object base, Object property) { } Object result = null; if (isResolvable(base)) { - Method method = toBeanProperty(base, property).getReadMethod(); + Method method = getReadMethod(base, property); if (method == null) { throw new PropertyNotFoundException("Cannot read property " + property); } @@ -649,6 +649,10 @@ protected Object[] coerceParams( return args; } + protected Method getReadMethod(Object base, Object property) { + return toBeanProperty(base, property).getReadMethod(); + } + private void coerceValue( Object array, int index, From 772de7fcecd74da4cefe9c08c37bf82339953362 Mon Sep 17 00:00:00 2001 From: Jeff Boulter Date: Mon, 13 Mar 2023 17:04:17 -0400 Subject: [PATCH 201/637] check iterator before calling next --- .../el/TypeConvertingMapELResolver.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/el/TypeConvertingMapELResolver.java b/src/main/java/com/hubspot/jinjava/el/TypeConvertingMapELResolver.java index 7d90543e3..55431cd82 100644 --- a/src/main/java/com/hubspot/jinjava/el/TypeConvertingMapELResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/TypeConvertingMapELResolver.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.el; +import java.util.Iterator; import java.util.Map; import javax.el.ELContext; import javax.el.ELException; @@ -21,14 +22,17 @@ public Object getValue(ELContext context, Object base, Object property) { } if (base instanceof Map && !((Map) base).isEmpty()) { - Class keyClass = ((Map) base).keySet().iterator().next().getClass(); - try { - value = ((Map) base).get(TYPE_CONVERTER.convert(property, keyClass)); - if (value != null) { - context.setPropertyResolved(true); + Iterator iterator = ((Map) base).keySet().iterator(); + if (iterator.hasNext()) { + Class keyClass = iterator.next().getClass(); + try { + value = ((Map) base).get(TYPE_CONVERTER.convert(property, keyClass)); + if (value != null) { + context.setPropertyResolved(true); + } + } catch (ELException ex) { + value = null; } - } catch (ELException ex) { - value = null; } } From 4134c007a035f68744ef54b8d05ac2f8fe965967 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 15 Mar 2023 11:14:58 -0400 Subject: [PATCH 202/637] Add filter which converts a map with camelCase attributes into one that also accepts snake_case keys --- .../lib/filter/AllowSnakeCaseFilter.java | 43 +++++++++++++++++++ .../jinjava/lib/filter/FilterLibrary.java | 1 + .../collections/SnakeCaseAccessibleMap.java | 43 +++++++++++++++++++ .../lib/filter/AllowSnakeCaseFilterTest.java | 28 ++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 src/main/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilter.java create mode 100644 src/main/java/com/hubspot/jinjava/objects/collections/SnakeCaseAccessibleMap.java create mode 100644 src/test/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilterTest.java diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilter.java new file mode 100644 index 000000000..3b9868968 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilter.java @@ -0,0 +1,43 @@ +package com.hubspot.jinjava.lib.filter; + +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; +import com.hubspot.jinjava.doc.annotations.JinjavaParam; +import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.objects.collections.PyMap; +import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; +import com.hubspot.jinjava.objects.collections.SnakeCaseAccessibleMap; +import java.util.Map; + +@JinjavaDoc( + value = "Allow keys on the provided camelCase map to be accessed using snake_case", + input = @JinjavaParam( + value = "map", + type = "dict", + desc = "The dict to make keys accessible using snake_case", + required = true + ), + snippets = { @JinjavaSnippet(code = "{{ {'fooBar': 'baz'}|allow_snake_case }}") } +) +public class AllowSnakeCaseFilter implements Filter { + public static final String NAME = "allow_snake_case"; + + @Override + public String getName() { + return NAME; + } + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + if (!(var instanceof Map)) { + return var; + } + Map map = (Map) var; + if (map instanceof PyMap) { + map = ((PyMap) map).toMap(); + } + return new SnakeCaseAccessibleMap( + new SizeLimitingPyMap(map, interpreter.getConfig().getMaxMapSize()) + ); + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java index 216a1d2bd..f5dfc30e3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java @@ -32,6 +32,7 @@ protected void registerDefaults() { registerClasses( AbsFilter.class, AddFilter.class, + AllowSnakeCaseFilter.class, AttrFilter.class, Base64DecodeFilter.class, Base64EncodeFilter.class, diff --git a/src/main/java/com/hubspot/jinjava/objects/collections/SnakeCaseAccessibleMap.java b/src/main/java/com/hubspot/jinjava/objects/collections/SnakeCaseAccessibleMap.java new file mode 100644 index 000000000..1c4b68c90 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/collections/SnakeCaseAccessibleMap.java @@ -0,0 +1,43 @@ +package com.hubspot.jinjava.objects.collections; + +import com.google.common.base.CaseFormat; +import com.hubspot.jinjava.lib.filter.AllowSnakeCaseFilter; +import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; +import java.util.Map; + +public class SnakeCaseAccessibleMap extends PyMap implements PyishSerializable { + + public SnakeCaseAccessibleMap(Map map) { + super(map); + } + + @Override + public Object get(Object key) { + Object result = super.get(key); + if (result == null && key instanceof String) { + return getWithCamelCase((String) key); + } + return result; + } + + private Object getWithCamelCase(String key) { + if (key == null) { + return null; + } + if (key.indexOf('_') == -1) { + return null; + } + return super.get(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, key)); + } + + @SuppressWarnings("unchecked") + @Override + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable + .append(PyishSerializable.writeValueAsString(toMap())) + .append('|') + .append(AllowSnakeCaseFilter.NAME); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilterTest.java new file mode 100644 index 000000000..5db6305b5 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilterTest.java @@ -0,0 +1,28 @@ +package com.hubspot.jinjava.lib.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; +import org.junit.Test; + +public class AllowSnakeCaseFilterTest extends BaseInterpretingTest { + + @Test + public void itDoesNotChangeNonMaps() { + assertThat(interpreter.render("{{ 'fooBar'|allow_snake_case }}")).isEqualTo("fooBar"); + } + + @Test + public void itMakesMapKeysAccessibleWithSnakeCase() { + assertThat(interpreter.render("{{ ({'fooBar': 'foo'}|allow_snake_case).foo_bar }}")) + .isEqualTo("foo"); + } + + @Test + public void itReserializesAsSnakeCaseAccessibleMap() { + interpreter.render("{% set map = {'fooBar': 'foo'}|allow_snake_case %}"); + assertThat(PyishObjectMapper.getAsPyishString(interpreter.getContext().get("map"))) + .isEqualTo("{'fooBar': 'foo'} |allow_snake_case"); + } +} From 0f535bae37ae7446d826ca6997e03dfde104aebd Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 15 Mar 2023 11:15:22 -0400 Subject: [PATCH 203/637] When executing in eager execution, reconstruct beans to be accessible with snake case --- .../BothCasingBeanSerializer.java | 24 +++++++++++++ .../PyishBeanSerializerModifier.java | 9 +++++ .../serialization/PyishObjectMapper.java | 11 +++++- .../serialization/PyishObjectMapperTest.java | 36 +++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java new file mode 100644 index 000000000..2552f296f --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java @@ -0,0 +1,24 @@ +package com.hubspot.jinjava.objects.serialization; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.hubspot.jinjava.lib.filter.AllowSnakeCaseFilter; +import java.io.IOException; + +public class BothCasingBeanSerializer extends JsonSerializer { + public static final BothCasingBeanSerializer INSTANCE = new BothCasingBeanSerializer(); + + private BothCasingBeanSerializer() {} + + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + StringBuilder sb = new StringBuilder(); + sb + .append(PyishSerializable.writeValueAsString(value)) + .append('|') + .append(AllowSnakeCaseFilter.NAME); + gen.writeRawValue(sb.toString()); + } +} diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java index a55f0bf36..600173439 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.ser.BeanSerializer; import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; import java.util.Map; @@ -23,6 +24,14 @@ public JsonSerializer modifySerializer( if (Map.Entry.class.isAssignableFrom(beanDesc.getBeanClass())) { return MapEntrySerializer.INSTANCE; } + if ( + serializer instanceof BeanSerializer && + Boolean.TRUE.equals( + config.getAttributes().getAttribute(PyishObjectMapper.EAGER_EXECUTION_ATTRIBUTE) + ) + ) { + return BothCasingBeanSerializer.INSTANCE; + } return serializer; } else { return PyishSerializer.INSTANCE; diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 14da54606..055168903 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -19,6 +19,7 @@ public class PyishObjectMapper { public static final ObjectWriter PYISH_OBJECT_WRITER; + public static final String EAGER_EXECUTION_ATTRIBUTE = "eagerExecution"; static { ObjectMapper mapper = new ObjectMapper( @@ -75,7 +76,15 @@ public static String getAsPyishStringOrThrow(Object val) throws IOException { } else { writer = new CharArrayWriter(); } - objectWriter.writeValue(writer, val); + objectWriter + .withAttribute( + EAGER_EXECUTION_ATTRIBUTE, + JinjavaInterpreter + .getCurrentMaybe() + .map(interpreter -> interpreter.getConfig().getExecutionMode().useEagerParser()) + .orElse(false) + ) + .writeValue(writer, val); return writer.toString(); } diff --git a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java index cadcb8be9..b5e1cd604 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -8,6 +8,7 @@ import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; import java.util.ArrayList; import java.util.HashMap; @@ -88,4 +89,39 @@ public void itLimitsDepth() { JinjavaInterpreter.popCurrent(); } } + + @Test + public void itSerializesToSnakeCaseAccessibleMap() { + try { + Jinjava jinjava = new Jinjava( + JinjavaConfig + .newBuilder() + .withExecutionMode(EagerExecutionMode.instance()) + .build() + ); + JinjavaInterpreter.pushCurrent(jinjava.newInterpreter()); + assertThat(PyishObjectMapper.getAsPyishString(new Foo("bar"))) + .isEqualTo("{'fooBar': 'bar'} |allow_snake_case"); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + + @Test + public void itDoesNotConvertToSnakeCaseMapInDefaultExecutionMode() { + assertThat(PyishObjectMapper.getAsPyishString(new Foo("bar")).trim()) + .isEqualTo("{'fooBar': 'bar'}"); + } + + static class Foo { + private final String bar; + + public Foo(String bar) { + this.bar = bar; + } + + public String getFooBar() { + return bar; + } + } } From dfde72a5626aad275996e277ee688327833f2f6f Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 16 Mar 2023 13:42:48 -0400 Subject: [PATCH 204/637] Change where eager execution attribute is checked to make use of caching serializers --- .../BothCasingBeanSerializer.java | 40 ++++++++++++++----- .../PyishBeanSerializerModifier.java | 9 +---- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java index 2552f296f..d25119d30 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java @@ -6,19 +6,39 @@ import com.hubspot.jinjava.lib.filter.AllowSnakeCaseFilter; import java.io.IOException; -public class BothCasingBeanSerializer extends JsonSerializer { - public static final BothCasingBeanSerializer INSTANCE = new BothCasingBeanSerializer(); +public class BothCasingBeanSerializer extends JsonSerializer { + private final JsonSerializer orignalSerializer; - private BothCasingBeanSerializer() {} + private BothCasingBeanSerializer(JsonSerializer jsonSerializer) { + this.orignalSerializer = jsonSerializer; + } + + public static BothCasingBeanSerializer wrapping( + JsonSerializer jsonSerializer + ) { + return new BothCasingBeanSerializer<>(jsonSerializer); + } @Override - public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) + public void serialize( + T value, + JsonGenerator gen, + SerializerProvider serializerProvider + ) throws IOException { - StringBuilder sb = new StringBuilder(); - sb - .append(PyishSerializable.writeValueAsString(value)) - .append('|') - .append(AllowSnakeCaseFilter.NAME); - gen.writeRawValue(sb.toString()); + if ( + Boolean.TRUE.equals( + serializerProvider.getAttribute(PyishObjectMapper.EAGER_EXECUTION_ATTRIBUTE) + ) + ) { + StringBuilder sb = new StringBuilder(); + sb + .append(PyishSerializable.writeValueAsString(value)) + .append('|') + .append(AllowSnakeCaseFilter.NAME); + gen.writeRawValue(sb.toString()); + } else { + orignalSerializer.serialize(value, gen, serializerProvider); + } } } diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java index 600173439..a3d84cdb1 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java @@ -24,13 +24,8 @@ public JsonSerializer modifySerializer( if (Map.Entry.class.isAssignableFrom(beanDesc.getBeanClass())) { return MapEntrySerializer.INSTANCE; } - if ( - serializer instanceof BeanSerializer && - Boolean.TRUE.equals( - config.getAttributes().getAttribute(PyishObjectMapper.EAGER_EXECUTION_ATTRIBUTE) - ) - ) { - return BothCasingBeanSerializer.INSTANCE; + if (serializer instanceof BeanSerializer) { + return BothCasingBeanSerializer.wrapping(serializer); } return serializer; } else { From c9f612817dceab05695786605c09a7264e98eced Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 16 Mar 2023 14:00:35 -0400 Subject: [PATCH 205/637] Change to only use allow_snake_case filter when the serialization isn't directly for output --- .../BothCasingBeanSerializer.java | 4 ++- .../serialization/PyishObjectMapper.java | 23 +++++++------ .../serialization/PyishObjectMapperTest.java | 32 +++++++++---------- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java index d25119d30..f966065be 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java @@ -28,9 +28,11 @@ public void serialize( throws IOException { if ( Boolean.TRUE.equals( - serializerProvider.getAttribute(PyishObjectMapper.EAGER_EXECUTION_ATTRIBUTE) + serializerProvider.getAttribute(PyishObjectMapper.ALLOW_SNAKE_CASE_ATTRIBUTE) ) ) { + // if it's directly for output, then we don't want to add the additional filter characters, + // as doing so would make the "|allow_snake_case" appear in the final output. StringBuilder sb = new StringBuilder(); sb .append(PyishSerializable.writeValueAsString(value)) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 055168903..950842f66 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -19,7 +19,7 @@ public class PyishObjectMapper { public static final ObjectWriter PYISH_OBJECT_WRITER; - public static final String EAGER_EXECUTION_ATTRIBUTE = "eagerExecution"; + public static final String ALLOW_SNAKE_CASE_ATTRIBUTE = "allowSnakeCase"; static { ObjectMapper mapper = new ObjectMapper( @@ -37,14 +37,18 @@ public class PyishObjectMapper { public static String getAsUnquotedPyishString(Object val) { if (val != null) { - return WhitespaceUtils.unquoteAndUnescape(getAsPyishString(val)); + return WhitespaceUtils.unquoteAndUnescape(getAsPyishString(val, true)); } return ""; } public static String getAsPyishString(Object val) { + return getAsPyishString(val, false); + } + + private static String getAsPyishString(Object val, boolean forOutput) { try { - return getAsPyishStringOrThrow(val); + return getAsPyishStringOrThrow(val, forOutput); } catch (IOException e) { if (e instanceof LengthLimitingJsonProcessingException) { throw new OutputTooBigException( @@ -57,6 +61,11 @@ public static String getAsPyishString(Object val) { } public static String getAsPyishStringOrThrow(Object val) throws IOException { + return getAsPyishStringOrThrow(val, false); + } + + public static String getAsPyishStringOrThrow(Object val, boolean forOutput) + throws IOException { ObjectWriter objectWriter = PYISH_OBJECT_WRITER; Writer writer; Optional maxOutputSize = JinjavaInterpreter @@ -77,13 +86,7 @@ public static String getAsPyishStringOrThrow(Object val) throws IOException { writer = new CharArrayWriter(); } objectWriter - .withAttribute( - EAGER_EXECUTION_ATTRIBUTE, - JinjavaInterpreter - .getCurrentMaybe() - .map(interpreter -> interpreter.getConfig().getExecutionMode().useEagerParser()) - .orElse(false) - ) + .withAttribute(ALLOW_SNAKE_CASE_ATTRIBUTE, !forOutput) .writeValue(writer, val); return writer.toString(); } diff --git a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java index b5e1cd604..65b937cad 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -7,8 +7,8 @@ import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; import java.util.ArrayList; import java.util.HashMap; @@ -92,25 +92,23 @@ public void itLimitsDepth() { @Test public void itSerializesToSnakeCaseAccessibleMap() { - try { - Jinjava jinjava = new Jinjava( - JinjavaConfig - .newBuilder() - .withExecutionMode(EagerExecutionMode.instance()) - .build() - ); - JinjavaInterpreter.pushCurrent(jinjava.newInterpreter()); - assertThat(PyishObjectMapper.getAsPyishString(new Foo("bar"))) - .isEqualTo("{'fooBar': 'bar'} |allow_snake_case"); - } finally { - JinjavaInterpreter.popCurrent(); - } + assertThat(PyishObjectMapper.getAsPyishString(new Foo("bar"))) + .isEqualTo("{'fooBar': 'bar'} |allow_snake_case"); } @Test - public void itDoesNotConvertToSnakeCaseMapInDefaultExecutionMode() { - assertThat(PyishObjectMapper.getAsPyishString(new Foo("bar")).trim()) - .isEqualTo("{'fooBar': 'bar'}"); + public void itDoesNotConvertToSnakeCaseMapWhenResultIsForOutput() { + Jinjava jinjava = new Jinjava( + JinjavaConfig + .newBuilder() + .withLegacyOverrides( + LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() + ) + .build() + ); + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + interpreter.getContext().put("foo", new Foo("bar")); + assertThat(interpreter.render("{{ foo }}")).isEqualTo("{'fooBar': 'bar'}"); } static class Foo { From 47fd189b72518d92c42ec715b510e4beb2200412 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 16 Mar 2023 14:29:17 -0400 Subject: [PATCH 206/637] Add legacy override to pyish serialize using snake case by default --- .../com/hubspot/jinjava/LegacyOverrides.java | 13 +++++++ .../serialization/PyishObjectMapper.java | 37 ++++++++++++++++--- .../serialization/PyishObjectMapperTest.java | 24 ++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/LegacyOverrides.java b/src/main/java/com/hubspot/jinjava/LegacyOverrides.java index 9fb46b03d..5ff01314f 100644 --- a/src/main/java/com/hubspot/jinjava/LegacyOverrides.java +++ b/src/main/java/com/hubspot/jinjava/LegacyOverrides.java @@ -9,6 +9,7 @@ public class LegacyOverrides { private final boolean evaluateMapKeys; private final boolean iterateOverMapKeys; private final boolean usePyishObjectMapper; + private final boolean useSnakeCasePropertyNaming; private final boolean whitespaceRequiredWithinTokens; private final boolean useNaturalOperatorPrecedence; private final boolean parseWhitespaceControlStrictly; @@ -17,6 +18,7 @@ private LegacyOverrides(Builder builder) { evaluateMapKeys = builder.evaluateMapKeys; iterateOverMapKeys = builder.iterateOverMapKeys; usePyishObjectMapper = builder.usePyishObjectMapper; + useSnakeCasePropertyNaming = builder.useSnakeCasePropertyNaming; whitespaceRequiredWithinTokens = builder.whitespaceRequiredWithinTokens; useNaturalOperatorPrecedence = builder.useNaturalOperatorPrecedence; parseWhitespaceControlStrictly = builder.parseWhitespaceControlStrictly; @@ -38,6 +40,10 @@ public boolean isUsePyishObjectMapper() { return usePyishObjectMapper; } + public boolean isUseSnakeCasePropertyNaming() { + return useSnakeCasePropertyNaming; + } + public boolean isWhitespaceRequiredWithinTokens() { return whitespaceRequiredWithinTokens; } @@ -54,6 +60,7 @@ public static class Builder { private boolean evaluateMapKeys = false; private boolean iterateOverMapKeys = false; private boolean usePyishObjectMapper = false; + private boolean useSnakeCasePropertyNaming = false; private boolean whitespaceRequiredWithinTokens = false; private boolean useNaturalOperatorPrecedence = false; private boolean parseWhitespaceControlStrictly = false; @@ -69,6 +76,7 @@ public static Builder from(LegacyOverrides legacyOverrides) { .withEvaluateMapKeys(legacyOverrides.evaluateMapKeys) .withIterateOverMapKeys(legacyOverrides.iterateOverMapKeys) .withUsePyishObjectMapper(legacyOverrides.usePyishObjectMapper) + .withUseSnakeCasePropertyNaming(legacyOverrides.useSnakeCasePropertyNaming) .withWhitespaceRequiredWithinTokens( legacyOverrides.whitespaceRequiredWithinTokens ) @@ -93,6 +101,11 @@ public Builder withUsePyishObjectMapper(boolean usePyishObjectMapper) { return this; } + public Builder withUseSnakeCasePropertyNaming(boolean useSnakeCasePropertyNaming) { + this.useSnakeCasePropertyNaming = useSnakeCasePropertyNaming; + return this; + } + public Builder withWhitespaceRequiredWithinTokens( boolean whitespaceRequiredWithinTokens ) { diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 950842f66..c7628aaf6 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; import com.hubspot.jinjava.interpret.JinjavaInterpreter; @@ -19,9 +20,23 @@ public class PyishObjectMapper { public static final ObjectWriter PYISH_OBJECT_WRITER; + public static final ObjectWriter SNAKE_CASE_PYISH_OBJECT_WRITER; public static final String ALLOW_SNAKE_CASE_ATTRIBUTE = "allowSnakeCase"; static { + PYISH_OBJECT_WRITER = + getPyishObjectMapper() + .writer(PyishPrettyPrinter.INSTANCE) + .with(PyishCharacterEscapes.INSTANCE); + + SNAKE_CASE_PYISH_OBJECT_WRITER = + getPyishObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .writer(PyishPrettyPrinter.INSTANCE) + .with(PyishCharacterEscapes.INSTANCE); + } + + private static ObjectMapper getPyishObjectMapper() { ObjectMapper mapper = new ObjectMapper( new JsonFactoryBuilder().quoteChar('\'').build() ) @@ -31,8 +46,7 @@ public class PyishObjectMapper { .addSerializer(PyishSerializable.class, PyishSerializer.INSTANCE) ); mapper.getSerializerProvider().setNullKeySerializer(new NullKeySerializer()); - PYISH_OBJECT_WRITER = - mapper.writer(PyishPrettyPrinter.INSTANCE).with(PyishCharacterEscapes.INSTANCE); + return mapper; } public static String getAsUnquotedPyishString(Object val) { @@ -66,7 +80,16 @@ public static String getAsPyishStringOrThrow(Object val) throws IOException { public static String getAsPyishStringOrThrow(Object val, boolean forOutput) throws IOException { - ObjectWriter objectWriter = PYISH_OBJECT_WRITER; + boolean useSnakeCaseMappingOverride = JinjavaInterpreter + .getCurrentMaybe() + .map( + interpreter -> + interpreter.getConfig().getLegacyOverrides().isUseSnakeCasePropertyNaming() + ) + .orElse(false); + ObjectWriter objectWriter = useSnakeCaseMappingOverride + ? SNAKE_CASE_PYISH_OBJECT_WRITER + : PYISH_OBJECT_WRITER; Writer writer; Optional maxOutputSize = JinjavaInterpreter .getCurrentMaybe() @@ -85,9 +108,11 @@ public static String getAsPyishStringOrThrow(Object val, boolean forOutput) } else { writer = new CharArrayWriter(); } - objectWriter - .withAttribute(ALLOW_SNAKE_CASE_ATTRIBUTE, !forOutput) - .writeValue(writer, val); + if (!useSnakeCaseMappingOverride) { + objectWriter = objectWriter.withAttribute(ALLOW_SNAKE_CASE_ATTRIBUTE, !forOutput); + } + objectWriter.writeValue(writer, val); + return writer.toString(); } diff --git a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java index 65b937cad..a96277dd3 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -111,6 +111,30 @@ public void itDoesNotConvertToSnakeCaseMapWhenResultIsForOutput() { assertThat(interpreter.render("{{ foo }}")).isEqualTo("{'fooBar': 'bar'}"); } + @Test + public void itSerializesToSnakeCaseWhenLegacyOverrideIsSet() { + Jinjava jinjava = new Jinjava( + JinjavaConfig + .newBuilder() + .withLegacyOverrides( + LegacyOverrides + .newBuilder() + .withUsePyishObjectMapper(true) + .withUseSnakeCasePropertyNaming(true) + .build() + ) + .build() + ); + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + try { + JinjavaInterpreter.pushCurrent(interpreter); + interpreter.getContext().put("foo", new Foo("bar")); + assertThat(interpreter.render("{{ foo }}")).isEqualTo("{'foo_bar': 'bar'}"); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + static class Foo { private final String bar; From 5cb42f51f7af1d3e4fe617c86a266f066b9d031c Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 16 Mar 2023 14:35:06 -0400 Subject: [PATCH 207/637] Also make default ObjectMapper for ToJsonFilter use snake_properties if legacy override is set --- .../java/com/hubspot/jinjava/JinjavaConfig.java | 16 ++++++++++++++-- .../jinjava/lib/filter/FromJsonFilter.java | 7 ++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java index 47d60a6d2..c37574897 100644 --- a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java +++ b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java @@ -18,6 +18,7 @@ import static com.hubspot.jinjava.lib.fn.Functions.DEFAULT_RANGE_LIMIT; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.hubspot.jinjava.el.JinjavaInterpreterResolver; import com.hubspot.jinjava.el.JinjavaNodePreProcessor; import com.hubspot.jinjava.el.JinjavaObjectUnwrapper; @@ -44,6 +45,7 @@ import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; +import javax.annotation.Nullable; import javax.el.ELResolver; public class JinjavaConfig { @@ -135,11 +137,21 @@ private JinjavaConfig(Builder builder) { legacyOverrides = builder.legacyOverrides; dateTimeProvider = builder.dateTimeProvider; enablePreciseDivideFilter = builder.enablePreciseDivideFilter; - objectMapper = builder.objectMapper; + objectMapper = setupObjectMapper(builder.objectMapper); objectUnwrapper = builder.objectUnwrapper; nodePreProcessor = builder.nodePreProcessor; } + private ObjectMapper setupObjectMapper(@Nullable ObjectMapper objectMapper) { + if (objectMapper == null) { + objectMapper = new ObjectMapper(); + if (legacyOverrides.isUseSnakeCasePropertyNaming()) { + objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); + } + } + return objectMapper; + } + public Charset getCharset() { return charset; } @@ -298,7 +310,7 @@ public static class Builder { private ExecutionMode executionMode = DefaultExecutionMode.instance(); private LegacyOverrides legacyOverrides = LegacyOverrides.NONE; private boolean enablePreciseDivideFilter = false; - private ObjectMapper objectMapper = new ObjectMapper(); + private ObjectMapper objectMapper = null; private ObjectUnwrapper objectUnwrapper = new JinjavaObjectUnwrapper(); private BiConsumer nodePreProcessor = new JinjavaNodePreProcessor(); diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FromJsonFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/FromJsonFilter.java index fbea6fa8d..bacee747d 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FromJsonFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FromJsonFilter.java @@ -1,6 +1,5 @@ package com.hubspot.jinjava.lib.filter; -import com.fasterxml.jackson.databind.ObjectMapper; import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; @@ -19,7 +18,6 @@ snippets = { @JinjavaSnippet(code = "{{object|fromJson}}") } ) public class FromJsonFilter implements Filter { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { @@ -31,7 +29,10 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) throw new InvalidInputException(interpreter, this, InvalidReason.STRING); } try { - return OBJECT_MAPPER.readValue((String) var, Object.class); + return interpreter + .getConfig() + .getObjectMapper() + .readValue((String) var, Object.class); } catch (IOException e) { throw new InvalidInputException(interpreter, this, InvalidReason.JSON_READ); } From b28c7dcb07a6bd6c37608ef97d3de9b0201c4e7e Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 17 Mar 2023 14:06:24 -0400 Subject: [PATCH 208/637] Also convert to snake case accessible map when serializing map entries --- .../serialization/MapEntrySerializer.java | 17 +++++++++++------ .../serialization/PyishObjectMapperTest.java | 11 +++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java index dd354bb46..e67cfd32a 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java @@ -26,11 +26,16 @@ public void serialize( ); String key; String value; + ObjectWriter objectWriter = PyishObjectMapper.PYISH_OBJECT_WRITER.withAttribute( + PyishObjectMapper.ALLOW_SNAKE_CASE_ATTRIBUTE, + serializerProvider.getAttribute(PyishObjectMapper.ALLOW_SNAKE_CASE_ATTRIBUTE) + ); if (remainingLength != null) { - ObjectWriter objectWriter = PyishObjectMapper.PYISH_OBJECT_WRITER.withAttribute( - LengthLimitingWriter.REMAINING_LENGTH_ATTRIBUTE, - remainingLength - ); + objectWriter = + objectWriter.withAttribute( + LengthLimitingWriter.REMAINING_LENGTH_ATTRIBUTE, + remainingLength + ); key = objectWriter.writeValueAsString(entry.getKey()); LengthLimitingWriter lengthLimitingWriter = new LengthLimitingWriter( new CharArrayWriter(), @@ -39,8 +44,8 @@ public void serialize( objectWriter.writeValue(lengthLimitingWriter, entry.getValue()); value = lengthLimitingWriter.toString(); } else { - key = PyishObjectMapper.PYISH_OBJECT_WRITER.writeValueAsString(entry.getKey()); - value = PyishObjectMapper.PYISH_OBJECT_WRITER.writeValueAsString(entry.getValue()); + key = objectWriter.writeValueAsString(entry.getKey()); + value = objectWriter.writeValueAsString(entry.getValue()); } jsonGenerator.writeRawValue(String.format("fn:map_entry(%s, %s)", key, value)); } diff --git a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java index a96277dd3..39b03e3c5 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -10,6 +10,7 @@ import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -96,6 +97,16 @@ public void itSerializesToSnakeCaseAccessibleMap() { .isEqualTo("{'fooBar': 'bar'} |allow_snake_case"); } + @Test + public void itSerializesToSnakeCaseAccessibleMapWhenInMapEntry() { + assertThat( + PyishObjectMapper.getAsPyishString( + new AbstractMap.SimpleImmutableEntry<>("foo", new Foo("bar")) + ) + ) + .isEqualTo("fn:map_entry('foo', {'fooBar': 'bar'} |allow_snake_case)"); + } + @Test public void itDoesNotConvertToSnakeCaseMapWhenResultIsForOutput() { Jinjava jinjava = new Jinjava( From 21932bc90df4fc6b7ddcf2e8a294d91761fa4125 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 16 Mar 2023 10:20:22 -0400 Subject: [PATCH 209/637] Add functionality for do blocks These are like inline do tags, except they evaluate the body and then discard the output rather than just the single expression. --- .../jinjava/interpret/JinjavaInterpreter.java | 6 +- .../expression/EagerExpressionStrategy.java | 28 +------ .../com/hubspot/jinjava/lib/tag/DoTag.java | 36 +++++---- .../com/hubspot/jinjava/lib/tag/SetTag.java | 2 +- .../jinjava/lib/tag/eager/EagerDoTag.java | 60 ++++++++++++--- .../jinjava/lib/tag/eager/EagerFromTag.java | 7 +- .../jinjava/lib/tag/eager/EagerImportTag.java | 7 +- .../lib/tag/eager/EagerStateChangingTag.java | 7 +- .../util/EagerReconstructionUtils.java | 73 ++++++++++++++----- .../lib/tag/eager/EagerImportTagTest.java | 18 ++--- ...-import-modification-in-for.expected.jinja | 16 ++-- ...les-deferred-from-import-as.expected.jinja | 4 +- ...andles-deferred-import-vars.expected.jinja | 8 +- ...-double-import-modification.expected.jinja | 8 +- ...ndles-import-in-deferred-if.expected.jinja | 4 +- .../uses-unique-macro-names.expected.jinja | 4 +- ...ucts-deferred-outside-block.expected.jinja | 4 +- .../tags/settag/set-var-and-deferred.jinja | 4 +- 18 files changed, 174 insertions(+), 122 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index ff7a3fc69..b92b3883f 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -33,8 +33,8 @@ import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; import com.hubspot.jinjava.interpret.TemplateError.ErrorType; import com.hubspot.jinjava.interpret.errorcategory.BasicTemplateErrorCategory; +import com.hubspot.jinjava.lib.tag.DoTag; import com.hubspot.jinjava.lib.tag.ExtendsTag; -import com.hubspot.jinjava.lib.tag.SetTag; import com.hubspot.jinjava.lib.tag.eager.EagerGenericTag; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.objects.serialization.PyishSerializable; @@ -399,9 +399,9 @@ public String render(Node root, boolean processExtendRoots) { resolveBlockStubs(output); if (ignoredOutput.length() > 0) { return ( - EagerReconstructionUtils.buildBlockSetTag( - SetTag.IGNORED_VARIABLE_NAME, + EagerReconstructionUtils.wrapInTag( ignoredOutput.toString(), + DoTag.TAG_NAME, this, false ) + diff --git a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java index 377118fe3..0a5c68e0e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java @@ -5,7 +5,6 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; import com.hubspot.jinjava.lib.filter.EscapeFilter; -import com.hubspot.jinjava.lib.tag.RawTag; import com.hubspot.jinjava.lib.tag.eager.DeferredToken; import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; import com.hubspot.jinjava.tree.output.RenderedOutputNode; @@ -65,7 +64,7 @@ private String eagerResolveExpression( interpreter ) ); - String helpers = wrapInExpression( + String deferredExpressionImage = wrapInExpression( eagerExecutionResult.getResult().toString(), interpreter ); @@ -74,7 +73,7 @@ private String eagerResolveExpression( interpreter, new DeferredToken( new ExpressionToken( - helpers, + deferredExpressionImage, master.getLineNumber(), master.getStartPosition(), master.getSymbols() @@ -93,7 +92,7 @@ private String eagerResolveExpression( ); // There is only a preserving prefix because it couldn't be entirely evaluated. return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( - prefixToPreserveState.toString() + helpers, + prefixToPreserveState.toString() + deferredExpressionImage, interpreter ); } @@ -123,8 +122,7 @@ public static String postProcessResult( } } } else { - // Possible macro/set tag in front of this one. Includes result - result = wrapInRawOrExpressionIfNeeded(result, interpreter); + result = EagerReconstructionUtils.wrapInRawIfNeeded(result, interpreter); } } @@ -147,24 +145,6 @@ private static long getParsingErrorsCount(JinjavaInterpreter interpreter) { .count(); } - private static String wrapInRawOrExpressionIfNeeded( - String output, - JinjavaInterpreter interpreter - ) { - JinjavaConfig config = interpreter.getConfig(); - if ( - config.getExecutionMode().isPreserveRawTags() && - !interpreter.getContext().isUnwrapRawOverride() && - ( - output.contains(config.getTokenScannerSymbols().getExpressionStart()) || - output.contains(config.getTokenScannerSymbols().getExpressionStartWithTag()) - ) - ) { - return EagerReconstructionUtils.wrapInTag(output, RawTag.TAG_NAME, interpreter); - } - return output; - } - private static String wrapInExpression(String output, JinjavaInterpreter interpreter) { JinjavaConfig config = interpreter.getConfig(); return String.format( diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/DoTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/DoTag.java index 07f3c3657..eec66d106 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/DoTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/DoTag.java @@ -4,40 +4,44 @@ import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.tree.TagNode; +import com.hubspot.jinjava.tree.parse.TagToken; import org.apache.commons.lang3.StringUtils; @JinjavaDoc( value = "Evaluates expression without printing out result.", - snippets = { @JinjavaSnippet(code = "{% do list.append('value 2') %}") } + snippets = { + @JinjavaSnippet(code = "{% do list.append('value 2') %}"), + @JinjavaSnippet( + desc = "Execute a block of code in the same scope while ignoring the output", + code = "{% do %}\n" + + "{% set foo = [] %}\n" + + "{{ foo.append('a') }}\n" + + "{% enddo %}" + ) + } ) @JinjavaTextMateSnippet(code = "{% do ${1:expr} %}") -public class DoTag implements Tag { +public class DoTag implements Tag, FlexibleTag { public static final String TAG_NAME = "do"; @Override public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { - if (StringUtils.isBlank(tagNode.getHelpers())) { - throw new TemplateSyntaxException( - tagNode.getMaster().getImage(), - "Tag 'do' expects expression", - tagNode.getLineNumber(), - tagNode.getStartPosition() - ); + if (hasEndTag((TagToken) tagNode.getMaster())) { + tagNode.getChildren().forEach(child -> child.render(interpreter)); + } else { + interpreter.resolveELExpression(tagNode.getHelpers(), tagNode.getLineNumber()); } - - interpreter.resolveELExpression(tagNode.getHelpers(), tagNode.getLineNumber()); return ""; } @Override - public String getEndTagName() { - return null; + public String getName() { + return TAG_NAME; } @Override - public String getName() { - return TAG_NAME; + public boolean hasEndTag(TagToken tagToken) { + return StringUtils.isBlank(tagToken.getHelpers()); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/SetTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/SetTag.java index 444951f75..eaf19896f 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/SetTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/SetTag.java @@ -70,7 +70,7 @@ code = "{% set name = 'Jack' %}\n" + "{% set message %}\n" + "My name is {{ name }}\n" + - "{% end_set %}" + "{% endset %}" ) } ) diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerDoTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerDoTag.java index 34ad53bb4..74105833e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerDoTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerDoTag.java @@ -1,12 +1,16 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.tag.DoTag; +import com.hubspot.jinjava.lib.tag.FlexibleTag; +import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; -import org.apache.commons.lang3.StringUtils; +import com.hubspot.jinjava.util.EagerContextWatcher; +import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; +import com.hubspot.jinjava.util.EagerReconstructionUtils; -public class EagerDoTag extends EagerStateChangingTag { +public class EagerDoTag extends EagerStateChangingTag implements FlexibleTag { public EagerDoTag() { super(new DoTag()); @@ -17,15 +21,51 @@ public EagerDoTag(DoTag doTag) { } @Override - public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter) { - String expr = tagToken.getHelpers(); - if (StringUtils.isBlank(expr)) { - throw new TemplateSyntaxException( + public String eagerInterpret( + TagNode tagNode, + JinjavaInterpreter interpreter, + InterpretException e + ) { + if (hasEndTag((TagToken) tagNode.getMaster())) { + EagerExecutionResult eagerExecutionResult = EagerContextWatcher.executeInChildContext( + eagerInterpreter -> + EagerExpressionResult.fromSupplier( + () -> renderChildren(tagNode, interpreter), + eagerInterpreter + ), interpreter, - tagToken.getImage(), - "Tag 'do' expects expression" + EagerContextWatcher + .EagerChildContextConfig.newBuilder() + .withTakeNewValue(true) + .withCheckForContextChanges(!interpreter.getContext().isDeferredExecutionMode()) + .build() + ); + StringBuilder prefixToPreserveState = new StringBuilder(); + if (interpreter.getContext().isDeferredExecutionMode()) { + prefixToPreserveState.append(eagerExecutionResult.getPrefixToPreserveState()); + } else { + interpreter.getContext().putAll(eagerExecutionResult.getSpeculativeBindings()); + } + if (eagerExecutionResult.getResult().isFullyResolved()) { + return (prefixToPreserveState.toString()); + } + return EagerReconstructionUtils.wrapInTag( + eagerExecutionResult.asTemplateString(), + getName(), + interpreter, + true ); } - return EagerPrintTag.interpretExpression(expr, tagToken, interpreter, false); + return EagerPrintTag.interpretExpression( + tagNode.getHelpers(), + (TagToken) tagNode.getMaster(), + interpreter, + false + ); + } + + @Override + public boolean hasEndTag(TagToken tagToken) { + return getTag().hasEndTag(tagToken); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTag.java index bb5d0e5c4..b4adfd923 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTag.java @@ -1,13 +1,12 @@ package com.hubspot.jinjava.lib.tag.eager; -import static com.hubspot.jinjava.lib.tag.SetTag.IGNORED_VARIABLE_NAME; - import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.lib.tag.DoTag; import com.hubspot.jinjava.lib.tag.FromTag; import com.hubspot.jinjava.loader.RelativePathResolver; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; @@ -119,9 +118,9 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter output + EagerReconstructionUtils.buildSetTag(newToOldImportNames, interpreter, true); } - return EagerReconstructionUtils.buildBlockSetTag( - IGNORED_VARIABLE_NAME, + return EagerReconstructionUtils.wrapInTag( output, + DoTag.TAG_NAME, interpreter, true ); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java index 1637ab966..4eb5b2531 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java @@ -9,8 +9,8 @@ import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.lib.tag.DoTag; import com.hubspot.jinjava.lib.tag.ImportTag; -import com.hubspot.jinjava.lib.tag.SetTag; import com.hubspot.jinjava.loader.RelativePathResolver; import com.hubspot.jinjava.objects.collections.PyMap; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; @@ -141,9 +141,9 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter childBindings ); } - return EagerReconstructionUtils.buildBlockSetTag( - SetTag.IGNORED_VARIABLE_NAME, + return EagerReconstructionUtils.wrapInTag( finalOutput, + DoTag.TAG_NAME, interpreter, true ); @@ -426,7 +426,6 @@ public static void integrateChild( JinjavaInterpreter child, JinjavaInterpreter parent ) { - childBindings.remove(SetTag.IGNORED_VARIABLE_NAME); for (MacroFunction macro : child.getContext().getGlobalMacros().values()) { if (parent.getContext().isDeferredExecutionMode()) { macro.setDeferred(true); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java index 0dd86c3ab..4392c7a26 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerStateChangingTag.java @@ -18,7 +18,7 @@ public EagerStateChangingTag(T tag) { } @Override - public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { + public final String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { return eagerInterpret(tagNode, interpreter, null); } @@ -42,10 +42,7 @@ public String eagerInterpret( eagerInterpreter -> EagerExpressionResult.fromString(renderChildren(tagNode, eagerInterpreter)), interpreter, - EagerContextWatcher - .EagerChildContextConfig.newBuilder() - .withForceDeferredExecutionMode(true) - .build() + EagerContextWatcher.EagerChildContextConfig.newBuilder().build() ) .asTemplateString() ); diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 26d8de6cf..6ca407547 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -403,7 +403,7 @@ public static String buildSetTag( * @param name The name of the variable to set. * @param value The string value, potentially containing jinja code to put in the set tag block. * @param interpreter The Jinjava interpreter. - * @param registerDeferredToken Whether or not to register the returned {@link SetTag} + * @param registerDeferredToken Whether to register the returned {@link SetTag} * token as an {@link DeferredToken}. * @return A jinjava-syntax string that is the image of a block set tag that will * be executed at a later time. @@ -502,7 +502,7 @@ public static String wrapInRawIfNeeded(String output, JinjavaInterpreter interpr interpreter.getConfig().getTokenScannerSymbols().getExpressionStartWithTag() ) ) { - output = wrapInTag(output, RawTag.TAG_NAME, interpreter); + output = wrapInTag(output, RawTag.TAG_NAME, interpreter, false); } } return output; @@ -519,31 +519,64 @@ public static String wrapInAutoEscapeIfNeeded( !interpreter.getContext().getParent().isAutoEscape() ) ) { - output = wrapInTag(output, AutoEscapeTag.TAG_NAME, interpreter); + output = wrapInTag(output, AutoEscapeTag.TAG_NAME, interpreter, false); } return output; } - public static String wrapInTag( - String s, + /** + * Wrap the string output in a specified block-type tag. + * @param body The string body to wrap. + * @param tagNameToWrap The name of the tag which will wrap around the {@param body}. + * @param interpreter The Jinjava interpreter. + * @param registerDeferredToken Whether to register the returned Tag + * token as an {@link DeferredToken}. + * @return A jinjava-syntax string that is the image of a block set tag that will + * be executed at a later time. + */public static String wrapInTag( + String body, String tagNameToWrap, - JinjavaInterpreter interpreter + JinjavaInterpreter interpreter, + boolean registerDeferredToken ) { - return ( - String.format( - "%s %s %s", - interpreter.getConfig().getTokenScannerSymbols().getExpressionStartWithTag(), - tagNameToWrap, - interpreter.getConfig().getTokenScannerSymbols().getExpressionEndWithTag() - ) + - s + - String.format( - "%s end%s %s", - interpreter.getConfig().getTokenScannerSymbols().getExpressionStartWithTag(), - tagNameToWrap, - interpreter.getConfig().getTokenScannerSymbols().getExpressionEndWithTag() - ) + Map> disabled = interpreter.getConfig().getDisabled(); + if ( + disabled != null && + disabled.containsKey(Library.TAG) && + disabled.get(Library.TAG).contains(tagNameToWrap) + ) { + throw new DisabledException(String.format("%s tag disabled", tagNameToWrap)); + } + LengthLimitingStringJoiner blockSetTokenBuilder = new LengthLimitingStringJoiner( + interpreter.getConfig().getMaxOutputSize(), + " " ); + StringJoiner endTokenBuilder = new StringJoiner(" "); + blockSetTokenBuilder + .add(interpreter.getConfig().getTokenScannerSymbols().getExpressionStartWithTag()) + .add(tagNameToWrap) + .add(interpreter.getConfig().getTokenScannerSymbols().getExpressionEndWithTag()); + endTokenBuilder + .add(interpreter.getConfig().getTokenScannerSymbols().getExpressionStartWithTag()) + .add("end" + tagNameToWrap) + .add(interpreter.getConfig().getTokenScannerSymbols().getExpressionEndWithTag()); + String image = blockSetTokenBuilder + body + endTokenBuilder; + if (registerDeferredToken) { + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + new DeferredToken( + new TagToken( + blockSetTokenBuilder.toString(), + interpreter.getLineNumber(), + interpreter.getPosition(), + interpreter.getConfig().getTokenScannerSymbols() + ), + Collections.emptySet(), + Collections.emptySet() + ) + ); + } + return image; } public static String wrapInChildScope(String toWrap, JinjavaInterpreter interpreter) { 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 eb3cb1bd4..2c9b2c36c 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 @@ -462,11 +462,11 @@ public void itHandlesQuadLayerInDeferredIf() { ); assertThat(result) .isEqualTo( - "{% if deferred %}{% set __ignored__ %}{% set current_path = 'import-tree-b.jinja' %}{% set a,foo_b = {'foo_a': 'a', 'import_resource_path': 'import-tree-a.jinja', 'something': 'somn'} ,null %}{% set b = {} %}{% for __ignored__ in [0] %}{% set __ignored__ %}{% set current_path = 'import-tree-a.jinja' %}{% set a = {} %}{% for __ignored__ in [0] %}{% set something = 'somn' %}{% do a.update({'something': something}) %}\n" + + "{% if deferred %}{% do %}{% set current_path = 'import-tree-b.jinja' %}{% set a,foo_b = {'foo_a': 'a', 'import_resource_path': 'import-tree-a.jinja', 'something': 'somn'} ,null %}{% set b = {} %}{% for __ignored__ in [0] %}{% do %}{% set current_path = 'import-tree-a.jinja' %}{% set a = {} %}{% for __ignored__ in [0] %}{% set something = 'somn' %}{% do a.update({'something': something}) %}\n" + "{% set foo_a = 'a' %}{% do a.update({'foo_a': foo_a}) %}\n" + - "{% do a.update({'foo_a': 'a','import_resource_path': 'import-tree-a.jinja','something': 'somn'}) %}{% endfor %}{% set current_path = 'import-tree-b.jinja' %}{% endset %}\n" + + "{% do a.update({'foo_a': 'a','import_resource_path': 'import-tree-a.jinja','something': 'somn'}) %}{% endfor %}{% set current_path = 'import-tree-b.jinja' %}{% enddo %}\n" + "{% set foo_b = 'b' + a.foo_a %}{% do b.update({'foo_b': foo_b}) %}\n" + - "{% do b.update({'a': a,'foo_b': foo_b,'import_resource_path': 'import-tree-b.jinja'}) %}{% endfor %}{% set current_path = '' %}{% endset %}{% endif %}" + "{% do b.update({'a': a,'foo_b': foo_b,'import_resource_path': 'import-tree-b.jinja'}) %}{% endfor %}{% set current_path = '' %}{% enddo %}{% endif %}" ); removeDeferredContextKeys(); @@ -707,8 +707,8 @@ public void itDoesNotSilentlyOverrideVariable() { .isEqualTo( "a" + "{% set vars = {'foo': 'a', 'import_resource_path': 'var-a.jinja'} %}{% if deferred %}" + - "{% set __ignored__ %}{% set current_path = 'var-b.jinja' %}{% set vars = {} %}{% for __ignored__ in [0] %}{% set foo = 'b' %}{% do vars.update({'foo': foo}) %}\n" + - "{% do vars.update({'foo': 'b','import_resource_path': 'var-b.jinja'}) %}{% endfor %}{% set current_path = '' %}{% endset %}" + + "{% do %}{% set current_path = 'var-b.jinja' %}{% set vars = {} %}{% for __ignored__ in [0] %}{% set foo = 'b' %}{% do vars.update({'foo': foo}) %}\n" + + "{% do vars.update({'foo': 'b','import_resource_path': 'var-b.jinja'}) %}{% endfor %}{% set current_path = '' %}{% enddo %}" + "{% endif %}" + "{{ vars.foo }}" ); @@ -734,8 +734,8 @@ public void itDoesNotSilentlyOverrideVariableWithoutAlias() { .isEqualTo( "a" + "{% set foo = 'a' %}{% if deferred %}" + - "{% set __ignored__ %}{% set current_path = 'var-b.jinja' %}{% set foo = 'b' %}\n" + - "{% set current_path = '' %}{% endset %}" + + "{% do %}{% set current_path = 'var-b.jinja' %}{% set foo = 'b' %}\n" + + "{% set current_path = '' %}{% enddo %}" + "{% endif %}" + "{{ foo }}" ); @@ -752,9 +752,9 @@ public void itDoesNotDeferImportedVariablesWhenNotInDeferredExecutionMode() { .trim(); assertThat(result) .isEqualTo( - "{% set __ignored__ %}{% set current_path = 'set-two-variables.jinja' %}{% set foo = deferred %}\n" + + "{% do %}{% set current_path = 'set-two-variables.jinja' %}{% set foo = deferred %}\n" + "\n" + - "{% set current_path = '' %}{% endset %}{{ foo }} bar" + "{% set current_path = '' %}{% enddo %}{{ foo }} bar" ); } diff --git a/src/test/resources/eager/does-not-override-import-modification-in-for.expected.jinja b/src/test/resources/eager/does-not-override-import-modification-in-for.expected.jinja index cb56c613d..5a8c51b9a 100644 --- a/src/test/resources/eager/does-not-override-import-modification-in-for.expected.jinja +++ b/src/test/resources/eager/does-not-override-import-modification-in-for.expected.jinja @@ -1,40 +1,40 @@ {% for __ignored__ in [0] %} -{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set bar1 = {} %}{% set bar1,foo = {} ,'start' %}{% if deferred %} +{% do %}{% set current_path = 'deferred-modification.jinja' %}{% set bar1 = {} %}{% set bar1,foo = {} ,'start' %}{% if deferred %} {% set foo = 'starta' %}{% do bar1.update({'foo': foo}) %} {% endif %} {% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}{% do bar1.update({'foo': foo}) %} -{% do bar1.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% endset %} +{% do bar1.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% enddo %} {{ bar1.foo }} -{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set bar2 = {} %}{% set bar2,foo = {} ,'start' %}{% if deferred %} +{% do %}{% set current_path = 'deferred-modification.jinja' %}{% set bar2 = {} %}{% set bar2,foo = {} ,'start' %}{% if deferred %} {% set foo = 'starta' %}{% do bar2.update({'foo': foo}) %} {% endif %} {% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}{% do bar2.update({'foo': foo}) %} -{% do bar2.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% endset %} +{% do bar2.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% enddo %} {{ bar2.foo }} -{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set bar1 = {} %}{% set bar1,foo = {} ,'start' %}{% if deferred %} +{% do %}{% set current_path = 'deferred-modification.jinja' %}{% set bar1 = {} %}{% set bar1,foo = {} ,'start' %}{% if deferred %} {% set foo = 'starta' %}{% do bar1.update({'foo': foo}) %} {% endif %} {% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}{% do bar1.update({'foo': foo}) %} -{% do bar1.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% endset %} +{% do bar1.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% enddo %} {{ bar1.foo }} -{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set bar2 = {} %}{% set bar2,foo = {} ,'start' %}{% if deferred %} +{% do %}{% set current_path = 'deferred-modification.jinja' %}{% set bar2 = {} %}{% set bar2,foo = {} ,'start' %}{% if deferred %} {% set foo = 'starta' %}{% do bar2.update({'foo': foo}) %} {% endif %} {% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}{% do bar2.update({'foo': foo}) %} -{% do bar2.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% endset %} +{% do bar2.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% enddo %} {{ bar2.foo }} {% endfor %} start diff --git a/src/test/resources/eager/handles-deferred-from-import-as.expected.jinja b/src/test/resources/eager/handles-deferred-from-import-as.expected.jinja index c7418e8a3..8a4df6f1e 100644 --- a/src/test/resources/eager/handles-deferred-from-import-as.expected.jinja +++ b/src/test/resources/eager/handles-deferred-from-import-as.expected.jinja @@ -1,5 +1,5 @@ -{% set myname = deferred + 7 %}{% set __ignored__ %} +{% set myname = deferred + 7 %}{% do %} {% set bar = myname + 19 %} Hello {{ myname }} -{% set from_bar = bar %}{% endset %}from_foo: Hello {{ myname }} +{% set from_bar = bar %}{% enddo %}from_foo: Hello {{ myname }} from_bar: {{ from_bar }} diff --git a/src/test/resources/eager/handles-deferred-import-vars.expected.jinja b/src/test/resources/eager/handles-deferred-import-vars.expected.jinja index f7008add8..483c662b6 100644 --- a/src/test/resources/eager/handles-deferred-import-vars.expected.jinja +++ b/src/test/resources/eager/handles-deferred-import-vars.expected.jinja @@ -1,11 +1,11 @@ -{% set myname = deferred + 3 %}{% set __ignored__ %} +{% set myname = deferred + 3 %}{% do %} {% set bar = myname + 19 %} Hello {{ myname }} -{% endset %}foo: Hello {{ myname }} +{% enddo %}foo: Hello {{ myname }} bar: {{ bar }} --- -{% set myname = deferred + 7 %}{% set __ignored__ %}{% set current_path = 'macro-and-set.jinja' %}{% set simple = {} %} +{% set myname = deferred + 7 %}{% do %}{% set current_path = 'macro-and-set.jinja' %}{% set simple = {} %} {% set bar = myname + 19 %}{% set simple = {} %}{% do simple.update({'bar': bar}) %} Hello {{ myname }} -{% do simple.update({'import_resource_path': 'macro-and-set.jinja'}) %}{% set current_path = '' %}{% endset %}simple.foo: {% set deferred_import_resource_path = 'macro-and-set.jinja' %}{% macro simple.foo() %}Hello {{ myname }}{% endmacro %}{% set deferred_import_resource_path = null %}{{ simple.foo() }} +{% do simple.update({'import_resource_path': 'macro-and-set.jinja'}) %}{% set current_path = '' %}{% enddo %}simple.foo: {% set deferred_import_resource_path = 'macro-and-set.jinja' %}{% macro simple.foo() %}Hello {{ myname }}{% endmacro %}{% set deferred_import_resource_path = null %}{{ simple.foo() }} simple.bar: {{ simple.bar }} diff --git a/src/test/resources/eager/handles-double-import-modification.expected.jinja b/src/test/resources/eager/handles-double-import-modification.expected.jinja index bda97e368..3ba9e59a6 100644 --- a/src/test/resources/eager/handles-double-import-modification.expected.jinja +++ b/src/test/resources/eager/handles-double-import-modification.expected.jinja @@ -1,20 +1,20 @@ -{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set foo = null %}{% set bar1 = {} %}{% set bar1 = {} %}{% if deferred %} +{% do %}{% set current_path = 'deferred-modification.jinja' %}{% set foo = null %}{% set bar1 = {} %}{% set bar1 = {} %}{% if deferred %} {% set foo = 'a' %}{% do bar1.update({'foo': foo}) %} {% endif %} {% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}{% do bar1.update({'foo': foo}) %} -{% do bar1.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% endset %} +{% do bar1.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% enddo %} --- -{% set __ignored__ %}{% set current_path = 'deferred-modification.jinja' %}{% set foo = null %}{% set bar2 = {} %}{% set bar2 = {} %}{% if deferred %} +{% do %}{% set current_path = 'deferred-modification.jinja' %}{% set foo = null %}{% set bar2 = {} %}{% set bar2 = {} %}{% if deferred %} {% set foo = 'a' %}{% do bar2.update({'foo': foo}) %} {% endif %} {% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}{% do bar2.update({'foo': foo}) %} -{% do bar2.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% endset %} +{% do bar2.update({'foo': foo,'import_resource_path': 'deferred-modification.jinja'}) %}{% set current_path = '' %}{% enddo %} --- {{ bar1.foo }} {{ bar2.foo }} diff --git a/src/test/resources/eager/handles-import-in-deferred-if.expected.jinja b/src/test/resources/eager/handles-import-in-deferred-if.expected.jinja index a2ed7392e..ff3237d13 100644 --- a/src/test/resources/eager/handles-import-in-deferred-if.expected.jinja +++ b/src/test/resources/eager/handles-import-in-deferred-if.expected.jinja @@ -1,7 +1,7 @@ {% set primary_line_height = 100 %}{% if deferred %} -{% set __ignored__ %}{% set current_path = '../settag/set-val.jinja' %}{% set simple = {} %}{% for __ignored__ in [0] %}{% set primary_line_height = 42 %}{% do simple.update({'primary_line_height': primary_line_height}) %}{% do simple.update({'primary_line_height': 42,'import_resource_path': '../settag/set-val.jinja'}) %}{% endfor %}{% set current_path = '' %}{% endset %} +{% do %}{% set current_path = '../settag/set-val.jinja' %}{% set simple = {} %}{% for __ignored__ in [0] %}{% set primary_line_height = 42 %}{% do simple.update({'primary_line_height': primary_line_height}) %}{% do simple.update({'primary_line_height': 42,'import_resource_path': '../settag/set-val.jinja'}) %}{% endfor %}{% set current_path = '' %}{% enddo %} {% else %} -{% set __ignored__ %}{% set current_path = '../settag/set-val.jinja' %}{% set primary_line_height = 42 %}{% set current_path = '' %}{% endset %} +{% do %}{% set current_path = '../settag/set-val.jinja' %}{% set primary_line_height = 42 %}{% set current_path = '' %}{% enddo %} {% endif %} simple.primary_line_height (deferred): {{ simple.primary_line_height }} primary_line_height (deferred): {{ primary_line_height }} diff --git a/src/test/resources/eager/uses-unique-macro-names.expected.jinja b/src/test/resources/eager/uses-unique-macro-names.expected.jinja index 713b24841..9fce79651 100644 --- a/src/test/resources/eager/uses-unique-macro-names.expected.jinja +++ b/src/test/resources/eager/uses-unique-macro-names.expected.jinja @@ -3,8 +3,8 @@ {% set __macro_foo_97643642_temp_variable_0__ %} Goodbye {{ myname }} {% endset %}{% set a = filter:upper.filter(__macro_foo_97643642_temp_variable_0__, ____int3rpr3t3r____) %} -{% set __ignored__ %}{% set current_path = 'macro-with-filter.jinja' %} +{% do %}{% set current_path = 'macro-with-filter.jinja' %} {% set __macro_foo_927217348_temp_variable_0__ %}Hello {{ myname }}{% endset %}{% set b = filter:upper.filter(__macro_foo_927217348_temp_variable_0__, ____int3rpr3t3r____) %} -{% set current_path = '' %}{% endset %} +{% set current_path = '' %}{% enddo %} {{ a }} {{ b }} diff --git a/src/test/resources/tags/eager/extendstag/reconstructs-deferred-outside-block.expected.jinja b/src/test/resources/tags/eager/extendstag/reconstructs-deferred-outside-block.expected.jinja index b0a755fc5..04a4e8625 100644 --- a/src/test/resources/tags/eager/extendstag/reconstructs-deferred-outside-block.expected.jinja +++ b/src/test/resources/tags/eager/extendstag/reconstructs-deferred-outside-block.expected.jinja @@ -1,9 +1,9 @@ -{% set __ignored__ %} +{% do %} {% if deferred %} {% set foo = 'yes' %} {% else %} {% set foo = 'no' %} -{% endif %}{% endset %} +{% endif %}{% enddo %}