diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..10338025d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: ci + +on: [ push, pull_request ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + java: [ 17, 21, 25 ] + name: jdk-${{ matrix.java }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-java@v3 + with: + distribution: "temurin" + # The JDK listed last will be the default and what Maven runs with + # https://github.com/marketplace/actions/setup-java-jdk#install-multiple-jdks + java-version: | + ${{ matrix.java }} + 25 + cache: "maven" + - name: "Build" + run: | + mvn \ + --batch-mode \ + -no-transfer-progress \ + -V \ + -Dproject.build.jdk.version=${{ matrix.java }} \ + verify + env: + JDK_JAVA_OPTIONS: --add-opens=java.base/java.lang=ALL-UNNAMED 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 - diff --git a/CHANGES.md b/CHANGES.md index e9b5bc5ac..e93944230 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,102 @@ # Jinjava Releases # +### 2026-02-02 Version 2.8.3 ([Maven Central](https://search.maven.org/artifact/com.hubspot.jinjava/jinjava/2.8.3/jar)) ### +* Disallow accessing properties on restricted classes while rendering through ForTag +* Upgrade jackson to version 2.20 +* [Add performance optimization to chained filters](https://github.com/HubSpot/jinjava/pull/1274) + +### 2026-02-02 Version 2.7.6 ([Maven Central](https://search.maven.org/artifact/com.hubspot.jinjava/jinjava/2.7.6/jar)) ### +* Disallow accessing properties on restricted classes while rendering through ForTag +* Upgrade jackson to version 2.20 + +### 2025-10-22 Version 2.8.2 ([Maven Central](https://search.maven.org/artifact/com.hubspot.jinjava/jinjava/2.8.2/jar)) ### +* [Fix helper token escape handling and unescaping when unquoting strings](https://github.com/HubSpot/jinjava/pull/1263) + +### 2025-09-30 Version 2.7.5 ([Maven Central](https://search.maven.org/artifact/com.hubspot.jinjava/jinjava/2.7.5/jar)) ### +* Disallow accessing properties on restricted classes while rendering + +### 2025-09-16 Version 2.8.1 ([Maven Central](https://search.maven.org/artifact/com.hubspot.jinjava/jinjava/2.8.1/jar)) ### +* Disallow accessing properties on restricted classes while rendering +* [Make stack operations use AutoCloseable for safer usage with try-with-resources](https://github.com/HubSpot/jinjava/pull/1250) + +### 2025-05-05 Version 2.8.0 ([Maven Central](https://search.maven.org/artifact/com.hubspot.jinjava/jinjava/2.8.0/jar)) ### +* [Target Java 17](https://github.com/HubSpot/jinjava/pull/1238) +* [Implement PyMap#get with optional default](https://github.com/HubSpot/jinjava/pull/1233) +* [Fix ConcurrentModificationException when sharing Context across threads](https://github.com/HubSpot/jinjava/pull/1239) +* [Fix max render depth tracking for {% call %} tags](https://github.com/HubSpot/jinjava/pull/1229) + +### 2024-12-06 Version 2.7.4 ([Maven Central](https://search.maven.org/artifact/com.hubspot.jinjava/jinjava/2.7.4/jar)) ### +* [Implement jinja2.ext.loopcontrols extensions (break and continue)](https://github.com/HubSpot/jinjava/pull/1219) +* [Apply whitespace rules for LStrip and Trim to comment blocks](https://github.com/HubSpot/jinjava/pull/1217) +### 2024-09-12 Version 2.7.3 ([Maven Central](https://search.maven.org/artifact/com.hubspot.jinjava/jinjava/2.7.3/jar)) ### +* [Add support for numeric keys in map literal](https://github.com/HubSpot/jinjava/pull/1152) +* [Add feature to consider undefined variable a TemplateError](https://github.com/HubSpot/jinjava/pull/1174) +* [Improve Optional value serialization](https://github.com/HubSpot/jinjava/pull/1175) +* [Fix bug where extends roots were processed inside of the RenderFilter](https://github.com/HubSpot/jinjava/pull/1159) +* [Don't allow illegal characters in XmlAttrFilter](https://github.com/HubSpot/jinjava/pull/1179) +* [Limit string length in `+` and `~` operators](https://github.com/HubSpot/jinjava/pull/1161) +* Fix various RuntimeExceptions in filters and functions +* Updates dependency versions +### 2024-02-12 Version 2.7.2 ([Maven Central](https://search.maven.org/artifact/com.hubspot.jinjava/jinjava/2.7.2/jar)) ### +* [Use interpreter's locale for strftime functions](https://github.com/HubSpot/jinjava/pull/1109) +* [Prevent infinite hashCode recursion in PyList or PyMap](https://github.com/HubSpot/jinjava/pull/1112) +* [Support aliasing macro function names in {% from %} tag](https://github.com/HubSpot/jinjava/pull/1117) +* [Add whitespace trimming functionality for notes and expressions](https://github.com/HubSpot/jinjava/pull/1122) +* [Add feature to prevent accidental expressions from being output](https://github.com/HubSpot/jinjava/pull/1123) +* [Add length-limiting to |render filter and add |closehtml filter](https://github.com/HubSpot/jinjava/pull/1128) +* [Add length-limiting to |tojson filter](https://github.com/HubSpot/jinjava/pull/1131) +* [Make |pprint filter output in JSON format](https://github.com/HubSpot/jinjava/pull/1132) +* [Allow for loop with `null` values](https://github.com/HubSpot/jinjava/pull/1140) +* [Add length-limiting when coercing strings](https://github.com/HubSpot/jinjava/pull/1142) +* [Add `ECHO_UNDEFINED` feature](https://github.com/HubSpot/jinjava/pull/1150) +* Various PRs for eager execution to support two-phase rendering. + +### 2023-08-11 Version 2.7.1 ([Maven Central](https://search.maven.org/artifact/com.hubspot.jinjava/jinjava/2.7.1/jar)) ### +* [Introduce `{% do %}` blocks](https://github.com/HubSpot/jinjava/pull/1030) +* [Add warnings for unclosed tokens](https://github.com/HubSpot/jinjava/pull/1093) +* [Fix `|default(null)` behavior](https://github.com/HubSpot/jinjava/pull/1008) +* [Improve EscapeJinjavaFilter](https://github.com/HubSpot/jinjava/pull/1027) +* [Improve BeanELResolver Extensibility](https://github.com/HubSpot/jinjava/pull/1028) +* [Add snake_case serialization config option](https://github.com/HubSpot/jinjava/pull/1031) +* [Add generic node pre/post processors](https://github.com/HubSpot/jinjava/pull/1045) +* [Upgrade jackson to 2.14.0](https://github.com/HubSpot/jinjava/pull/1051) +* [Upgrade jsoup to 1.15.3](https://github.com/HubSpot/jinjava/pull/927) +* [Upgrade guava to 31.1](https://github.com/HubSpot/jinjava/pull/1103) +* [Add feature flags to JinjavaConfig](https://github.com/HubSpot/jinjava/pull/1066) +* [Make restricted methods and properties configurable](https://github.com/HubSpot/jinjava/pull/1076) +* [Gracefully handle invalid escaped quotes](https://github.com/HubSpot/jinjava/pull/1098) +* [Warn when datetime filters use null arguments](https://github.com/HubSpot/jinjava/pull/1064) +* [Fix interpreter scope inside striptags filter](https://github.com/HubSpot/jinjava/pull/1068) +* Fix various RuntimeExceptions in filters and functions +* Various PRs for eager execution to support two-phase rendering. + +### 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) @@ -13,8 +111,8 @@ * [Make LazyExpression memoization disable-able](https://github.com/HubSpot/jinjava/pull/673) * [Add new MapELResolver with type coercion to support accessing enum keys](https://github.com/HubSpot/jinjava/pull/688) * Add methods to [remove error from interpreter](https://github.com/HubSpot/jinjava/pull/694), -[get the last error](https://github.com/HubSpot/jinjava/pull/695), -and [remove the last error](https://github.com/HubSpot/jinjava/pull/696) + [get the last error](https://github.com/HubSpot/jinjava/pull/695), + and [remove the last error](https://github.com/HubSpot/jinjava/pull/696) * [Pass value of throwInterpreterErrors to child contexts](https://github.com/HubSpot/jinjava/pull/697) * [Support Assignment Blocks with Set tags](https://github.com/HubSpot/jinjava/pull/698) * [Handle spaces better in for loop expressions](https://github.com/HubSpot/jinjava/pull/706) @@ -394,7 +492,7 @@ and [remove the last error](https://github.com/HubSpot/jinjava/pull/696) ### Version 2.1.1 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.hubspot.jinjava%22%20AND%20v%3A%222.1.1%22)) ### -* Better error messages for invalid assignment in expression +* Better error messages for invalid assignment in expression * Allow for locale-based date formatting in StrftimeFormatter * Use configured locale for Functions.datetimeformat @@ -417,7 +515,7 @@ and [remove the last error](https://github.com/HubSpot/jinjava/pull/696) ### Version 2.0.9 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.hubspot.jinjava%22%20AND%20v%3A%222.0.9%22)) ### * update truncate_html filter to support preserving words by default, with an additional parameter to chop words at length -* added unique filter to remove duplicate objects from a sequence +* added unique filter to remove duplicate objects from a sequence * add support for global trim_blocks, lstrip_blocks config settings ### Version 2.0.8 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.hubspot.jinjava%22%20AND%20v%3A%222.0.8%22)) ### @@ -469,8 +567,8 @@ and [remove the last error](https://github.com/HubSpot/jinjava/pull/696) * 2.0.x requires JDK 8, as it contains some critical fixes to date formatting for certain languages (i.e. Finnish months) * The 2.0.x release has some significant refactorings in the parsing code: -** nests the .parse package under the existing .tree package -** consolidating the token scanner logic, updating the node tree parser + ** nests the .parse package under the existing .tree package + ** consolidating the token scanner logic, updating the node tree parser * future updates will be able to detect more specific template syntax errors than was previously possible. @@ -525,4 +623,3 @@ and [remove the last error](https://github.com/HubSpot/jinjava/pull/696) ### Version 1.0.0 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.hubspot.jinjava%22%20AND%20v%3A%221.0.0%22)) ### * Initial Public Release - diff --git a/README.md b/README.md index 7489e5273..e1285fa37 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ You will likely want to provide your own implementation of `ResourceLoader` to hook into your application's template repository, and then tell jinjava about it: ```java -JinjavaConfig config = new JinjavaConfig(); +JinjavaConfig config = JinjavaConfig.builder().build(); Jinjava jinjava = new Jinjava(config); jinjava.setResourceLocator(new MyCustomResourceLocator()); @@ -87,7 +87,7 @@ jinjava.setResourceLocator(new MyCustomResourceLocator()); To use more than one `ResourceLocator`, use a `CascadingResourceLocator`. ```java -JinjavaConfig config = new JinjavaConfig(); +JinjavaConfig config = JinjavaConfig.builder().build(); Jinjava jinjava = new Jinjava(config); jinjava.setResourceLocator(new MyCustomResourceLocator(), new FileResourceLocator()); 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 6c50e9178..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.26 - - - 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"; - } - } - -} diff --git a/src/main/resources/jinjava-checkstyle.xml b/checkstyle.xml similarity index 100% rename from src/main/resources/jinjava-checkstyle.xml rename to checkstyle.xml diff --git a/pom.xml b/pom.xml index 67e11dfed..4f50a5f71 100644 --- a/pom.xml +++ b/pom.xml @@ -5,20 +5,31 @@ com.hubspot basepom - 25.6 + 65.1 com.hubspot.jinjava jinjava - 2.6.1-SNAPSHOT + 3.0.0-SNAPSHOT + + ${project.groupId}:${project.artifactId} Jinja templating engine implemented in Java - false - - 3.24.1-GA + 17 0.8.3 + 3.0.1 + 1.9 + 1.5 + 5.20.0 + 2.20.1 + + + --add-opens=java.base/java.lang=ALL-UNNAMED + + + @@ -26,7 +37,7 @@ org.jsoup jsoup - 1.14.2 + 1.15.3 de.odysseus.juel @@ -51,13 +62,53 @@ commons-net commons-net - 3.3 + 3.9.0 com.googlecode.java-ipv6 java-ipv6 0.17 + + com.fasterxml.jackson.core + jackson-annotations + 2.20 + + + com.fasterxml.jackson.core + jackson-databind + ${dep.jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${dep.jackson.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${dep.jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${dep.jackson.version} + + + com.hubspot.immutables + hubspot-style + ${dep.hubspot-immutables.version} + + + com.hubspot.immutables + immutables-exceptions + ${dep.hubspot-immutables.version} + + + com.hubspot + algebra + ${dep.algebra.version} + @@ -122,10 +173,19 @@ com.fasterxml.jackson.dataformat jackson-dataformat-yaml + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ch.obermuhlner big-math + + com.google.errorprone + error_prone_annotations + runtime + ch.qos.logback @@ -152,6 +212,16 @@ mockito-core test + + org.immutables + value + provided + + + com.hubspot + algebra + + @@ -177,7 +247,6 @@ org.apache.maven.plugins maven-checkstyle-plugin - 2.17 validate @@ -186,7 +255,7 @@ validate - ${basedir}/src/main/resources/jinjava-checkstyle.xml + checkstyle.xml UTF-8 true true @@ -194,6 +263,13 @@ + + com.github.spotbugs + spotbugs-maven-plugin + + spotbugs-exclude-filter.xml + + org.apache.maven.plugins maven-shade-plugin @@ -212,6 +288,7 @@ de.odysseus.juel:juel-api de.odysseus.juel:juel-impl + org.jsoup:jsoup @@ -223,16 +300,63 @@ de.odysseus.el jinjava.de.odysseus.el + + org.jsoup + jinjava.org.jsoup + + + + + ${project.build.targetJdk} + + + + + org.basepom.maven + duplicate-finder-maven-plugin + + + module-info + META-INF.versions.9.module-info + + + + + org.apache.maven.plugins + maven-surefire-plugin + + @{argLine} ${basepom.test.add.opens} + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.immutables + value + + + + https://github.com/HubSpot/jinjava + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + jaredstehler @@ -248,6 +372,16 @@ scm:git:git@github.com:HubSpot/jinjava.git scm:git:git@github.com:HubSpot/jinjava.git git@github.com:HubSpot/jinjava.git - HEAD + HEAD$ + + + + basepom.central-release + + true + true + + + diff --git a/spotbugs-exclude-filter.xml b/spotbugs-exclude-filter.xml new file mode 100644 index 000000000..94e88a3c1 --- /dev/null +++ b/spotbugs-exclude-filter.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/hubspot/jinjava/Jinjava.java b/src/main/java/com/hubspot/jinjava/Jinjava.java index f2a9878a6..a5d834b48 100644 --- a/src/main/java/com/hubspot/jinjava/Jinjava.java +++ b/src/main/java/com/hubspot/jinjava/Jinjava.java @@ -61,6 +61,7 @@ * @author jstehler */ public class Jinjava { + private ExpressionFactory expressionFactory; private ExpressionFactory eagerExpressionFactory; private ResourceLocator resourceLocator; @@ -72,7 +73,7 @@ public class Jinjava { * Create a new Jinjava processor instance with the default global config */ public Jinjava() { - this(new JinjavaConfig()); + this(JinjavaConfig.builder().build()); } /** @@ -230,10 +231,10 @@ public RenderResult renderForResult( JinjavaInterpreter parentInterpreter = JinjavaInterpreter.getCurrent(); if (parentInterpreter != null) { renderConfig = parentInterpreter.getConfig(); - Map bindingsWithParentContext = new HashMap<>(bindings); - if (parentInterpreter.getContext() != null) { - bindingsWithParentContext.putAll(parentInterpreter.getContext()); - } + Map bindingsWithParentContext = createBindingsWithParentContext( + bindings, + parentInterpreter.getContext() + ); context = new Context( copyGlobalContext(), @@ -244,52 +245,51 @@ public RenderResult renderForResult( context = new Context(copyGlobalContext(), bindings, renderConfig.getDisabled()); } - JinjavaInterpreter interpreter = globalConfig - .getInterpreterFactory() - .newInstance(this, context, renderConfig); - JinjavaInterpreter.pushCurrent(interpreter); - try { - String result = interpreter.render(template); - return new RenderResult( - result, - interpreter.getContext(), - interpreter.getErrorsCopy() - ); - } catch (InterpretException e) { - if (e instanceof TemplateSyntaxException) { + JinjavaInterpreter interpreter = globalConfig + .getInterpreterFactory() + .newInstance(this, context, renderConfig); + try { + String result = interpreter.render(template); + return new RenderResult( + result, + interpreter.getContext(), + interpreter.getErrorsCopy() + ); + } catch (InterpretException e) { + if (e instanceof TemplateSyntaxException) { + return new RenderResult( + TemplateError.fromException((TemplateSyntaxException) e), + interpreter.getContext(), + interpreter.getErrorsCopy() + ); + } + return new RenderResult( + TemplateError.fromSyntaxError(e), + interpreter.getContext(), + interpreter.getErrorsCopy() + ); + } catch (InvalidArgumentException e) { return new RenderResult( - TemplateError.fromException((TemplateSyntaxException) e), + TemplateError.fromInvalidArgumentException(e), + interpreter.getContext(), + interpreter.getErrorsCopy() + ); + } catch (InvalidInputException e) { + return new RenderResult( + TemplateError.fromInvalidInputException(e), + interpreter.getContext(), + interpreter.getErrorsCopy() + ); + } catch (Exception e) { + return new RenderResult( + TemplateError.fromException(e), interpreter.getContext(), interpreter.getErrorsCopy() ); } - return new RenderResult( - TemplateError.fromSyntaxError(e), - interpreter.getContext(), - interpreter.getErrorsCopy() - ); - } catch (InvalidArgumentException e) { - return new RenderResult( - TemplateError.fromInvalidArgumentException(e), - interpreter.getContext(), - interpreter.getErrorsCopy() - ); - } catch (InvalidInputException e) { - return new RenderResult( - TemplateError.fromInvalidInputException(e), - interpreter.getContext(), - interpreter.getErrorsCopy() - ); - } catch (Exception e) { - return new RenderResult( - TemplateError.fromException(e), - interpreter.getContext(), - interpreter.getErrorsCopy() - ); } finally { globalContext.reset(); - JinjavaInterpreter.popCurrent(); } } @@ -320,6 +320,17 @@ public void registerExpTest(ExpTest t) { globalContext.registerExpTest(t); } + protected Map createBindingsWithParentContext( + Map bindings, + Map bindingsFromParentContext + ) { + Map bindingsWithParentContext = new HashMap<>(bindings); + if (bindingsFromParentContext != null) { + bindingsWithParentContext.putAll(bindingsFromParentContext); + } + return bindingsWithParentContext; + } + private Context copyGlobalContext() { Context context = new Context(null, globalContext); // copy registered. diff --git a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java index ad527458c..0ef3c53d6 100644 --- a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java +++ b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java @@ -18,417 +18,255 @@ 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.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.el.JinjavaInterpreterResolver; -import com.hubspot.jinjava.interpret.Context; +import com.hubspot.jinjava.el.JinjavaObjectUnwrapper; +import com.hubspot.jinjava.el.JinjavaProcessors; +import com.hubspot.jinjava.el.ObjectUnwrapper; +import com.hubspot.jinjava.el.ext.AllowlistMethodValidator; +import com.hubspot.jinjava.el.ext.AllowlistReturnTypeValidator; +import com.hubspot.jinjava.features.FeatureConfig; +import com.hubspot.jinjava.features.Features; 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.objects.date.CurrentDateTimeProvider; +import com.hubspot.jinjava.objects.date.DateTimeProvider; 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; import java.nio.charset.StandardCharsets; import java.time.ZoneId; import java.time.ZoneOffset; -import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; import javax.el.ELResolver; - +import org.immutables.value.Value; + +@Value.Immutable(singleton = true) +@JinjavaImmutableStyle.WithStyle +@Value.Style( + init = "with*", + get = { "is*", "get*" }, // Detect 'get' and 'is' prefixes in accessor methods + build = "buildImpl", // This is an alias for keeping binary compatibility on the "build" method. + visibility = Value.Style.ImplementationVisibility.PACKAGE +) public class JinjavaConfig { - private final Charset charset; - private final Locale locale; - private final ZoneId timeZone; - private final int maxRenderDepth; - private final long maxOutputSize; - - private final boolean trimBlocks; - private final boolean lstripBlocks; - - private final boolean enableRecursiveMacroCalls; - private final int maxMacroRecursionDepth; - - private final Map> disabled; - private final boolean failOnUnknownTokens; - private final boolean nestedInterpretationEnabled; - private final RandomNumberGeneratorStrategy randomNumberGenerator; - private final boolean validationMode; - private final long maxStringLength; - private final int maxListSize; - private final int maxMapSize; - private final int rangeLimit; - private final int maxNumDeferredTokens; - private final InterpreterFactory interpreterFactory; - private TokenScannerSymbols tokenScannerSymbols; - private final ELResolver elResolver; - private final ExecutionMode executionMode; - private final LegacyOverrides legacyOverrides; - private final boolean enablePreciseDivideFilter; - private final ObjectMapper objectMapper; - public static Builder newBuilder() { - return new Builder(); - } - - public JinjavaConfig() { - this(newBuilder()); - } - - public JinjavaConfig(InterpreterFactory interpreterFactory) { - this(newBuilder().withInterperterFactory(interpreterFactory)); - } - - public JinjavaConfig( - Charset charset, - Locale locale, - ZoneId timeZone, - int maxRenderDepth - ) { - this( - newBuilder() - .withCharset(charset) - .withLocale(locale) - .withTimeZone(timeZone) - .withMaxRenderDepth(maxRenderDepth) - ); - } - - private JinjavaConfig(Builder builder) { - charset = builder.charset; - locale = builder.locale; - timeZone = builder.timeZone; - maxRenderDepth = builder.maxRenderDepth; - disabled = builder.disabled; - trimBlocks = builder.trimBlocks; - lstripBlocks = builder.lstripBlocks; - enableRecursiveMacroCalls = builder.enableRecursiveMacroCalls; - maxMacroRecursionDepth = builder.maxMacroRecursionDepth; - failOnUnknownTokens = builder.failOnUnknownTokens; - maxOutputSize = builder.maxOutputSize; - nestedInterpretationEnabled = builder.nestedInterpretationEnabled; - randomNumberGenerator = builder.randomNumberGeneratorStrategy; - validationMode = builder.validationMode; - maxStringLength = builder.maxStringLength; - maxListSize = builder.maxListSize; - maxMapSize = builder.maxMapSize; - rangeLimit = builder.rangeLimit; - maxNumDeferredTokens = builder.maxNumDeferredTokens; - interpreterFactory = builder.interpreterFactory; - tokenScannerSymbols = builder.tokenScannerSymbols; - elResolver = builder.elResolver; - executionMode = builder.executionMode; - legacyOverrides = builder.legacyOverrides; - enablePreciseDivideFilter = builder.enablePreciseDivideFilter; - objectMapper = builder.objectMapper; - } + public JinjavaConfig() {} + @Value.Default public Charset getCharset() { - return charset; + return StandardCharsets.UTF_8; } + @Value.Default public Locale getLocale() { - return locale; + return Locale.ENGLISH; } + @Value.Default public ZoneId getTimeZone() { - return timeZone; + return ZoneOffset.UTC; } + @Value.Default public int getMaxRenderDepth() { - return maxRenderDepth; + return 10; } + @Value.Default public long getMaxOutputSize() { - return maxOutputSize; + return 0; } - public int getMaxListSize() { - return maxListSize; + @Value.Default + public boolean isTrimBlocks() { + return false; } - public int getMaxMapSize() { - return maxMapSize; + @Value.Default + public boolean isLstripBlocks() { + return false; } - public int getRangeLimit() { - return rangeLimit; + @Value.Default + public boolean isEnableRecursiveMacroCalls() { + return false; } - public int getMaxNumDeferredTokens() { - return maxNumDeferredTokens; + @Value.Default + public int getMaxMacroRecursionDepth() { + return 0; } - public RandomNumberGeneratorStrategy getRandomNumberGeneratorStrategy() { - return randomNumberGenerator; + @Value.Default + public Map> getDisabled() { + return ImmutableMap.of(); } - public boolean isTrimBlocks() { - return trimBlocks; + @Value.Default + public boolean isFailOnUnknownTokens() { + return false; } - public boolean isLstripBlocks() { - return lstripBlocks; + @Value.Default + public boolean isNestedInterpretationEnabled() { + return false; // Default changed to false in 3.0 } - public boolean isEnableRecursiveMacroCalls() { - return enableRecursiveMacroCalls; + @Value.Default + public RandomNumberGeneratorStrategy getRandomNumberGeneratorStrategy() { + return RandomNumberGeneratorStrategy.THREAD_LOCAL; } - public int getMaxMacroRecursionDepth() { - return maxMacroRecursionDepth; + @Value.Default + public boolean isValidationMode() { + return false; } - public Map> getDisabled() { - return disabled; + @Value.Default + public long getMaxStringLength() { + return getMaxOutputSize(); } - public boolean isFailOnUnknownTokens() { - return failOnUnknownTokens; + @Value.Default + public int getMaxListSize() { + return Integer.MAX_VALUE; } - public boolean isNestedInterpretationEnabled() { - return nestedInterpretationEnabled; + @Value.Default + public int getMaxMapSize() { + return Integer.MAX_VALUE; } - public boolean isValidationMode() { - return validationMode; + @Value.Default + public int getRangeLimit() { + return DEFAULT_RANGE_LIMIT; } - public long getMaxStringLength() { - return maxStringLength; + @Value.Default + public int getMaxNumDeferredTokens() { + return 1000; } + @Value.Default public InterpreterFactory getInterpreterFactory() { - return interpreterFactory; + return new JinjavaInterpreterFactory(); } + @Value.Default + public DateTimeProvider getDateTimeProvider() { + return new CurrentDateTimeProvider(); + } + + @Value.Default public TokenScannerSymbols getTokenScannerSymbols() { - return tokenScannerSymbols; + return new DefaultTokenScannerSymbols(); } - public void setTokenScannerSymbols(TokenScannerSymbols tokenScannerSymbols) { - this.tokenScannerSymbols = tokenScannerSymbols; + @Value.Default + public AllowlistMethodValidator getMethodValidator() { + return AllowlistMethodValidator.DEFAULT; } - public ELResolver getElResolver() { - return elResolver; + @Value.Default + public AllowlistReturnTypeValidator getReturnTypeValidator() { + return AllowlistReturnTypeValidator.DEFAULT; } - public ObjectMapper getObjectMapper() { - return objectMapper; + @Value.Default + public ELResolver getElResolver() { + return isDefaultReadOnlyResolver() + ? JinjavaInterpreterResolver.DEFAULT_RESOLVER_READ_ONLY + : JinjavaInterpreterResolver.DEFAULT_RESOLVER_READ_WRITE; } - /** - * @deprecated Replaced by {@link LegacyOverrides#isIterateOverMapKeys()} - */ - @Deprecated - public boolean isIterateOverMapKeys() { - return legacyOverrides.isIterateOverMapKeys(); + @Value.Default + public boolean isDefaultReadOnlyResolver() { + return true; } + @Value.Default public ExecutionMode getExecutionMode() { - return executionMode; + return DefaultExecutionMode.instance(); } + @Value.Default public LegacyOverrides getLegacyOverrides() { - return legacyOverrides; + return LegacyOverrides.THREE_POINT_0; } + @Value.Default public boolean getEnablePreciseDivideFilter() { - return enablePreciseDivideFilter; - } - - public static class Builder { - private Charset charset = StandardCharsets.UTF_8; - private Locale locale = Locale.ENGLISH; - private ZoneId timeZone = ZoneOffset.UTC; - private int maxRenderDepth = 10; - private long maxOutputSize = 0; // in bytes - private Map> disabled = new HashMap<>(); - - private boolean trimBlocks; - private boolean lstripBlocks; - - private boolean enableRecursiveMacroCalls; - private int maxMacroRecursionDepth; - private boolean failOnUnknownTokens; - private boolean nestedInterpretationEnabled = true; - private RandomNumberGeneratorStrategy randomNumberGeneratorStrategy = - RandomNumberGeneratorStrategy.THREAD_LOCAL; - private boolean validationMode = false; - private long maxStringLength = 0; - private int rangeLimit = DEFAULT_RANGE_LIMIT; - private int maxNumDeferredTokens = 1000; - private InterpreterFactory interpreterFactory = new JinjavaInterpreterFactory(); - private TokenScannerSymbols tokenScannerSymbols = new DefaultTokenScannerSymbols(); - private ELResolver elResolver = JinjavaInterpreterResolver.DEFAULT_RESOLVER_READ_ONLY; - private int maxListSize = Integer.MAX_VALUE; - private int maxMapSize = Integer.MAX_VALUE; - private ExecutionMode executionMode = DefaultExecutionMode.instance(); - private LegacyOverrides legacyOverrides = LegacyOverrides.NONE; - private boolean enablePreciseDivideFilter = false; - private ObjectMapper objectMapper = new ObjectMapper(); - - private Builder() {} - - public Builder withCharset(Charset charset) { - this.charset = charset; - return this; - } - - public Builder withLocale(Locale locale) { - this.locale = locale; - return this; - } - - public Builder withTimeZone(ZoneId timeZone) { - this.timeZone = timeZone; - return this; - } - - public Builder withDisabled(Map> disabled) { - this.disabled = disabled; - return this; - } - - public Builder withMaxRenderDepth(int maxRenderDepth) { - this.maxRenderDepth = maxRenderDepth; - return this; - } - - public Builder withRandomNumberGeneratorStrategy( - RandomNumberGeneratorStrategy randomNumberGeneratorStrategy - ) { - this.randomNumberGeneratorStrategy = randomNumberGeneratorStrategy; - return this; - } - - public Builder withTrimBlocks(boolean trimBlocks) { - this.trimBlocks = trimBlocks; - return this; - } - - public Builder withLstripBlocks(boolean lstripBlocks) { - this.lstripBlocks = lstripBlocks; - return this; - } - - public Builder withEnableRecursiveMacroCalls(boolean enableRecursiveMacroCalls) { - this.enableRecursiveMacroCalls = enableRecursiveMacroCalls; - return this; - } - - public Builder withMaxMacroRecursionDepth(int maxMacroRecursionDepth) { - this.maxMacroRecursionDepth = maxMacroRecursionDepth; - return this; - } - - public Builder withReadOnlyResolver(boolean readOnlyResolver) { - this.elResolver = - readOnlyResolver - ? JinjavaInterpreterResolver.DEFAULT_RESOLVER_READ_ONLY - : JinjavaInterpreterResolver.DEFAULT_RESOLVER_READ_WRITE; - return this; - } - - public Builder withElResolver(ELResolver elResolver) { - this.elResolver = elResolver; - return this; - } - - public Builder withFailOnUnknownTokens(boolean failOnUnknownTokens) { - this.failOnUnknownTokens = failOnUnknownTokens; - return this; - } - - public Builder withMaxOutputSize(long maxOutputSize) { - this.maxOutputSize = maxOutputSize; - return this; - } - - public Builder withNestedInterpretationEnabled(boolean nestedInterpretationEnabled) { - this.nestedInterpretationEnabled = nestedInterpretationEnabled; - return this; - } - - public Builder withValidationMode(boolean validationMode) { - this.validationMode = validationMode; - return this; - } - - public Builder withMaxStringLength(long maxStringLength) { - this.maxStringLength = maxStringLength; - return this; - } + return false; + } - public Builder withMaxListSize(int maxListSize) { - this.maxListSize = maxListSize; - return this; - } + @Value.Default + public boolean isEnableFilterChainOptimization() { + return false; + } - public Builder withMaxMapSize(int maxMapSize) { - this.maxMapSize = maxMapSize; - return this; + @Value.Default + public ObjectMapper getObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper().registerModule(new Jdk8Module()); + if (getLegacyOverrides().isUseSnakeCasePropertyNaming()) { + objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); } + return objectMapper; + } - public Builder withRangeLimit(int rangeLimit) { - this.rangeLimit = rangeLimit; - return this; - } + @Value.Default + public ObjectUnwrapper getObjectUnwrapper() { + return new JinjavaObjectUnwrapper(); + } - public Builder withMaxNumDeferredTokens(int maxNumDeferredTokens) { - this.maxNumDeferredTokens = maxNumDeferredTokens; - return this; - } + @Value.Derived + public Features getFeatures() { + return new Features(getFeatureConfig()); + } - public Builder withInterperterFactory(InterpreterFactory interperterFactory) { - this.interpreterFactory = interperterFactory; - return this; - } + @Value.Default + public FeatureConfig getFeatureConfig() { + return FeatureConfig.newBuilder().build(); + } - public Builder withTokenScannerSymbols(TokenScannerSymbols tokenScannerSymbols) { - this.tokenScannerSymbols = tokenScannerSymbols; - return this; - } + @Value.Default + public JinjavaProcessors getProcessors() { + return JinjavaProcessors.newBuilder().build(); + } - /** - * @deprecated Replaced by {@link LegacyOverrides.Builder#withIterateOverMapKeys(boolean)}} - */ - @Deprecated - public Builder withIterateOverMapKeys(boolean iterateOverMapKeys) { - return withLegacyOverrides( - LegacyOverrides - .Builder.from(legacyOverrides) - .withIterateOverMapKeys(iterateOverMapKeys) - .build() - ); - } + @Deprecated + public BiConsumer getNodePreProcessor() { + return getProcessors().getNodePreProcessor(); + } - public Builder withExecutionMode(ExecutionMode executionMode) { - this.executionMode = executionMode; - return this; - } + @Deprecated + public boolean isIterateOverMapKeys() { + return getLegacyOverrides().isIterateOverMapKeys(); + } - public Builder withLegacyOverrides(LegacyOverrides legacyOverrides) { - this.legacyOverrides = legacyOverrides; - return this; - } + public static class Builder extends ImmutableJinjavaConfig.Builder { - public Builder withEnablePreciseDivideFilter(boolean enablePreciseDivideFilter) { - this.enablePreciseDivideFilter = enablePreciseDivideFilter; - return this; + public JinjavaConfig build() { + return super.buildImpl(); } + } - public Builder withObjectMapper(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - return this; - } + public static Builder builder() { + return new Builder(); + } - public JinjavaConfig build() { - return new JinjavaConfig(this); - } + public static Builder newBuilder() { + return builder(); } } diff --git a/src/main/java/com/hubspot/jinjava/JinjavaImmutableStyle.java b/src/main/java/com/hubspot/jinjava/JinjavaImmutableStyle.java new file mode 100644 index 000000000..4a38d3789 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/JinjavaImmutableStyle.java @@ -0,0 +1,16 @@ +package com.hubspot.jinjava; + +import org.immutables.value.Value; + +@Value.Style( + init = "set*", + get = { "is*", "get*" } // Detect 'get' and 'is' prefixes in accessor methods +) +public @interface JinjavaImmutableStyle { + @Value.Style( + init = "with*", + get = { "is*", "get*" } // Detect 'get' and 'is' prefixes in accessor methods + ) + @interface WithStyle { + } +} diff --git a/src/main/java/com/hubspot/jinjava/LegacyOverrides.java b/src/main/java/com/hubspot/jinjava/LegacyOverrides.java index 9fb46b03d..be16ae950 100644 --- a/src/main/java/com/hubspot/jinjava/LegacyOverrides.java +++ b/src/main/java/com/hubspot/jinjava/LegacyOverrides.java @@ -3,23 +3,57 @@ /** * This class allows Jinjava to be configured to override legacy behaviour. * LegacyOverrides.NONE signifies that none of the legacy functionality will be overridden. + * LegacyOverrides.ALL signifies that all new functionality will be used; avoid legacy "bugs". */ public class LegacyOverrides { + public static final LegacyOverrides NONE = new LegacyOverrides.Builder().build(); + public static final LegacyOverrides THREE_POINT_0 = new Builder() + .withEvaluateMapKeys(true) + .withIterateOverMapKeys(true) + .withUsePyishObjectMapper(true) + .withUseSnakeCasePropertyNaming(true) + .withUseNaturalOperatorPrecedence(true) + .withParseWhitespaceControlStrictly(true) + .withAllowAdjacentTextNodes(true) + .withUseTrimmingForNotesAndExpressions(true) + .withKeepNullableLoopValues(true) + .withIteratorOnlyReverseFilter(true) + .build(); + public static final LegacyOverrides ALL = new LegacyOverrides.Builder() + .withEvaluateMapKeys(true) + .withIterateOverMapKeys(true) + .withUsePyishObjectMapper(true) + .withUseSnakeCasePropertyNaming(true) + .withUseNaturalOperatorPrecedence(true) + .withParseWhitespaceControlStrictly(true) + .withAllowAdjacentTextNodes(true) + .withUseTrimmingForNotesAndExpressions(true) + .withKeepNullableLoopValues(true) + .withIteratorOnlyReverseFilter(true) + .build(); private final boolean evaluateMapKeys; private final boolean iterateOverMapKeys; private final boolean usePyishObjectMapper; - private final boolean whitespaceRequiredWithinTokens; + private final boolean useSnakeCasePropertyNaming; private final boolean useNaturalOperatorPrecedence; private final boolean parseWhitespaceControlStrictly; + private final boolean allowAdjacentTextNodes; + private final boolean useTrimmingForNotesAndExpressions; + private final boolean keepNullableLoopValues; + private final boolean iteratorOnlyReverseFilter; private LegacyOverrides(Builder builder) { evaluateMapKeys = builder.evaluateMapKeys; iterateOverMapKeys = builder.iterateOverMapKeys; usePyishObjectMapper = builder.usePyishObjectMapper; - whitespaceRequiredWithinTokens = builder.whitespaceRequiredWithinTokens; + useSnakeCasePropertyNaming = builder.useSnakeCasePropertyNaming; useNaturalOperatorPrecedence = builder.useNaturalOperatorPrecedence; parseWhitespaceControlStrictly = builder.parseWhitespaceControlStrictly; + allowAdjacentTextNodes = builder.allowAdjacentTextNodes; + useTrimmingForNotesAndExpressions = builder.useTrimmingForNotesAndExpressions; + keepNullableLoopValues = builder.keepNullableLoopValues; + iteratorOnlyReverseFilter = builder.iteratorOnlyReverseFilter; } public static Builder newBuilder() { @@ -38,8 +72,8 @@ public boolean isUsePyishObjectMapper() { return usePyishObjectMapper; } - public boolean isWhitespaceRequiredWithinTokens() { - return whitespaceRequiredWithinTokens; + public boolean isUseSnakeCasePropertyNaming() { + return useSnakeCasePropertyNaming; } public boolean isUseNaturalOperatorPrecedence() { @@ -50,13 +84,34 @@ public boolean isParseWhitespaceControlStrictly() { return parseWhitespaceControlStrictly; } + public boolean isAllowAdjacentTextNodes() { + return allowAdjacentTextNodes; + } + + public boolean isUseTrimmingForNotesAndExpressions() { + return useTrimmingForNotesAndExpressions; + } + + public boolean isKeepNullableLoopValues() { + return keepNullableLoopValues; + } + + public boolean isIteratorOnlyReverseFilter() { + return iteratorOnlyReverseFilter; + } + public static class Builder { + private boolean evaluateMapKeys = false; private boolean iterateOverMapKeys = false; private boolean usePyishObjectMapper = false; - private boolean whitespaceRequiredWithinTokens = false; + private boolean useSnakeCasePropertyNaming = false; private boolean useNaturalOperatorPrecedence = false; private boolean parseWhitespaceControlStrictly = false; + private boolean allowAdjacentTextNodes = false; + private boolean useTrimmingForNotesAndExpressions = false; + private boolean keepNullableLoopValues = false; + private boolean iteratorOnlyReverseFilter = false; private Builder() {} @@ -69,13 +124,17 @@ public static Builder from(LegacyOverrides legacyOverrides) { .withEvaluateMapKeys(legacyOverrides.evaluateMapKeys) .withIterateOverMapKeys(legacyOverrides.iterateOverMapKeys) .withUsePyishObjectMapper(legacyOverrides.usePyishObjectMapper) - .withWhitespaceRequiredWithinTokens( - legacyOverrides.whitespaceRequiredWithinTokens - ) + .withUseSnakeCasePropertyNaming(legacyOverrides.useSnakeCasePropertyNaming) .withUseNaturalOperatorPrecedence(legacyOverrides.useNaturalOperatorPrecedence) .withParseWhitespaceControlStrictly( legacyOverrides.parseWhitespaceControlStrictly - ); + ) + .withAllowAdjacentTextNodes(legacyOverrides.allowAdjacentTextNodes) + .withUseTrimmingForNotesAndExpressions( + legacyOverrides.useTrimmingForNotesAndExpressions + ) + .withKeepNullableLoopValues(legacyOverrides.keepNullableLoopValues) + .withIteratorOnlyReverseFilter(legacyOverrides.iteratorOnlyReverseFilter); } public Builder withEvaluateMapKeys(boolean evaluateMapKeys) { @@ -93,10 +152,8 @@ public Builder withUsePyishObjectMapper(boolean usePyishObjectMapper) { return this; } - public Builder withWhitespaceRequiredWithinTokens( - boolean whitespaceRequiredWithinTokens - ) { - this.whitespaceRequiredWithinTokens = whitespaceRequiredWithinTokens; + public Builder withUseSnakeCasePropertyNaming(boolean useSnakeCasePropertyNaming) { + this.useSnakeCasePropertyNaming = useSnakeCasePropertyNaming; return this; } @@ -113,5 +170,27 @@ public Builder withParseWhitespaceControlStrictly( this.parseWhitespaceControlStrictly = parseWhitespaceControlStrictly; return this; } + + public Builder withAllowAdjacentTextNodes(boolean allowAdjacentTextNodes) { + this.allowAdjacentTextNodes = allowAdjacentTextNodes; + return this; + } + + public Builder withUseTrimmingForNotesAndExpressions( + boolean useTrimmingForNotesAndExpressions + ) { + this.useTrimmingForNotesAndExpressions = useTrimmingForNotesAndExpressions; + return this; + } + + public Builder withKeepNullableLoopValues(boolean keepNullableLoopValues) { + this.keepNullableLoopValues = keepNullableLoopValues; + return this; + } + + public Builder withIteratorOnlyReverseFilter(boolean iteratorOnlyReverseFilter) { + this.iteratorOnlyReverseFilter = iteratorOnlyReverseFilter; + return this; + } } } diff --git a/src/main/java/com/hubspot/jinjava/doc/JinjavaDoc.java b/src/main/java/com/hubspot/jinjava/doc/JinjavaDoc.java index ef4bb042b..2b03a8728 100644 --- a/src/main/java/com/hubspot/jinjava/doc/JinjavaDoc.java +++ b/src/main/java/com/hubspot/jinjava/doc/JinjavaDoc.java @@ -4,6 +4,7 @@ import java.util.TreeMap; public class JinjavaDoc { + private final Map expTests = new TreeMap<>(); private final Map filters = new TreeMap<>(); private final Map functions = new TreeMap<>(); diff --git a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocFactory.java b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocFactory.java index a937d7afd..fcb7b14cc 100644 --- a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocFactory.java +++ b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocFactory.java @@ -21,13 +21,12 @@ import org.slf4j.LoggerFactory; public class JinjavaDocFactory { + private static final Logger LOG = LoggerFactory.getLogger(JinjavaDocFactory.class); private static final Class JINJAVA_DOC_CLASS = com.hubspot.jinjava.doc.annotations.JinjavaDoc.class; - private static final String GUICE_CLASS_INDICATOR = "$$EnhancerByGuice$$"; - private final Jinjava jinjava; public JinjavaDocFactory(Jinjava jinjava) { @@ -63,15 +62,38 @@ 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( + "Tag {} doesn't have a @{} annotation", + tag.getName(), + JINJAVA_DOC_CLASS.getName() + ); + doc.addTag( + new JinjavaDocTag( + tag.getName(), + StringUtils.isBlank(tag.getEndTagName()), + "", + "", + false, + new JinjavaDocParam[] {}, + new JinjavaDocParam[] {}, + new JinjavaDocSnippet[] {}, + Collections.emptyMap() + ) + ); + } else if (!docAnnotation.hidden()) { + doc.addCodeSnippet(tag.getName(), getTagSnippet(tag)); + } } } private void addExpTests(JinjavaDoc doc) { for (ExpTest t : jinjava.getGlobalContextCopy().getAllExpTests()) { - com.hubspot.jinjava.doc.annotations.JinjavaDoc docAnnotation = getJinjavaDocAnnotation( - t.getClass() - ); + com.hubspot.jinjava.doc.annotations.JinjavaDoc docAnnotation = + getJinjavaDocAnnotation(t.getClass()); if (docAnnotation == null) { LOG.warn( @@ -110,9 +132,8 @@ private void addExpTests(JinjavaDoc doc) { private void addFilterDocs(JinjavaDoc doc) { for (Filter f : jinjava.getGlobalContextCopy().getAllFilters()) { - com.hubspot.jinjava.doc.annotations.JinjavaDoc docAnnotation = getJinjavaDocAnnotation( - f.getClass() - ); + com.hubspot.jinjava.doc.annotations.JinjavaDoc docAnnotation = + getJinjavaDocAnnotation(f.getClass()); if (docAnnotation == null) { LOG.warn( @@ -167,9 +188,8 @@ private void addFnDocs(JinjavaDoc doc) { } } - com.hubspot.jinjava.doc.annotations.JinjavaDoc docAnnotation = realMethod.getAnnotation( - com.hubspot.jinjava.doc.annotations.JinjavaDoc.class - ); + com.hubspot.jinjava.doc.annotations.JinjavaDoc docAnnotation = + realMethod.getAnnotation(com.hubspot.jinjava.doc.annotations.JinjavaDoc.class); if (docAnnotation == null) { LOG.warn( @@ -212,9 +232,8 @@ private void addTagDocs(JinjavaDoc doc) { if (t instanceof EndTag) { continue; } - com.hubspot.jinjava.doc.annotations.JinjavaDoc docAnnotation = getJinjavaDocAnnotation( - t.getClass() - ); + com.hubspot.jinjava.doc.annotations.JinjavaDoc docAnnotation = + getJinjavaDocAnnotation(t.getClass()); if (docAnnotation == null) { LOG.warn( @@ -295,11 +314,7 @@ private Map extractMeta(JinjavaMetaValue[] metaValues) { private com.hubspot.jinjava.doc.annotations.JinjavaDoc getJinjavaDocAnnotation( Class clazz ) { - if ( - clazz.getName().contains(GUICE_CLASS_INDICATOR) && clazz.getSuperclass() != null - ) { - clazz = clazz.getSuperclass(); - } + clazz = InjectedContextFunctionProxy.removeGuiceWrapping(clazz); return clazz.getAnnotation(com.hubspot.jinjava.doc.annotations.JinjavaDoc.class); } @@ -311,9 +326,8 @@ private String getTagSnippet(Tag tag) { if (annotation != null) { return annotation.code(); } - com.hubspot.jinjava.doc.annotations.JinjavaDoc docAnnotation = getJinjavaDocAnnotation( - tag.getClass() - ); + com.hubspot.jinjava.doc.annotations.JinjavaDoc docAnnotation = + getJinjavaDocAnnotation(tag.getClass()); StringBuilder snippet = new StringBuilder("{% "); snippet.append(tag.getName()); int i = 1; @@ -330,7 +344,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")) { diff --git a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocItem.java b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocItem.java index f7212a1f8..543957ad2 100644 --- a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocItem.java +++ b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocItem.java @@ -3,6 +3,7 @@ import java.util.Map; public abstract class JinjavaDocItem { + private final String name; private final String desc; private final String aliasOf; diff --git a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocParam.java b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocParam.java index 863b6cd0b..8ff38dacb 100644 --- a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocParam.java +++ b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocParam.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.doc; public class JinjavaDocParam { + private final String name; private final String type; private final String desc; diff --git a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocSnippet.java b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocSnippet.java index c673352c5..78ba4c7f5 100644 --- a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocSnippet.java +++ b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocSnippet.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.doc; public class JinjavaDocSnippet { + private final String desc; private final String code; private final String output; diff --git a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocTag.java b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocTag.java index e2ca84ce0..39ae25d96 100644 --- a/src/main/java/com/hubspot/jinjava/doc/JinjavaDocTag.java +++ b/src/main/java/com/hubspot/jinjava/doc/JinjavaDocTag.java @@ -3,6 +3,7 @@ import java.util.Map; public class JinjavaDocTag extends JinjavaDocItem { + private final boolean empty; public JinjavaDocTag( diff --git a/src/main/java/com/hubspot/jinjava/doc/annotations/JinjavaDoc.java b/src/main/java/com/hubspot/jinjava/doc/annotations/JinjavaDoc.java index 880fa9d80..36dbd278b 100644 --- a/src/main/java/com/hubspot/jinjava/doc/annotations/JinjavaDoc.java +++ b/src/main/java/com/hubspot/jinjava/doc/annotations/JinjavaDoc.java @@ -1,12 +1,14 @@ package com.hubspot.jinjava.doc.annotations; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) +@Inherited public @interface JinjavaDoc { String value() default ""; diff --git a/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java b/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java index cbc0fa723..aaab4a4c6 100644 --- a/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java @@ -13,7 +13,7 @@ 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.OutputTooBigException; import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; @@ -22,10 +22,12 @@ import com.hubspot.jinjava.interpret.UnknownTokenException; import com.hubspot.jinjava.interpret.errorcategory.BasicTemplateErrorCategory; import com.hubspot.jinjava.lib.fn.ELFunctionDefinition; +import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.util.WhitespaceUtils; import de.odysseus.el.tree.TreeBuilderException; import java.util.Arrays; import java.util.List; +import java.util.Objects; import javax.el.ELException; import javax.el.ExpressionFactory; import javax.el.PropertyNotFoundException; @@ -36,10 +38,12 @@ * Resolves Jinja expressions. */ public class ExpressionResolver { + private final JinjavaInterpreter interpreter; private final ExpressionFactory expressionFactory; - private final JinjavaInterpreterResolver resolver; + private final ReturnTypeValidatingJinjavaInterpreterResolver resolver; private final JinjavaELContext elContext; + private final ObjectUnwrapper objectUnwrapper; private static final String EXPRESSION_START_TOKEN = "#{"; private static final String EXPRESSION_END_TOKEN = "}"; @@ -51,11 +55,16 @@ public ExpressionResolver(JinjavaInterpreter interpreter, Jinjava jinjava) { ? jinjava.getEagerExpressionFactory() : jinjava.getExpressionFactory(); - this.resolver = new JinjavaInterpreterResolver(interpreter); + this.resolver = + new ReturnTypeValidatingJinjavaInterpreterResolver( + interpreter.getConfig().getReturnTypeValidator(), + new JinjavaInterpreterResolver(interpreter) + ); this.elContext = new JinjavaELContext(interpreter, resolver); for (ELFunctionDefinition fn : jinjava.getGlobalContext().getAllFunctions()) { this.elContext.setFunction(fn.getNamespace(), fn.getLocalName(), fn.getMethod()); } + objectUnwrapper = interpreter.getConfig().getObjectUnwrapper(); } /** @@ -65,19 +74,43 @@ public ExpressionResolver(JinjavaInterpreter interpreter, Jinjava jinjava) { * @return Value of expression. */ public Object resolveExpression(String expression) { + return resolveExpression(expression, true); + } + + /** + * 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; } expression = expression.trim(); - interpreter.getContext().addResolvedExpression(expression); - - if (WhitespaceUtils.isWrappedWith(expression, "[", "]")) { - Arrays - .stream(expression.substring(1, expression.length() - 1).split(",")) - .forEach( - substring -> interpreter.getContext().addResolvedExpression(substring.trim()) - ); + if (addToResolvedExpressions) { + if (WhitespaceUtils.isWrappedWith(expression, "[", "]")) { + String commaSeparatedExpress = expression + .substring(1, expression.length() - 1) + .trim(); + // Over-simplified way to detect JSON format, avoid to break it for adding resolved expressions. + if ( + !commaSeparatedExpress.startsWith("{") || !commaSeparatedExpress.endsWith("}") + ) { + Arrays + .stream(commaSeparatedExpress.split(",")) + .forEach(substring -> + interpreter.getContext().addResolvedExpression(substring.trim()) + ); + } + } + interpreter.getContext().addResolvedExpression(expression); } + try { String elExpression = EXPRESSION_START_TOKEN + expression + EXPRESSION_END_TOKEN; ValueExpression valueExp = expressionFactory.createValueExpression( @@ -94,10 +127,7 @@ public Object resolveExpression(String expression) { ); } - // resolve the LazyExpression supplier automatically - if (result instanceof LazyExpression) { - result = ((LazyExpression) result).get(); - } + result = objectUnwrapper.unwrapObject(result); validateResult(result); @@ -127,7 +157,10 @@ public Object resolveExpression(String expression) { TemplateError.fromException( new TemplateSyntaxException( expression.substring( - Math.max(e.getPosition() - EXPRESSION_START_TOKEN.length(), 0) + Math.min( + expression.length(), + Math.max(e.getPosition() - EXPRESSION_START_TOKEN.length(), 0) + ) ), "Error parsing '" + expression + "': " + errorMessage, interpreter.getLineNumber(), @@ -137,79 +170,7 @@ public Object resolveExpression(String expression) { ) ); } catch (ELException e) { - if (e.getCause() != null && e.getCause() instanceof DeferredValueException) { - throw (DeferredValueException) e.getCause(); - } - if (e.getCause() != null && e.getCause() instanceof TemplateSyntaxException) { - interpreter.addError( - TemplateError.fromException((TemplateSyntaxException) e.getCause()) - ); - } else if (e.getCause() != null && e.getCause() instanceof InvalidInputException) { - interpreter.addError( - TemplateError.fromInvalidInputException((InvalidInputException) e.getCause()) - ); - } else if ( - e.getCause() != null && e.getCause() instanceof InvalidArgumentException - ) { - interpreter.addError( - TemplateError.fromInvalidArgumentException( - (InvalidArgumentException) e.getCause() - ) - ); - } else if ( - e.getCause() != null && e.getCause() instanceof CollectionTooBigException - ) { - interpreter.addError( - new TemplateError( - ErrorType.FATAL, - ErrorReason.COLLECTION_TOO_BIG, - e.getCause().getMessage(), - null, - interpreter.getLineNumber(), - interpreter.getPosition(), - e - ) - ); - // rethrow because this is a hard limit and it will likely only happen in loops that we need to terminate - throw e; - } else if ( - e.getCause() != null && e.getCause() instanceof IndexOutOfRangeException - ) { - interpreter.addError( - new TemplateError( - ErrorType.WARNING, - ErrorReason.EXCEPTION, - ErrorItem.FUNCTION, - e.getMessage(), - null, - interpreter.getLineNumber(), - interpreter.getPosition(), - e - ) - ); - } else { - String originatingException = getRootCauseMessage(e); - final String combinedMessage = String.format( - "%s%nOriginating Exception:%n%s", - e.getMessage(), - originatingException - ); - interpreter.addError( - TemplateError.fromException( - new TemplateSyntaxException( - expression, - ( - e.getCause() == null || - StringUtils.endsWith(originatingException, e.getCause().getMessage()) - ) - ? e.getMessage() - : combinedMessage, - interpreter.getLineNumber(), - e - ) - ) - ); - } + handleELException(expression, e); } catch (DisabledException e) { interpreter.addError( new TemplateError( @@ -233,6 +194,20 @@ public Object resolveExpression(String expression) { interpreter.addError(TemplateError.fromInvalidInputException(e)); } catch (InvalidArgumentException e) { interpreter.addError(TemplateError.fromInvalidArgumentException(e)); + } catch (ArithmeticException e) { + interpreter.addError( + TemplateError.fromInvalidInputException( + new InvalidInputException( + interpreter, + ExpressionResolver.class.getName(), + String.format( + "ArithmeticException when resolving expression [%s]: " + + getRootCauseMessage(e), + expression + ) + ) + ) + ); } catch (Exception e) { interpreter.addError( TemplateError.fromException( @@ -252,6 +227,89 @@ public Object resolveExpression(String expression) { return null; } + private void handleELException(String expression, ELException e) { + if (e.getCause() != null && e.getCause() instanceof DeferredValueException) { + throw (DeferredValueException) e.getCause(); + } + if (e.getCause() != null && e.getCause() instanceof TemplateSyntaxException) { + interpreter.addError( + TemplateError.fromException((TemplateSyntaxException) e.getCause()) + ); + } else if (e.getCause() != null && e.getCause() instanceof InvalidInputException) { + interpreter.addError( + TemplateError.fromInvalidInputException((InvalidInputException) e.getCause()) + ); + } else if (e.getCause() != null && e.getCause() instanceof InvalidArgumentException) { + interpreter.addError( + TemplateError.fromInvalidArgumentException( + (InvalidArgumentException) e.getCause() + ) + ); + } else if ( + e.getCause() != null && e.getCause() instanceof CollectionTooBigException + ) { + interpreter.addError( + new TemplateError( + ErrorType.FATAL, + ErrorReason.COLLECTION_TOO_BIG, + e.getCause().getMessage(), + null, + interpreter.getLineNumber(), + interpreter.getPosition(), + e + ) + ); + // rethrow because this is a hard limit and it will likely only happen in loops that we need to terminate + throw e; + } else if (e.getCause() != null && e.getCause() instanceof IndexOutOfRangeException) { + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.EXCEPTION, + ErrorItem.FUNCTION, + e.getMessage(), + null, + interpreter.getLineNumber(), + interpreter.getPosition(), + e + ) + ); + } else if (e.getCause() != null && e.getCause() instanceof OutputTooBigException) { + interpreter.addError( + new TemplateError( + ErrorType.FATAL, + ErrorReason.OUTPUT_TOO_BIG, + ErrorItem.FUNCTION, + e.getCause().getMessage(), + null, + interpreter.getLineNumber(), + interpreter.getPosition(), + e + ) + ); + } else { + String originatingException = getRootCauseMessage(e); + final String combinedMessage = String.format( + "%s%nOriginating Exception:%n%s", + e.getMessage(), + originatingException + ); + interpreter.addError( + TemplateError.fromException( + new TemplateSyntaxException( + expression, + (e.getCause() == null || + StringUtils.endsWith(originatingException, e.getCause().getMessage())) + ? e.getMessage() + : combinedMessage, + interpreter.getLineNumber(), + e + ) + ) + ); + } + } + private void validateResult(Object result) { if (result instanceof NamedParameter) { throw new ELException( @@ -291,4 +349,11 @@ public Object resolveProperty(Object object, List propertyNames) { public Object wrap(Object object) { return resolver.wrap(object); } + + public String getAsString(Object object) { + if (interpreter.getConfig().getLegacyOverrides().isUsePyishObjectMapper()) { + return PyishObjectMapper.getAsUnquotedPyishString(object); + } + return Objects.toString(object, ""); + } } diff --git a/src/main/java/com/hubspot/jinjava/el/ExtendedSyntaxBuilder.java b/src/main/java/com/hubspot/jinjava/el/ExtendedSyntaxBuilder.java index c63401272..1dc7ad5dd 100644 --- a/src/main/java/com/hubspot/jinjava/el/ExtendedSyntaxBuilder.java +++ b/src/main/java/com/hubspot/jinjava/el/ExtendedSyntaxBuilder.java @@ -12,6 +12,7 @@ * */ public class ExtendedSyntaxBuilder extends Builder { + private static final long serialVersionUID = 1L; public ExtendedSyntaxBuilder() { diff --git a/src/main/java/com/hubspot/jinjava/el/HasInterpreter.java b/src/main/java/com/hubspot/jinjava/el/HasInterpreter.java new file mode 100644 index 000000000..8b22e98ee --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/HasInterpreter.java @@ -0,0 +1,7 @@ +package com.hubspot.jinjava.el; + +import com.hubspot.jinjava.interpret.JinjavaInterpreter; + +public interface HasInterpreter { + JinjavaInterpreter interpreter(); +} diff --git a/src/main/java/com/hubspot/jinjava/el/JinjavaELContext.java b/src/main/java/com/hubspot/jinjava/el/JinjavaELContext.java index 962b33ea4..8336b3e95 100644 --- a/src/main/java/com/hubspot/jinjava/el/JinjavaELContext.java +++ b/src/main/java/com/hubspot/jinjava/el/JinjavaELContext.java @@ -5,10 +5,12 @@ import java.lang.reflect.Method; import javax.el.ELResolver; -public class JinjavaELContext extends SimpleContext { +public class JinjavaELContext extends SimpleContext implements HasInterpreter { + private JinjavaInterpreter interpreter; private MacroFunctionMapper functionMapper; + @Deprecated public JinjavaELContext() { super(); } @@ -18,6 +20,11 @@ public JinjavaELContext(JinjavaInterpreter interpreter, ELResolver resolver) { this.interpreter = interpreter; } + @Override + public JinjavaInterpreter interpreter() { + return interpreter; + } + @Override public MacroFunctionMapper getFunctionMapper() { if (functionMapper == null) { diff --git a/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java b/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java index cfc519f67..c60372b99 100644 --- a/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/JinjavaInterpreterResolver.java @@ -9,11 +9,9 @@ import com.hubspot.jinjava.el.ext.JinjavaBeanELResolver; import com.hubspot.jinjava.el.ext.JinjavaListELResolver; import com.hubspot.jinjava.el.ext.NamedParameter; -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.LazyExpression; import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; @@ -23,16 +21,17 @@ import com.hubspot.jinjava.objects.PyWrapper; import com.hubspot.jinjava.objects.collections.SizeLimitingPyList; import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; +import com.hubspot.jinjava.objects.collections.SizeLimitingPySet; 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; +import com.hubspot.jinjava.util.DeferredValueUtils; import de.odysseus.el.util.SimpleResolver; 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; @@ -41,7 +40,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.Optional; +import java.util.Set; import javax.el.ArrayELResolver; import javax.el.CompositeELResolver; import javax.el.ELContext; @@ -52,8 +51,8 @@ import org.apache.commons.lang3.StringUtils; public class JinjavaInterpreterResolver extends SimpleResolver { - public static final ELResolver DEFAULT_RESOLVER_READ_ONLY = new CompositeELResolver() { + public static final ELResolver DEFAULT_RESOLVER_READ_ONLY = new CompositeELResolver() { { add(new ArrayELResolver(true)); add(new JinjavaListELResolver(true)); @@ -64,7 +63,6 @@ public class JinjavaInterpreterResolver extends SimpleResolver { }; public static final ELResolver DEFAULT_RESOLVER_READ_WRITE = new CompositeELResolver() { - { add(new ArrayELResolver(false)); add(new JinjavaListELResolver(false)); @@ -75,10 +73,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,20 +204,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; - } + base = objectUnwrapper.unwrapObject(base); + if (base == null) { + return null; } // java doesn't natively support negative array indices, so the @@ -241,23 +230,12 @@ 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(); + value = objectUnwrapper.unwrapObject(value); + if (value == null) { + return null; } - if (value instanceof LazyExpression) { - value = ((LazyExpression) value).get(); - if (value == null) { - return null; - } - } - - if (value instanceof DeferredValue) { + if (DeferredValueUtils.isFullyDeferred(value)) { if (interpreter.getConfig().getExecutionMode().useEagerParser()) { throw new DeferredParsingException(this, propertyName); } else { @@ -298,7 +276,7 @@ private Object getValue( } context.setPropertyResolved(true); - return wrap(value); + return value; } @SuppressWarnings("unchecked") @@ -306,15 +284,16 @@ Object wrap(Object value) { if (value == null) { return null; } + if (value instanceof String || value instanceof Number || value instanceof Boolean) { + return value; + } if (value instanceof PyishSerializable) { return value; } - if (value instanceof LazyExpression) { - value = ((LazyExpression) value).get(); - if (value == null) { - return null; - } + value = objectUnwrapper.unwrapObject(value); + if (value == null) { + return null; } if (value instanceof PyWrapper) { @@ -334,6 +313,12 @@ Object wrap(Object value) { interpreter.getConfig().getMaxListSize() ); } + if (Set.class.isAssignableFrom(value.getClass())) { + return new SizeLimitingPySet( + (Set) value, + interpreter.getConfig().getMaxListSize() + ); + } if (Map.class.isAssignableFrom(value.getClass())) { // FIXME: ensure keys are actually strings, if not, convert them return new SizeLimitingPyMap( @@ -380,22 +365,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 +396,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/el/JinjavaNodePreProcessor.java b/src/main/java/com/hubspot/jinjava/el/JinjavaNodePreProcessor.java new file mode 100644 index 000000000..64a490945 --- /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 extends JinjavaNodeProcessor { + + @Override + public void accept(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/JinjavaNodeProcessor.java b/src/main/java/com/hubspot/jinjava/el/JinjavaNodeProcessor.java new file mode 100644 index 000000000..64ebdde4b --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/JinjavaNodeProcessor.java @@ -0,0 +1,11 @@ +package com.hubspot.jinjava.el; + +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.tree.Node; +import java.util.function.BiConsumer; + +public class JinjavaNodeProcessor implements BiConsumer { + + @Override + public void accept(Node node, JinjavaInterpreter interpreter) {} +} 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/JinjavaProcessors.java b/src/main/java/com/hubspot/jinjava/el/JinjavaProcessors.java new file mode 100644 index 000000000..01195f64a --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/JinjavaProcessors.java @@ -0,0 +1,59 @@ +package com.hubspot.jinjava.el; + +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.tree.Node; +import java.util.function.BiConsumer; + +public class JinjavaProcessors { + + private final BiConsumer nodePreProcessor; + private final BiConsumer nodePostProcessor; + + private JinjavaProcessors(Builder builder) { + nodePreProcessor = builder.nodePreProcessor; + nodePostProcessor = builder.nodePostProcessor; + } + + public BiConsumer getNodePreProcessor() { + return nodePreProcessor; + } + + public BiConsumer getNodePostProcessor() { + return nodePostProcessor; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static Builder newBuilder(JinjavaProcessors processors) { + return new Builder(processors); + } + + public static class Builder { + + private BiConsumer nodePreProcessor = (n, i) -> {}; + private BiConsumer nodePostProcessor = (n, i) -> {}; + + private Builder() {} + + private Builder(JinjavaProcessors processors) { + this.nodePreProcessor = processors.nodePreProcessor; + this.nodePostProcessor = processors.nodePostProcessor; + } + + public Builder withNodePreProcessor(BiConsumer processor) { + this.nodePreProcessor = processor; + return this; + } + + public Builder withNodePostProcessor(BiConsumer processor) { + this.nodePostProcessor = processor; + return this; + } + + public JinjavaProcessors build() { + return new JinjavaProcessors(this); + } + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/MacroFunctionMapper.java b/src/main/java/com/hubspot/jinjava/el/MacroFunctionMapper.java index 449bc2f98..a685abb19 100644 --- a/src/main/java/com/hubspot/jinjava/el/MacroFunctionMapper.java +++ b/src/main/java/com/hubspot/jinjava/el/MacroFunctionMapper.java @@ -12,6 +12,7 @@ import javax.el.FunctionMapper; public class MacroFunctionMapper extends FunctionMapper { + private final JinjavaInterpreter interpreter; private Map map = Collections.emptyMap(); diff --git a/src/main/java/com/hubspot/jinjava/el/NoInvokeELContext.java b/src/main/java/com/hubspot/jinjava/el/NoInvokeELContext.java index 4e7900d6f..3d3f92f4e 100644 --- a/src/main/java/com/hubspot/jinjava/el/NoInvokeELContext.java +++ b/src/main/java/com/hubspot/jinjava/el/NoInvokeELContext.java @@ -1,12 +1,14 @@ package com.hubspot.jinjava.el; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; import javax.el.ELContext; import javax.el.ELResolver; import javax.el.FunctionMapper; import javax.el.VariableMapper; -public class NoInvokeELContext extends ELContext { - private ELContext delegate; +public class NoInvokeELContext extends ELContext implements HasInterpreter { + + private final ELContext delegate; private NoInvokeELResolver elResolver; public NoInvokeELContext(ELContext delegate) { @@ -30,4 +32,12 @@ public FunctionMapper getFunctionMapper() { public VariableMapper getVariableMapper() { return delegate.getVariableMapper(); } + + @Override + public JinjavaInterpreter interpreter() { + if (delegate instanceof HasInterpreter hasInterpreter) { + return hasInterpreter.interpreter(); + } + return JinjavaInterpreter.getCurrent(); + } } diff --git a/src/main/java/com/hubspot/jinjava/el/NoInvokeELResolver.java b/src/main/java/com/hubspot/jinjava/el/NoInvokeELResolver.java index b007aad3d..41f2256c0 100644 --- a/src/main/java/com/hubspot/jinjava/el/NoInvokeELResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/NoInvokeELResolver.java @@ -1,6 +1,6 @@ package com.hubspot.jinjava.el; -import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.interpret.DeferredValueException; import java.beans.FeatureDescriptor; import java.util.Iterator; import javax.el.ELContext; @@ -12,6 +12,7 @@ * so disallows modification and invocation which may result in modification of values. */ public class NoInvokeELResolver extends ELResolver { + private ELResolver delegate; public NoInvokeELResolver(ELResolver delegate) { @@ -48,7 +49,7 @@ public boolean isReadOnly(ELContext elContext, Object base, Object property) { @Override public void setValue(ELContext elContext, Object base, Object property, Object value) { - throw new DeferredParsingException("NoInvokeELResolver"); + throw new DeferredValueException("NoInvokeELResolver"); } @Override @@ -59,6 +60,6 @@ public Object invoke( Class[] paramTypes, Object[] params ) { - throw new DeferredParsingException("NoInvokeELResolver"); + throw new DeferredValueException("NoInvokeELResolver"); } } 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/el/ReturnTypeValidatingJinjavaInterpreterResolver.java b/src/main/java/com/hubspot/jinjava/el/ReturnTypeValidatingJinjavaInterpreterResolver.java new file mode 100644 index 000000000..0a874f5e1 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ReturnTypeValidatingJinjavaInterpreterResolver.java @@ -0,0 +1,73 @@ +package com.hubspot.jinjava.el; + +import com.hubspot.jinjava.el.ext.AllowlistReturnTypeValidator; +import java.beans.FeatureDescriptor; +import java.util.Iterator; +import javax.el.ELContext; +import javax.el.ELResolver; + +class ReturnTypeValidatingJinjavaInterpreterResolver extends ELResolver { + + private final AllowlistReturnTypeValidator returnTypeValidator; + private final JinjavaInterpreterResolver delegate; + + ReturnTypeValidatingJinjavaInterpreterResolver( + AllowlistReturnTypeValidator returnTypeValidator, + JinjavaInterpreterResolver delegate + ) { + this.returnTypeValidator = returnTypeValidator; + this.delegate = delegate; + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + return delegate.getCommonPropertyType(context, base); + } + + @Override + public Iterator getFeatureDescriptors( + ELContext context, + Object base + ) { + return delegate.getFeatureDescriptors(context, base); + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + return delegate.getType(context, base, property); + } + + @Override + public Object getValue(ELContext context, Object base, Object property) { + return returnTypeValidator.validateReturnType( + wrap(delegate.getValue(context, base, property)) + ); + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + return delegate.isReadOnly(context, base, property); + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + delegate.setValue(context, base, property, value); + } + + @Override + public Object invoke( + ELContext context, + Object base, + Object method, + Class[] paramTypes, + Object[] params + ) { + return returnTypeValidator.validateReturnType( + wrap(delegate.invoke(context, base, method, paramTypes, params)) + ); + } + + Object wrap(Object object) { + return delegate.wrap(object); + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/TruthyTypeConverter.java b/src/main/java/com/hubspot/jinjava/el/TruthyTypeConverter.java index f3800a079..25babab14 100644 --- a/src/main/java/com/hubspot/jinjava/el/TruthyTypeConverter.java +++ b/src/main/java/com/hubspot/jinjava/el/TruthyTypeConverter.java @@ -5,11 +5,15 @@ import de.odysseus.el.misc.TypeConverterImpl; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.Collection; import java.util.EnumSet; +import java.util.Iterator; import javax.el.ELException; public class TruthyTypeConverter extends TypeConverterImpl { + private static final long serialVersionUID = 1L; + public static final int MAX_COLLECTION_STRING_LENGTH = 1_000_000; @Override protected Boolean coerceToBoolean(Object value) { @@ -93,7 +97,44 @@ protected String coerceToString(Object value) { if (value instanceof DummyObject) { return ""; } - return super.coerceToString(value); + if (value == null) { + return ""; + } + if (value instanceof String) { + return (String) value; + } + if (value instanceof Enum) { + return ((Enum) value).name(); + } + + if (value instanceof Collection) { + return coerceCollection((Collection) value); + } + + return value.toString(); + } + + private String coerceCollection(Collection value) { + Iterator it = value.iterator(); + if (!it.hasNext()) { + return "[]"; + } + + StringBuilder sb = new StringBuilder(); + + sb.append('['); + for (;;) { + Object e = it.next(); + sb.append(e == this ? "(this Collection)" : e); + if (sb.length() > MAX_COLLECTION_STRING_LENGTH) { + return sb.append(", ...]").toString(); + } + if (!it.hasNext()) { + return sb.append(']').toString(); + } + sb.append(','); + sb.append(' '); + } } @Override diff --git a/src/main/java/com/hubspot/jinjava/el/TypeConvertingMapELResolver.java b/src/main/java/com/hubspot/jinjava/el/TypeConvertingMapELResolver.java index 7d90543e3..1c6304e7b 100644 --- a/src/main/java/com/hubspot/jinjava/el/TypeConvertingMapELResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/TypeConvertingMapELResolver.java @@ -1,11 +1,13 @@ package com.hubspot.jinjava.el; +import java.util.Iterator; import java.util.Map; import javax.el.ELContext; import javax.el.ELException; import javax.el.MapELResolver; public class TypeConvertingMapELResolver extends MapELResolver { + private static final TruthyTypeConverter TYPE_CONVERTER = new TruthyTypeConverter(); public TypeConvertingMapELResolver(boolean readOnly) { @@ -21,14 +23,27 @@ 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(); + Class keyClass = null; + while (iterator.hasNext()) { + Object nextObject = iterator.next(); + if (nextObject != null) { + keyClass = nextObject.getClass(); + break; + } + } + + if (keyClass == null) { + value = ((Map) base).get(property); + } else { + 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; } } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AbsOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/AbsOperator.java index 6e5ef9d29..5fc001db4 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/AbsOperator.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/AbsOperator.java @@ -11,6 +11,7 @@ import de.odysseus.el.tree.impl.ast.AstUnary.SimpleOperator; public class AbsOperator extends SimpleOperator { + public static final ExtensionToken TOKEN = new Scanner.ExtensionToken("+"); public static final AbsOperator OP = new AbsOperator(); @@ -47,7 +48,6 @@ public String toString() { public static ExtensionHandler getHandler(boolean eager) { return new ExtensionHandler(ExtensionPoint.UNARY) { - @Override public AstNode createAstNode(AstNode... children) { return eager ? new EagerAstUnary(children[0], OP) : new AstUnary(children[0], OP); diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AbstractCallableMethod.java b/src/main/java/com/hubspot/jinjava/el/ext/AbstractCallableMethod.java index ebf4e6ce1..91b95550c 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/AbstractCallableMethod.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/AbstractCallableMethod.java @@ -14,6 +14,7 @@ * */ public abstract class AbstractCallableMethod { + public static final Method EVAL_METHOD; static { diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AdditionOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/AdditionOperator.java index 4dff03e5b..b9f6a99b6 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/AdditionOperator.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/AdditionOperator.java @@ -10,7 +10,9 @@ import java.util.Map; import java.util.Objects; -public class AdditionOperator extends AstBinary.SimpleOperator { +public class AdditionOperator + extends AstBinary.SimpleOperator + implements StringBuildingOperator { @SuppressWarnings("unchecked") @Override @@ -33,7 +35,10 @@ protected Object apply(TypeConverter converter, Object o1, Object o2) { } if (o1 instanceof String || o2 instanceof String) { - return Objects.toString(o1).concat(Objects.toString(o2)); + return getStringBuilder() + .append(Objects.toString(o1)) + .append(Objects.toString(o2)) + .toString(); } return NumberOperations.add(converter, o1, o2); diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AllowlistGroup.java b/src/main/java/com/hubspot/jinjava/el/ext/AllowlistGroup.java new file mode 100644 index 000000000..0205f572a --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/AllowlistGroup.java @@ -0,0 +1,211 @@ +package com.hubspot.jinjava.el.ext; + +import com.google.common.collect.ForwardingCollection; +import com.google.common.collect.ForwardingList; +import com.google.common.collect.ForwardingMap; +import com.google.common.collect.ForwardingSet; +import com.hubspot.jinjava.interpret.NullValue; +import com.hubspot.jinjava.lib.exptest.ExpTest; +import com.hubspot.jinjava.lib.filter.Filter; +import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.lib.fn.eager.EagerMacroFunction; +import com.hubspot.jinjava.objects.DummyObject; +import com.hubspot.jinjava.objects.Namespace; +import com.hubspot.jinjava.objects.SafeString; +import com.hubspot.jinjava.objects.collections.PyList; +import com.hubspot.jinjava.objects.collections.PyMap; +import com.hubspot.jinjava.objects.collections.PySet; +import com.hubspot.jinjava.objects.collections.SizeLimitingPyList; +import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; +import com.hubspot.jinjava.objects.collections.SizeLimitingPySet; +import com.hubspot.jinjava.objects.collections.SnakeCaseAccessibleMap; +import com.hubspot.jinjava.objects.date.FormattedDate; +import com.hubspot.jinjava.objects.date.PyishDate; +import com.hubspot.jinjava.util.ForLoop; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; + +public enum AllowlistGroup { + JavaPrimitives { + private static final String[] ARRAY = { + String.class.getCanonicalName(), + Long.class.getCanonicalName(), + Integer.class.getCanonicalName(), + Double.class.getCanonicalName(), + Byte.class.getCanonicalName(), + Character.class.getCanonicalName(), + Float.class.getCanonicalName(), + Boolean.class.getCanonicalName(), + Short.class.getCanonicalName(), + long.class.getCanonicalName(), + int.class.getCanonicalName(), + double.class.getCanonicalName(), + byte.class.getCanonicalName(), + char.class.getCanonicalName(), + float.class.getCanonicalName(), + boolean.class.getCanonicalName(), + short.class.getCanonicalName(), + BigDecimal.class.getCanonicalName(), + BigInteger.class.getCanonicalName(), + }; + + @Override + String[] allowedReturnTypeClasses() { + return ARRAY; + } + + @Override + String[] allowedDeclaredMethodsFromClasses() { + return ARRAY; + } + }, + JinjavaObjects { + private static final String[] ARRAY = { + PyList.class.getCanonicalName(), + PyMap.class.getCanonicalName(), + PySet.class.getCanonicalName(), + SizeLimitingPyMap.class.getCanonicalName(), + SizeLimitingPyList.class.getCanonicalName(), + SizeLimitingPySet.class.getCanonicalName(), + SnakeCaseAccessibleMap.class.getCanonicalName(), + FormattedDate.class.getCanonicalName(), + PyishDate.class.getCanonicalName(), + DummyObject.class.getCanonicalName(), + Namespace.class.getCanonicalName(), + SafeString.class.getCanonicalName(), + NullValue.class.getCanonicalName(), + }; + + @Override + String[] allowedReturnTypeClasses() { + return ARRAY; + } + + @Override + String[] allowedDeclaredMethodsFromClasses() { + return ARRAY; + } + }, + Collections { + private static final String[] ARRAY = { + Map.Entry.class.getCanonicalName(), + PyList.class.getCanonicalName(), + PyMap.class.getCanonicalName(), + PySet.class.getCanonicalName(), + SizeLimitingPyMap.class.getCanonicalName(), + SizeLimitingPyList.class.getCanonicalName(), + SizeLimitingPySet.class.getCanonicalName(), + ArrayList.class.getCanonicalName(), + ForwardingList.class.getCanonicalName(), + ForwardingMap.class.getCanonicalName(), + ForwardingSet.class.getCanonicalName(), + ForwardingCollection.class.getCanonicalName(), + LinkedHashMap.class.getCanonicalName(), + "%s.Entry".formatted(LinkedHashMap.class.getCanonicalName()), + "%s.LinkedValues".formatted(LinkedHashMap.class.getCanonicalName()), + }; + + @Override + String[] allowedReturnTypeClasses() { + return ARRAY; + } + + @Override + String[] allowedDeclaredMethodsFromClasses() { + return ARRAY; + } + + @Override + boolean enableArrays() { + return true; + } + }, + JinjavaTagConstructs { + private static final String[] ARRAY = { + ForLoop.class.getCanonicalName(), + MacroFunction.class.getCanonicalName(), + EagerMacroFunction.class.getCanonicalName(), + }; + + @Override + String[] allowedReturnTypeClasses() { + return ARRAY; + } + + @Override + String[] allowedDeclaredMethodsFromClasses() { + return ARRAY; + } + }, + JinjavaFilters { + private static final String[] ARRAY = { Filter.class.getPackageName() + '.' }; + + @Override + String[] allowedDeclaredMethodsFromCanonicalClassPrefixes() { + return ARRAY; + } + + @Override + String[] allowedReturnTypeCanonicalClassPrefixes() { + return ARRAY; + } + }, + JinjavaFunctions { + private static final String[] ARRAY = { + ZonedDateTime.class.getCanonicalName(), + NamedParameter.class.getCanonicalName(), + }; + + @Override + String[] allowedDeclaredMethodsFromClasses() { + return ARRAY; + } + + @Override + String[] allowedReturnTypeClasses() { + return ARRAY; + } + }, + JinjavaExpTests { + private static final String[] ARRAY = { ExpTest.class.getPackageName() + '.' }; + + @Override + String[] allowedDeclaredMethodsFromCanonicalClassPrefixes() { + return ARRAY; + } + + @Override + String[] allowedReturnTypeCanonicalClassPrefixes() { + return ARRAY; + } + }; + + Method[] allowMethods() { + return new Method[0]; + } + + String[] allowedDeclaredMethodsFromCanonicalClassPrefixes() { + return new String[0]; + } + + String[] allowedDeclaredMethodsFromClasses() { + return new String[0]; + } + + String[] allowedReturnTypeCanonicalClassPrefixes() { + return new String[0]; + } + + String[] allowedReturnTypeClasses() { + return new String[0]; + } + + boolean enableArrays() { + return false; + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AllowlistMethodValidator.java b/src/main/java/com/hubspot/jinjava/el/ext/AllowlistMethodValidator.java new file mode 100644 index 000000000..7e19b70c1 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/AllowlistMethodValidator.java @@ -0,0 +1,79 @@ +package com.hubspot.jinjava.el.ext; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +public final class AllowlistMethodValidator { + + public static final AllowlistMethodValidator DEFAULT = AllowlistMethodValidator.create( + MethodValidatorConfig.builder().addDefaultAllowlistGroups().build() + ); + private final ConcurrentHashMap allowedMethodsCache; + private final ImmutableSet allowedMethods; + private final ImmutableSet allowedDeclaredMethodsFromCanonicalClassPrefixes; + private final ImmutableSet allowedDeclaredMethodsFromCanonicalClassNames; + private final Consumer onRejectedMethod; + private final ImmutableList additionalValidators; + + public static AllowlistMethodValidator create( + MethodValidatorConfig methodValidatorConfig, + MethodValidator... additionalValidators + ) { + return new AllowlistMethodValidator( + methodValidatorConfig, + ImmutableList.copyOf(additionalValidators) + ); + } + + private AllowlistMethodValidator( + MethodValidatorConfig methodValidatorConfig, + ImmutableList additionalValidators + ) { + this.allowedMethods = methodValidatorConfig.allowedMethods(); + this.allowedDeclaredMethodsFromCanonicalClassPrefixes = + methodValidatorConfig.allowedDeclaredMethodsFromCanonicalClassPrefixes(); + this.allowedDeclaredMethodsFromCanonicalClassNames = + methodValidatorConfig.allowedDeclaredMethodsFromCanonicalClassNames(); + this.onRejectedMethod = methodValidatorConfig.onRejectedMethod(); + this.additionalValidators = additionalValidators; + this.allowedMethodsCache = new ConcurrentHashMap<>(); + } + + public Method validateMethod(Method m) { + if (m == null) { + return null; + } + boolean isAllowedMethod = allowedMethodsCache.computeIfAbsent( + m, + m1 -> { + Class clazz = m1.getDeclaringClass(); + String canonicalClassName = clazz.getCanonicalName(); + if (canonicalClassName == null) { + return false; + } + return ( + allowedMethods.contains(m1) || + allowedDeclaredMethodsFromCanonicalClassNames.contains(canonicalClassName) || + allowedDeclaredMethodsFromCanonicalClassPrefixes + .stream() + .anyMatch(canonicalClassName::startsWith) + ); + } + ); + if (!isAllowedMethod) { + onRejectedMethod.accept(m); + return null; + } + for (MethodValidator v : additionalValidators) { + if (v.validateMethod(m) == null) { + onRejectedMethod.accept(m); + return null; + } + } + + return m; + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AllowlistReturnTypeValidator.java b/src/main/java/com/hubspot/jinjava/el/ext/AllowlistReturnTypeValidator.java new file mode 100644 index 000000000..e42d79888 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/AllowlistReturnTypeValidator.java @@ -0,0 +1,82 @@ +package com.hubspot.jinjava.el.ext; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +public final class AllowlistReturnTypeValidator { + + public static final AllowlistReturnTypeValidator DEFAULT = + AllowlistReturnTypeValidator.create( + ReturnTypeValidatorConfig.builder().addDefaultAllowlistGroups().build() + ); + private final ConcurrentHashMap, Boolean> allowedReturnTypesCache; + + private final ImmutableSet allowedCanonicalClassPrefixes; + private final ImmutableSet allowedCanonicalClassNames; + private final boolean allowArrays; + private final Consumer> onRejectedClass; + private final ImmutableList additionalValidators; + + public static AllowlistReturnTypeValidator create( + ReturnTypeValidatorConfig returnTypeValidatorConfig, + ReturnTypeValidator... additionalValidators + ) { + return new AllowlistReturnTypeValidator( + returnTypeValidatorConfig, + ImmutableList.copyOf(additionalValidators) + ); + } + + private AllowlistReturnTypeValidator( + ReturnTypeValidatorConfig returnTypeValidatorConfig, + ImmutableList additionalValidators + ) { + this.allowedCanonicalClassPrefixes = + returnTypeValidatorConfig.allowedCanonicalClassPrefixes(); + this.allowedCanonicalClassNames = + returnTypeValidatorConfig.allowedCanonicalClassNames(); + this.allowArrays = returnTypeValidatorConfig.allowArrays(); + this.onRejectedClass = returnTypeValidatorConfig.onRejectedClass(); + this.additionalValidators = additionalValidators; + this.allowedReturnTypesCache = new ConcurrentHashMap<>(); + } + + public Object validateReturnType(Object o) { + if (o == null) { + return null; + } + if (o instanceof String || o instanceof Number || o instanceof Boolean) { + return o; + } + Class clazz = o.getClass(); + if (clazz.isArray() && allowArrays) { + return o; + } + boolean isAllowedClassName = allowedReturnTypesCache.computeIfAbsent( + clazz, + c -> { + String canonicalClassName = c.getCanonicalName(); + if (canonicalClassName == null) { + return false; + } + return ( + allowedCanonicalClassNames.contains(canonicalClassName) || + allowedCanonicalClassPrefixes.stream().anyMatch(canonicalClassName::startsWith) + ); + } + ); + if (!isAllowedClassName) { + onRejectedClass.accept(clazz); + return null; + } + for (ReturnTypeValidator v : additionalValidators) { + if (v.validateReturnType(o) == null) { + onRejectedClass.accept(clazz); + return null; + } + } + return o; + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AstDict.java b/src/main/java/com/hubspot/jinjava/el/ext/AstDict.java index 36d52e4f8..d92acf7b3 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/AstDict.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/AstDict.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.el.ext; +import com.hubspot.jinjava.el.HasInterpreter; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateStateException; import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; @@ -7,6 +8,7 @@ import de.odysseus.el.tree.impl.ast.AstIdentifier; import de.odysseus.el.tree.impl.ast.AstLiteral; import de.odysseus.el.tree.impl.ast.AstNode; +import de.odysseus.el.tree.impl.ast.AstNumber; import de.odysseus.el.tree.impl.ast.AstString; import java.util.LinkedHashMap; import java.util.Map; @@ -14,6 +16,7 @@ import javax.el.ELContext; public class AstDict extends AstLiteral { + protected final Map dict; public AstDict(Map dict) { @@ -24,10 +27,7 @@ public AstDict(Map dict) { public Object eval(Bindings bindings, ELContext context) { Map resolved = new LinkedHashMap<>(); - JinjavaInterpreter interpreter = (JinjavaInterpreter) context - .getELResolver() - .getValue(context, null, ExtendedParser.INTERPRETER); - + JinjavaInterpreter interpreter = ((HasInterpreter) context).interpreter(); for (Map.Entry entry : dict.entrySet()) { String key; AstNode entryKey = entry.getKey(); @@ -44,9 +44,14 @@ public Object eval(Bindings bindings, ELContext context) { } else { key = ((AstIdentifier) entryKey).getName(); } + } else if (entryKey instanceof AstNumber) { + // This is a hack to treat numeric keys as string keys in the dictionary. + // In most cases this is adequate since the keys are typically treated as + // strings. + key = Objects.toString(entryKey.eval(bindings, context)); } else { throw new TemplateStateException( - "Dict key must be a string or identifier, was: " + entryKey + "Dict key must be a string, or identifier, or a number, was: " + entryKey ); } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AstFilterChain.java b/src/main/java/com/hubspot/jinjava/el/ext/AstFilterChain.java new file mode 100644 index 000000000..bb29d5694 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/AstFilterChain.java @@ -0,0 +1,206 @@ +package com.hubspot.jinjava.el.ext; + +import com.hubspot.jinjava.el.HasInterpreter; +import com.hubspot.jinjava.interpret.DisabledException; +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 com.hubspot.jinjava.lib.filter.Filter; +import com.hubspot.jinjava.objects.SafeString; +import de.odysseus.el.tree.Bindings; +import de.odysseus.el.tree.impl.ast.AstNode; +import de.odysseus.el.tree.impl.ast.AstParameters; +import de.odysseus.el.tree.impl.ast.AstRightValue; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.el.ELContext; +import javax.el.ELException; + +/** + * AST node for a chain of filters applied to an input expression. + * Instead of creating nested AstMethod calls for each filter in a chain like: + * filter:length.filter(filter:lower.filter(filter:trim.filter(input))) + * + * This node represents the entire chain as a single evaluation unit: + * input|trim|lower|length + * + * This optimization reduces: + * - Filter lookups (done once per filter instead of per AST node traversal) + * - Method invocation overhead + * - Object wrapping/unwrapping between filters + * - Context operations + */ +public class AstFilterChain extends AstRightValue { + + protected final AstNode input; + protected final List filterSpecs; + + public AstFilterChain(AstNode input, List filterSpecs) { + this.input = Objects.requireNonNull(input, "Input node cannot be null"); + this.filterSpecs = Objects.requireNonNull(filterSpecs, "Filter specs cannot be null"); + if (filterSpecs.isEmpty()) { + throw new IllegalArgumentException("Filter chain must have at least one filter"); + } + } + + public AstNode getInput() { + return input; + } + + public List getFilterSpecs() { + return filterSpecs; + } + + @Override + public Object eval(Bindings bindings, ELContext context) { + JinjavaInterpreter interpreter = getInterpreter(context); + + if (interpreter.getContext().isValidationMode()) { + return ""; + } + + Object value = input.eval(bindings, context); + + for (FilterSpec spec : filterSpecs) { + String filterKey = ExtendedParser.FILTER_PREFIX + spec.getName(); + interpreter.getContext().addResolvedValue(filterKey); + + Filter filter; + try { + filter = interpreter.getContext().getFilter(spec.getName()); + } catch (DisabledException e) { + interpreter.addError( + new TemplateError( + ErrorType.FATAL, + ErrorReason.DISABLED, + ErrorItem.FILTER, + e.getMessage(), + spec.getName(), + interpreter.getLineNumber(), + -1, + e + ) + ); + value = null; + continue; + } + if (filter == null) { + value = null; + continue; + } + + Object[] args = evaluateFilterArgs(spec, bindings, context); + Map kwargs = extractNamedParams(args); + Object[] positionalArgs = extractPositionalArgs(args); + + boolean wasSafeString = value instanceof SafeString; + if (wasSafeString) { + value = value.toString(); + } + + try { + value = filter.filter(value, interpreter, positionalArgs, kwargs); + } catch (ELException e) { + throw e; + } catch (RuntimeException e) { + throw new ELException( + String.format("Error in filter '%s': %s", spec.getName(), e.getMessage()), + e + ); + } + + if (wasSafeString && filter.preserveSafeString() && value instanceof String) { + value = new SafeString((String) value); + } + } + + return value; + } + + protected JinjavaInterpreter getInterpreter(ELContext context) { + return ((HasInterpreter) context).interpreter(); + } + + protected Object[] evaluateFilterArgs( + FilterSpec spec, + Bindings bindings, + ELContext context + ) { + AstParameters params = spec.getParams(); + if (params == null || params.getCardinality() == 0) { + return new Object[0]; + } + + Object[] args = new Object[params.getCardinality()]; + for (int i = 0; i < params.getCardinality(); i++) { + args[i] = params.getChild(i).eval(bindings, context); + } + return args; + } + + private Map extractNamedParams(Object[] args) { + Map kwargs = new LinkedHashMap<>(); + for (Object arg : args) { + if (arg instanceof NamedParameter) { + NamedParameter namedParam = (NamedParameter) arg; + kwargs.put(namedParam.getName(), namedParam.getValue()); + } + } + return kwargs; + } + + private Object[] extractPositionalArgs(Object[] args) { + List positional = new ArrayList<>(); + for (Object arg : args) { + if (!(arg instanceof NamedParameter)) { + positional.add(arg); + } + } + return positional.toArray(); + } + + @Override + public void appendStructure(StringBuilder builder, Bindings bindings) { + input.appendStructure(builder, bindings); + for (FilterSpec spec : filterSpecs) { + builder.append('|').append(spec.getName()); + AstParameters params = spec.getParams(); + if (params != null && params.getCardinality() > 0) { + params.appendStructure(builder, bindings); + } + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(input.toString()); + for (FilterSpec spec : filterSpecs) { + sb.append('|').append(spec.toString()); + } + return sb.toString(); + } + + @Override + public int getCardinality() { + return 1 + filterSpecs.size(); + } + + @Override + public AstNode getChild(int i) { + if (i == 0) { + return input; + } + int filterIndex = i - 1; + if (filterIndex < filterSpecs.size()) { + FilterSpec spec = filterSpecs.get(filterIndex); + return spec.getParams(); + } + return null; + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AstList.java b/src/main/java/com/hubspot/jinjava/el/ext/AstList.java index df63b3fe4..163548a2e 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/AstList.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/AstList.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.el.ext; +import com.hubspot.jinjava.el.HasInterpreter; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.objects.collections.SizeLimitingPyList; import de.odysseus.el.tree.Bindings; @@ -11,6 +12,7 @@ import org.apache.commons.lang3.StringUtils; public class AstList extends AstLiteral { + protected final AstParameters elements; public AstList(AstParameters elements) { @@ -25,9 +27,7 @@ public Object eval(Bindings bindings, ELContext context) { list.add(elements.getChild(i).eval(bindings, context)); } - JinjavaInterpreter interpreter = (JinjavaInterpreter) context - .getELResolver() - .getValue(context, null, ExtendedParser.INTERPRETER); + JinjavaInterpreter interpreter = ((HasInterpreter) context).interpreter(); return new SizeLimitingPyList(list, interpreter.getConfig().getMaxListSize()); } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AstMacroFunction.java b/src/main/java/com/hubspot/jinjava/el/ext/AstMacroFunction.java index 185c60792..044249407 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/AstMacroFunction.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/AstMacroFunction.java @@ -1,10 +1,14 @@ package com.hubspot.jinjava.el.ext; import com.google.common.collect.ImmutableMap; +import com.hubspot.algebra.Result; +import com.hubspot.jinjava.el.HasInterpreter; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl; import com.hubspot.jinjava.interpret.CallStack; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.MacroTagCycleException; +import com.hubspot.jinjava.interpret.TagCycleException; import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.errorcategory.BasicTemplateErrorCategory; import com.hubspot.jinjava.lib.fn.MacroFunction; @@ -18,15 +22,17 @@ public class AstMacroFunction extends AstFunction { + public enum MacroCallError { + CYCLE_DETECTED, + } + public AstMacroFunction(String name, int index, AstParameters params, boolean varargs) { super(name, index, params, varargs); } @Override public Object eval(Bindings bindings, ELContext context) { - JinjavaInterpreter interpreter = (JinjavaInterpreter) context - .getELResolver() - .getValue(context, null, ExtendedParser.INTERPRETER); + JinjavaInterpreter interpreter = ((HasInterpreter) context).interpreter(); MacroFunction macroFunction = interpreter.getContext().getGlobalMacro(getName()); if (macroFunction != null) { @@ -37,30 +43,16 @@ public Object eval(Bindings bindings, ELContext context) { interpreter.getPosition() ); } - if (!macroFunction.isCaller()) { - if (checkAndPushMacroStack(interpreter, getName())) { - return ""; - } + if (macroFunction.isCaller()) { + return wrapInvoke(bindings, context, macroFunction); } - - try { - return invoke( - bindings, - context, - macroFunction, - AbstractCallableMethod.EVAL_METHOD - ); - } catch (IllegalAccessException e) { - throw new ELException(LocalMessages.get("error.function.access", getName()), e); - } catch (InvocationTargetException e) { - throw new ELException( - LocalMessages.get("error.function.invocation", getName()), - e.getCause() - ); - } finally { - if (!macroFunction.isCaller()) { - interpreter.getContext().getMacroStack().pop(); - } + try ( + AutoCloseableImpl> macroStackPush = + checkAndPushMacroStackWithWrapper(interpreter, getName()).get() + ) { + return macroStackPush + .value() + .match(err -> "", path -> wrapInvoke(bindings, context, macroFunction)); } } @@ -69,62 +61,104 @@ public Object eval(Bindings bindings, ELContext context) { : super.eval(bindings, context); } - public static boolean checkAndPushMacroStack( + private Object wrapInvoke( + Bindings bindings, + ELContext context, + MacroFunction macroFunction + ) { + try { + return invoke(bindings, context, macroFunction, AbstractCallableMethod.EVAL_METHOD); + } catch (IllegalAccessException e) { + throw new ELException(LocalMessages.get("error.function.access", getName()), e); + } catch (InvocationTargetException e) { + throw new ELException( + LocalMessages.get("error.function.invocation", getName()), + e.getCause() + ); + } + } + + public static AutoCloseableSupplier> checkAndPushMacroStackWithWrapper( JinjavaInterpreter interpreter, String name ) { CallStack macroStack = interpreter.getContext().getMacroStack(); - try { - if (interpreter.getConfig().isEnableRecursiveMacroCalls()) { - if (interpreter.getConfig().getMaxMacroRecursionDepth() != 0) { - macroStack.pushWithMaxDepth( + if (interpreter.getConfig().isEnableRecursiveMacroCalls()) { + if (interpreter.getConfig().getMaxMacroRecursionDepth() != 0) { + return macroStack + .closeablePushWithMaxDepth( name, interpreter.getConfig().getMaxMacroRecursionDepth(), interpreter.getLineNumber(), interpreter.getPosition() + ) + .map(result -> + result.mapErr(err -> { + handleMacroCycleError(interpreter, name, err); + return MacroCallError.CYCLE_DETECTED; + }) ); - } else { - macroStack.pushWithoutCycleCheck( + } else { + return macroStack + .closeablePushWithoutCycleCheck( name, interpreter.getLineNumber(), interpreter.getPosition() - ); - } - } else { - macroStack.push(name, -1, -1); - } - } catch (MacroTagCycleException e) { - int maxDepth = interpreter.getConfig().getMaxMacroRecursionDepth(); - if (maxDepth != 0 && interpreter.getConfig().isValidationMode()) { - // validation mode is only concerned with syntax - return true; + ) + .map(Result::ok); } - - String message = maxDepth == 0 - ? String.format("Cycle detected for macro '%s'", name) - : String.format( - "Max recursion limit of %d reached for macro '%s'", - maxDepth, - name - ); - - interpreter.addError( - new TemplateError( - TemplateError.ErrorType.WARNING, - TemplateError.ErrorReason.EXCEPTION, - TemplateError.ErrorItem.TAG, - message, - null, - e.getLineNumber(), - e.getStartPosition(), - e, - BasicTemplateErrorCategory.CYCLE_DETECTED, - ImmutableMap.of("name", name) - ) + } + return macroStack + .closeablePush(name, -1, -1) + .map(result -> + result.mapErr(err -> { + handleMacroCycleError(interpreter, name, err); + return MacroCallError.CYCLE_DETECTED; + }) ); + } - return true; + private static void handleMacroCycleError( + JinjavaInterpreter interpreter, + String name, + TagCycleException e + ) { + int maxDepth = interpreter.getConfig().getMaxMacroRecursionDepth(); + if (maxDepth != 0 && interpreter.getConfig().isValidationMode()) { + // validation mode is only concerned with syntax + return; } - return false; + + String message = maxDepth == 0 + ? String.format("Cycle detected for macro '%s'", name) + : String.format("Max recursion limit of %d reached for macro '%s'", maxDepth, name); + + interpreter.addError( + new TemplateError( + TemplateError.ErrorType.WARNING, + TemplateError.ErrorReason.EXCEPTION, + TemplateError.ErrorItem.TAG, + message, + null, + e.getLineNumber(), + e.getStartPosition(), + e, + BasicTemplateErrorCategory.CYCLE_DETECTED, + ImmutableMap.of("name", name) + ) + ); + } + + @Deprecated + public static boolean checkAndPushMacroStack( + JinjavaInterpreter interpreter, + String name + ) { + return checkAndPushMacroStackWithWrapper(interpreter, name) + .dangerouslyGetWithoutClosing() + .match( + err -> true, // cycle detected + ok -> false // no cycle + ); } } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AstNamedParameter.java b/src/main/java/com/hubspot/jinjava/el/ext/AstNamedParameter.java index cc0134be5..2a78251eb 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/AstNamedParameter.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/AstNamedParameter.java @@ -7,6 +7,7 @@ import javax.el.ELContext; public class AstNamedParameter extends AstLiteral { + private final AstIdentifier name; private final AstNode value; diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AstRangeBracket.java b/src/main/java/com/hubspot/jinjava/el/ext/AstRangeBracket.java index 696b11bb7..97eccfee2 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/AstRangeBracket.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/AstRangeBracket.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.el.ext; import com.google.common.collect.Iterables; +import com.hubspot.jinjava.el.HasInterpreter; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.objects.collections.PyList; import com.hubspot.jinjava.objects.collections.SizeLimitingPyList; @@ -17,6 +18,7 @@ import javax.el.PropertyNotFoundException; public class AstRangeBracket extends AstBracket { + protected final AstNode rangeMax; public AstRangeBracket( @@ -78,9 +80,7 @@ public Object eval(Bindings bindings, ELContext context) { int startNum = ((Number) start).intValue(); int endNum = ((Number) end).intValue(); - JinjavaInterpreter interpreter = (JinjavaInterpreter) context - .getELResolver() - .getValue(context, null, ExtendedParser.INTERPRETER); + JinjavaInterpreter interpreter = ((HasInterpreter) context).interpreter(); PyList result = new SizeLimitingPyList( new ArrayList<>(), @@ -117,6 +117,9 @@ public Object eval(Bindings bindings, ELContext context) { } private String evalString(String base, Bindings bindings, ELContext context) { + if (base.length() == 0) { + return base; + } int startNum = intVal(property, 0, base.length(), bindings, context); int endNum = intVal(rangeMax, base.length(), base.length(), bindings, context); endNum = Math.min(endNum, base.length()); diff --git a/src/main/java/com/hubspot/jinjava/el/ext/BannedAllowlistOptions.java b/src/main/java/com/hubspot/jinjava/el/ext/BannedAllowlistOptions.java new file mode 100644 index 000000000..6949f4933 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/BannedAllowlistOptions.java @@ -0,0 +1,64 @@ +package com.hubspot.jinjava.el.ext; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableSet; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +public class BannedAllowlistOptions { + + // These aren't required, but they prevent someone from misconfiguring Jinjava to allow sandbox bypass unintentionally + private static final String JAVA_LANG_REFLECT_PACKAGE = + Method.class.getPackage().getName(); // java.lang.reflect + private static final String JACKSON_DATABIND_PACKAGE = + ObjectMapper.class.getPackage().getName(); // com.fasterxml.jackson.databind + + private static final String[] BANNED_PREFIXES = { + Class.class.getCanonicalName(), + Object.class.getCanonicalName(), + JAVA_LANG_REFLECT_PACKAGE, + JACKSON_DATABIND_PACKAGE, + }; + + private static final Set ALLOWED_JINJAVA_PREFIXES = Stream + .concat( + Stream.of("com.hubspot.jinjava.testobjects."), + Arrays + .stream(AllowlistGroup.values()) + .flatMap(g -> + Stream + .of( + g.allowedDeclaredMethodsFromCanonicalClassPrefixes(), + g.allowedReturnTypeCanonicalClassPrefixes(), + g.allowedDeclaredMethodsFromClasses(), + g.allowedReturnTypeClasses() + ) + .flatMap(Arrays::stream) + ) + ) + .collect(ImmutableSet.toImmutableSet()); + + public static List findBannedPrefixes(Stream prefixes) { + return prefixes + .filter(prefixOrName -> + Arrays + .stream(BANNED_PREFIXES) + .anyMatch(banned -> + banned.startsWith(prefixOrName) || prefixOrName.startsWith(banned) + ) || + isIllegalJinjavaClass(prefixOrName) + ) + .toList(); + } + + private static boolean isIllegalJinjavaClass(String prefixOrName) { + if (!prefixOrName.startsWith("com.hubspot.jinjava")) { + return false; + } + // e.g. com.hubspot.jinjava.lib.exptest is allowed, but com.hubspot.jinjava.Jinjava will not be + return ALLOWED_JINJAVA_PREFIXES.stream().noneMatch(prefixOrName::startsWith); + } +} 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..9210ca86d --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java @@ -0,0 +1,736 @@ +/* + * 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 { + + private static PropertyNotFoundException propertyNotFoundException = + new PropertyNotFoundException("Could not find property"); + + 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; + } + + // 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; + 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 = getReadMethod(base, property); + 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 = getWriteMethod(base, property); + 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, 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; + } + + // 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)); + } 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; + } + + protected Method getWriteMethod(Object base, Object property) { + return toBeanProperty(base, property).getWriteMethod(); + } + + protected Method getReadMethod(Object base, Object property) { + return toBeanProperty(base, property).getReadMethod(); + } + + protected 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. + * @return base != null + */ + private 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 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 propertyNotFoundException; + } + 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 loader + * The classLoader used to load the beans. + */ + @SuppressWarnings("unused") + private void purgeBeanClasses(ClassLoader loader) { + Iterator> classes = cache.keySet().iterator(); + while (classes.hasNext()) { + if (loader == classes.next().getClassLoader()) { + classes.remove(); + } + } + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/ext/CollectionMembershipOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/CollectionMembershipOperator.java index 28c93dd0a..662197cfe 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/CollectionMembershipOperator.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/CollectionMembershipOperator.java @@ -9,6 +9,7 @@ import de.odysseus.el.tree.impl.ast.AstBinary.SimpleOperator; import de.odysseus.el.tree.impl.ast.AstNode; import java.util.Collection; +import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; @@ -47,14 +48,30 @@ public Object apply(TypeConverter converter, Object o1, Object o2) { if (Map.class.isAssignableFrom(o2.getClass())) { Map map = (Map) o2; - if (!map.isEmpty()) { - try { - Class keyClass = map.keySet().iterator().next().getClass(); - return map.containsKey(converter.convert(o1, keyClass)); - } catch (ELException | NoSuchElementException e) { - return Boolean.FALSE; + // An implementation of Map can override isEmpty() to false, but return empty keySet. + if (map.isEmpty() || map.keySet().isEmpty()) { + return Boolean.FALSE; + } + Iterator iterator = map.keySet().iterator(); + Object key = iterator.next(); + if (key == null) { + if (o1 == null) { + return Boolean.TRUE; + } else { + if (iterator.hasNext()) { + // Must be non-null this time. + key = iterator.next(); + } else { + return Boolean.FALSE; + } } } + try { + Class keyClass = key.getClass(); + return map.containsKey(converter.convert(o1, keyClass)); + } catch (ELException | NoSuchElementException e) { + return Boolean.FALSE; + } } return Boolean.FALSE; @@ -65,7 +82,8 @@ public String toString() { return TOKEN.getImage(); } - public static final CollectionMembershipOperator OP = new CollectionMembershipOperator(); + public static final CollectionMembershipOperator OP = + new CollectionMembershipOperator(); public static final Scanner.ExtensionToken TOKEN = new Scanner.ExtensionToken("in"); public static final ExtensionHandler HANDLER = getHandler(false); @@ -73,7 +91,6 @@ public String toString() { private static ExtensionHandler getHandler(boolean eager) { return new ExtensionHandler(ExtensionPoint.CMP) { - @Override public AstNode createAstNode(AstNode... children) { return eager diff --git a/src/main/java/com/hubspot/jinjava/el/ext/CollectionNonMembershipOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/CollectionNonMembershipOperator.java index dc1e6b4f5..cd7bb681f 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/CollectionNonMembershipOperator.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/CollectionNonMembershipOperator.java @@ -21,8 +21,10 @@ public String toString() { return TOKEN.getImage(); } - public static final CollectionNonMembershipOperator NOT_IN_OP = new CollectionNonMembershipOperator(); - public static final CollectionMembershipOperator IN_OP = new CollectionMembershipOperator(); + public static final CollectionNonMembershipOperator NOT_IN_OP = + new CollectionNonMembershipOperator(); + public static final CollectionMembershipOperator IN_OP = + new CollectionMembershipOperator(); public static final Scanner.ExtensionToken TOKEN = new Scanner.ExtensionToken("not in"); public static final ExtensionHandler HANDLER = getHandler(false); @@ -30,7 +32,6 @@ public String toString() { private static ExtensionHandler getHandler(boolean eager) { return new ExtensionHandler(ExtensionPoint.CMP) { - @Override public AstNode createAstNode(AstNode... children) { return eager diff --git a/src/main/java/com/hubspot/jinjava/el/ext/DeferredInvocationResolutionException.java b/src/main/java/com/hubspot/jinjava/el/ext/DeferredInvocationResolutionException.java new file mode 100644 index 000000000..4a31f6277 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/DeferredInvocationResolutionException.java @@ -0,0 +1,8 @@ +package com.hubspot.jinjava.el.ext; + +public class DeferredInvocationResolutionException extends DeferredParsingException { + + public DeferredInvocationResolutionException(String invocationResultString) { + super(DeferredInvocationResolutionException.class, invocationResultString); + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/ext/DeferredParsingException.java b/src/main/java/com/hubspot/jinjava/el/ext/DeferredParsingException.java index c8842308e..b03d1e6e0 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/DeferredParsingException.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/DeferredParsingException.java @@ -3,16 +3,29 @@ import com.hubspot.jinjava.interpret.DeferredValueException; public class DeferredParsingException extends DeferredValueException { + private final String deferredEvalResult; private final Object sourceNode; + private final IdentifierPreservationStrategy identifierPreservationStrategy; - public DeferredParsingException(String message) { - super(message); - this.deferredEvalResult = message; - this.sourceNode = null; + public DeferredParsingException(Object sourceNode, String deferredEvalResult) { + super( + String.format( + "%s could not be parsed more than: %s", + sourceNode.getClass(), + deferredEvalResult + ) + ); + this.deferredEvalResult = deferredEvalResult; + this.sourceNode = sourceNode; + this.identifierPreservationStrategy = IdentifierPreservationStrategy.RESOLVING; } - public DeferredParsingException(Object sourceNode, String deferredEvalResult) { + public DeferredParsingException( + Object sourceNode, + String deferredEvalResult, + IdentifierPreservationStrategy identifierPreservationStrategy + ) { super( String.format( "%s could not be parsed more than: %s", @@ -22,6 +35,7 @@ public DeferredParsingException(Object sourceNode, String deferredEvalResult) { ); this.deferredEvalResult = deferredEvalResult; this.sourceNode = sourceNode; + this.identifierPreservationStrategy = identifierPreservationStrategy; } public String getDeferredEvalResult() { @@ -31,4 +45,8 @@ public String getDeferredEvalResult() { public Object getSourceNode() { return sourceNode; } + + public IdentifierPreservationStrategy getIdentifierPreservationStrategy() { + return identifierPreservationStrategy; + } } 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..a743aa2e0 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java @@ -51,6 +51,7 @@ import javax.el.ELException; public class ExtendedParser extends Parser { + public static final String INTERPRETER = "____int3rpr3t3r____"; public static final String FILTER_PREFIX = "filter:"; public static final String EXPTEST_PREFIX = "exptest:"; @@ -116,7 +117,6 @@ public ExtendedParser(Builder context, String input) { putExtensionHandler( PIPE, new ExtensionHandler(ExtensionPoint.AND) { - @Override public AstNode createAstNode(AstNode... children) { throw new ELException("Illegal use of '|' operator"); @@ -281,14 +281,16 @@ protected AstNode nonliteral() throws ScanException, ParseException { switch (getToken().getSymbol()) { case IDENTIFIER: String name = consumeToken().getImage(); - if ( - getToken().getSymbol() == COLON && - lookahead(0).getSymbol() == IDENTIFIER && - (lookahead(1).getSymbol() == LPAREN || (isPossibleExpTestOrFilter(name))) - ) { // ns:f(...) - consumeToken(); - name += ":" + getToken().getImage(); - consumeToken(); + if (getToken().getSymbol() == COLON && getToken().getImage().equals(":")) { + Symbol lookahead = lookahead(0).getSymbol(); + if ( + isPossibleExpTest(lookahead) && + (lookahead(1).getSymbol() == LPAREN || (isPossibleExpTestOrFilter(name))) + ) { // ns:f(...) + consumeToken(); + name += ":" + getToken().getImage(); + consumeToken(); + } } if (getToken().getSymbol() == LPAREN) { // function v = function(name, params()); @@ -529,30 +531,11 @@ protected AstNode value() throws ScanException, ParseException { private AstNode parseOperators(AstNode left) throws ScanException, ParseException { if ("|".equals(getToken().getImage()) && lookahead(0).getSymbol() == IDENTIFIER) { - AstNode v = left; - - do { - consumeToken(); // '|' - String filterName = consumeToken().getImage(); - List filterParams = Lists.newArrayList(v, interpreter()); - - // optional filter args - if (getToken().getSymbol() == Symbol.LPAREN) { - AstParameters astParameters = params(); - for (int i = 0; i < astParameters.getCardinality(); i++) { - filterParams.add(astParameters.getChild(i)); - } - } - - AstProperty filterProperty = createAstDot( - identifier(FILTER_PREFIX + filterName), - "filter", - true - ); - v = createAstMethod(filterProperty, createAstParameters(filterParams)); // function("filter:" + filterName, new AstParameters(filterParams)); - } while ("|".equals(getToken().getImage())); - - return v; + if (shouldUseFilterChainOptimization()) { + return parseFiltersAsChain(left); + } else { + return parseFiltersAsNestedMethods(left); + } } else if ( "is".equals(getToken().getImage()) && "not".equals(lookahead(0).getImage()) && @@ -575,6 +558,68 @@ protected AstParameters createAstParameters(List nodes) { return new AstParameters(nodes); } + protected AstFilterChain createAstFilterChain( + AstNode input, + List filterSpecs + ) { + return new AstFilterChain(input, filterSpecs); + } + + private AstNode parseFiltersAsChain(AstNode left) throws ScanException, ParseException { + List filterSpecs = new ArrayList<>(); + + do { + consumeToken(); // '|' + String filterName = consumeToken().getImage(); + AstParameters filterParams = null; + + // optional filter args + if (getToken().getSymbol() == Symbol.LPAREN) { + filterParams = params(); + } + + filterSpecs.add(new FilterSpec(filterName, filterParams)); + } while ("|".equals(getToken().getImage())); + + return createAstFilterChain(left, filterSpecs); + } + + protected AstNode parseFiltersAsNestedMethods(AstNode left) + throws ScanException, ParseException { + AstNode v = left; + + do { + consumeToken(); // '|' + String filterName = consumeToken().getImage(); + List filterParams = Lists.newArrayList(v, interpreter()); + + // optional filter args + if (getToken().getSymbol() == Symbol.LPAREN) { + AstParameters astParameters = params(); + for (int i = 0; i < astParameters.getCardinality(); i++) { + filterParams.add(astParameters.getChild(i)); + } + } + + AstProperty filterProperty = createAstDot( + identifier(FILTER_PREFIX + filterName), + "filter", + true + ); + v = createAstMethod(filterProperty, createAstParameters(filterParams)); + } while ("|".equals(getToken().getImage())); + + return v; + } + + protected boolean shouldUseFilterChainOptimization() { + return JinjavaInterpreter + .getCurrentMaybe() + .map(JinjavaInterpreter::getConfig) + .map(JinjavaConfig::isEnableFilterChainOptimization) + .orElse(false); + } + private boolean isPossibleExpTest(Symbol symbol) { return VALID_SYMBOLS_FOR_EXP_TEST.contains(symbol); } @@ -583,9 +628,9 @@ private boolean isPossibleExpTestOrFilter(String namespace) throws ParseException, ScanException { if ( FILTER_PREFIX.substring(0, FILTER_PREFIX.length() - 1).equals(namespace) || - EXPTEST_PREFIX.substring(0, EXPTEST_PREFIX.length() - 1).equals(namespace) && - lookahead(1).getSymbol() == DOT && - lookahead(2).getSymbol() == IDENTIFIER + (EXPTEST_PREFIX.substring(0, EXPTEST_PREFIX.length() - 1).equals(namespace) && + lookahead(1).getSymbol() == DOT && + lookahead(2).getSymbol() == IDENTIFIER) ) { Token property = lookahead(2); if ( @@ -624,7 +669,6 @@ protected Scanner createScanner(String expression) { } private static final ExtensionHandler NULL_EXT_HANDLER = new ExtensionHandler(null) { - @Override public AstNode createAstNode(AstNode... children) { return null; diff --git a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedScanner.java b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedScanner.java index bf54da80f..1116c275b 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedScanner.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedScanner.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.el.ext; import de.odysseus.el.tree.impl.Scanner; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -58,6 +59,10 @@ protected boolean isWhitespace(char c) { } } + @SuppressFBWarnings( + value = "HSM_HIDING_METHOD", + justification = "Purposefully overriding to use static method instance of this class." + ) protected static void addKeyToken(Token token) { try { ADD_KEY_TOKEN_METHOD.invoke(null, token); diff --git a/src/main/java/com/hubspot/jinjava/el/ext/FilterSpec.java b/src/main/java/com/hubspot/jinjava/el/ext/FilterSpec.java new file mode 100644 index 000000000..175016913 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/FilterSpec.java @@ -0,0 +1,48 @@ +package com.hubspot.jinjava.el.ext; + +import de.odysseus.el.tree.impl.ast.AstParameters; +import java.util.Objects; + +/** + * Specification for a filter in a filter chain. + * Holds the filter name and optional parameters. + */ +public class FilterSpec { + + private final String name; + private final AstParameters params; + + public FilterSpec(String name, AstParameters params) { + this.name = Objects.requireNonNull(name, "Filter name cannot be null"); + this.params = params; + } + + public String getName() { + return name; + } + + public AstParameters getParams() { + return params; + } + + public boolean hasParams() { + return params != null && params.getCardinality() > 0; + } + + @Override + public String toString() { + if (hasParams()) { + StringBuilder sb = new StringBuilder(name); + sb.append('('); + for (int i = 0; i < params.getCardinality(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(params.getChild(i)); + } + sb.append(')'); + return sb.toString(); + } + return name; + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/ext/IdentifierPreservationStrategy.java b/src/main/java/com/hubspot/jinjava/el/ext/IdentifierPreservationStrategy.java new file mode 100644 index 000000000..d3e6536dd --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/IdentifierPreservationStrategy.java @@ -0,0 +1,20 @@ +package com.hubspot.jinjava.el.ext; + +public enum IdentifierPreservationStrategy { + PRESERVING(true), + RESOLVING(false); + + public static IdentifierPreservationStrategy preserving(boolean preserveIdentifier) { + return preserveIdentifier ? PRESERVING : RESOLVING; + } + + private final boolean preserving; + + IdentifierPreservationStrategy(boolean preserving) { + this.preserving = preserving; + } + + public boolean isPreserving() { + return preserving; + } +} 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..dad811583 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java @@ -2,68 +2,133 @@ 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 java.lang.reflect.Constructor; -import java.lang.reflect.Field; +import com.hubspot.jinjava.util.EagerReconstructionUtils; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.MethodDescriptor; +import java.lang.invoke.MethodType; +import java.lang.reflect.Array; import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; import java.util.Set; -import javax.el.BeanELResolver; +import java.util.concurrent.ConcurrentHashMap; import javax.el.ELContext; +import javax.el.ELException; +import javax.el.ExpressionFactory; import javax.el.MethodNotFoundException; /** * {@link BeanELResolver} supporting snake case property names. */ public class JinjavaBeanELResolver extends BeanELResolver { - private static final Set RESTRICTED_PROPERTIES = ImmutableSet - .builder() - .add("class") - .build(); - private static final Set RESTRICTED_METHODS = ImmutableSet + private static final Set DEFERRED_EXECUTION_RESTRICTED_METHODS = ImmutableSet .builder() - .add("class") - .add("clone") - .add("hashCode") - .add("getClass") - .add("getDeclaringClass") - .add("forName") - .add("notify") - .add("notifyAll") - .add("wait") + .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}. - */ - public JinjavaBeanELResolver() {} + protected static final class BeanMethods { + + private final Map> map = new HashMap<>(); + + public BeanMethods(Class baseClass) { + MethodDescriptor[] descriptors; + try { + descriptors = Introspector.getBeanInfo(baseClass).getMethodDescriptors(); + } catch (IntrospectionException e) { + throw new ELException(e); + } + for (MethodDescriptor descriptor : descriptors) { + map.compute( + descriptor.getName(), + (k, v) -> { + if (v == null) { + v = new LinkedList<>(); + } + v.add(new BeanMethod(descriptor)); + return v; + } + ); + } + } + + public List getBeanMethods(String methodName) { + return map.get(methodName); + } + + protected static final class BeanMethod { + + private final MethodDescriptor descriptor; + + private volatile Method method; + + public BeanMethod(MethodDescriptor descriptor) { + this.descriptor = descriptor; + } + + public Method getMethod() { + if (method == null) { + method = findAccessibleMethod(descriptor.getMethod()); + } + return method; + } + } + } + + private final ConcurrentHashMap, BeanMethods> beanMethodsCache; + + public JinjavaBeanELResolver() { + this(true); + } /** - * Creates a new {@link JinjavaBeanELResolver} whose read-only status is determined by the given parameter. + * Creates a new read/write {@link JinjavaBeanELResolver}. */ public JinjavaBeanELResolver(boolean readOnly) { super(readOnly); + this.beanMethodsCache = new ConcurrentHashMap, BeanMethods>(); } @Override public Class getType(ELContext context, Object base, Object property) { - return super.getType(context, base, validatePropertyName(property)); + return super.getType(context, base, transformPropertyName(property)); } @Override public Object getValue(ELContext context, Object base, Object property) { - Object result = super.getValue(context, base, validatePropertyName(property)); - return result instanceof Class ? null : result; + return super.getValue(context, base, transformPropertyName(property)); } @Override public boolean isReadOnly(ELContext context, Object base, Object property) { - return super.isReadOnly(context, base, validatePropertyName(property)); + return super.isReadOnly(context, base, transformPropertyName(property)); } @Override public void setValue(ELContext context, Object base, Object property, Object value) { - super.setValue(context, base, validatePropertyName(property), value); + super.setValue(context, base, transformPropertyName(property), value); } @Override @@ -74,37 +139,134 @@ public Object invoke( Class[] paramTypes, Object[] params ) { - if (method == null || RESTRICTED_METHODS.contains(method.toString())) { - throw new MethodNotFoundException( - "Cannot find method '" + method + "' in " + base.getClass() + if (method == null) { + throw new MethodNotFoundException("Cannot find method null in " + base.getClass()); + } + 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() + ) ); } - if (isRestrictedClass(base)) { - throw new MethodNotFoundException( - "Cannot find method '" + method + "' in " + base.getClass() - ); + return super.invoke(context, base, method, paramTypes, params); + } + + // As opposed to supporting ____int3rpr3t3r____, coerce JinjavaInterpreter on validated methods + @Override + protected void coerceValue( + Object array, + int index, + ExpressionFactory factory, + Object value, + Class type + ) { + if (type.equals(JinjavaInterpreter.class)) { + Array.set(array, index, JinjavaInterpreter.getCurrent()); // Could be null if there's no current interpreter + } else { + super.coerceValue(array, index, factory, value, type); } + } + + @Override + protected Method findMethod( + Object base, + String name, + Class[] types, + Object[] params, + int paramCount + ) { + Method method; + if (types != null) { + method = super.findMethod(base, name, types, params, paramCount); + } else { + Method varArgsMethod = null; + BeanMethods beanMethods = beanMethodsCache.get(base.getClass()); + if (beanMethods == null) { + BeanMethods newBeanMethods = new BeanMethods(base.getClass()); + beanMethods = beanMethodsCache.putIfAbsent(base.getClass(), newBeanMethods); + if (beanMethods == null) { // put succeeded, use new value + beanMethods = newBeanMethods; + } + } - Object result = super.invoke(context, base, method, paramTypes, params); + List potentialMethods = new LinkedList<>(); - if (isRestrictedClass(result)) { - throw new MethodNotFoundException( - "Cannot find method '" + method + "' in " + base.getClass() - ); + List methodsForName = beanMethods.getBeanMethods(name); + if (methodsForName == null) { + methodsForName = List.of(); + } + for (BeanMethods.BeanMethod bm : methodsForName) { + Method m = bm.getMethod(); + int formalParamCount = m.getParameterTypes().length; + if (m.isVarArgs() && paramCount >= formalParamCount - 1) { + varArgsMethod = m; + } else if (paramCount == formalParamCount) { + potentialMethods.add(m); + } + } + final Method finalVarArgsMethod = varArgsMethod; + method = + potentialMethods + .stream() + .filter(m -> checkAssignableParameterTypes(params, m)) + .min(JinjavaBeanELResolver::pickMoreSpecificMethod) + .orElseGet(() -> potentialMethods.stream().findAny().orElse(finalVarArgsMethod) + ); } + return getAllowlistMethodValidator().validateMethod(method); + } - return result; + @Override + protected Method getWriteMethod(Object base, Object property) { + return getAllowlistMethodValidator() + .validateMethod(super.getWriteMethod(base, property)); } - private String validatePropertyName(Object property) { - String propertyName = transformPropertyName(property); + @Override + protected Method getReadMethod(Object base, Object property) { + return getAllowlistMethodValidator() + .validateMethod(super.getReadMethod(base, property)); + } - if (RESTRICTED_PROPERTIES.contains(propertyName)) { - return null; + private static AllowlistMethodValidator getAllowlistMethodValidator() { + return JinjavaInterpreter + .getCurrentMaybe() + .map(interpreter -> interpreter.getConfig().getMethodValidator()) + .orElse(AllowlistMethodValidator.DEFAULT); + } + + 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; + } - return propertyName; + 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; } /** @@ -121,24 +283,4 @@ private String transformPropertyName(Object property) { } return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, propertyStr); } - - protected boolean isRestrictedClass(Object o) { - if (o == null) { - return false; - } - - return ( - ( - o.getClass().getPackage() != null && - o.getClass().getPackage().getName().startsWith("java.lang.reflect") - ) || - o instanceof Class || - o instanceof ClassLoader || - o instanceof Thread || - o instanceof Method || - o instanceof Field || - o instanceof Constructor || - o instanceof JinjavaInterpreter - ); - } } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/MethodValidator.java b/src/main/java/com/hubspot/jinjava/el/ext/MethodValidator.java new file mode 100644 index 000000000..eb065b7d2 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/MethodValidator.java @@ -0,0 +1,10 @@ +package com.hubspot.jinjava.el.ext; + +import java.lang.reflect.Method; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public interface MethodValidator { + @Nullable + Method validateMethod(@Nonnull Method m); +} diff --git a/src/main/java/com/hubspot/jinjava/el/ext/MethodValidatorConfig.java b/src/main/java/com/hubspot/jinjava/el/ext/MethodValidatorConfig.java new file mode 100644 index 000000000..52ecea9e1 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/MethodValidatorConfig.java @@ -0,0 +1,77 @@ +package com.hubspot.jinjava.el.ext; + +import com.google.common.collect.ImmutableSet; +import com.hubspot.jinjava.JinjavaImmutableStyle; +import java.lang.reflect.Method; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; +import org.immutables.value.Value; + +@Value.Immutable(singleton = true) +@JinjavaImmutableStyle +public abstract class MethodValidatorConfig { + + public abstract ImmutableSet allowedMethods(); + + public abstract ImmutableSet allowedDeclaredMethodsFromCanonicalClassPrefixes(); + + public abstract ImmutableSet allowedDeclaredMethodsFromCanonicalClassNames(); + + @Value.Default + public Consumer onRejectedMethod() { + return m -> {}; + } + + @Value.Check + void banClassesAndMethods() { + List list = BannedAllowlistOptions.findBannedPrefixes( + Stream + .of( + allowedMethods() + .stream() + .map(method -> method.getDeclaringClass().getCanonicalName()), + allowedDeclaredMethodsFromCanonicalClassPrefixes().stream(), + allowedDeclaredMethodsFromCanonicalClassNames().stream() + ) + .flatMap(Function.identity()) + ); + if (!list.isEmpty()) { + throw new IllegalStateException( + "Banned classes or prefixes (Object.class, Class.class, java.lang.reflect, com.fasterxml.jackson.databind) are not allowed: " + + list + ); + } + } + + public static MethodValidatorConfig of() { + return ImmutableMethodValidatorConfig.of(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends ImmutableMethodValidatorConfig.Builder { + + Builder() {} + + public Builder addDefaultAllowlistGroups() { + return addAllowlistGroups(AllowlistGroup.values()); + } + + public Builder addAllowlistGroups(AllowlistGroup... allowlistGroups) { + for (AllowlistGroup allowlistGroup : allowlistGroups) { + this.addAllowedMethods(allowlistGroup.allowMethods()) + .addAllowedDeclaredMethodsFromCanonicalClassPrefixes( + allowlistGroup.allowedDeclaredMethodsFromCanonicalClassPrefixes() + ) + .addAllowedDeclaredMethodsFromCanonicalClassNames( + allowlistGroup.allowedDeclaredMethodsFromClasses() + ); + } + return this; + } + } +} 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..b47ba1133 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java @@ -1,9 +1,11 @@ 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 { + private final String name; private final Object value; @@ -26,7 +28,12 @@ public String toString() { } @Override - public String toPyishString() { - return name + "=" + PyishSerializable.writeValueAsString(value); + @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/el/ext/NamedParameterOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/NamedParameterOperator.java index 369995c80..3d50953de 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/NamedParameterOperator.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/NamedParameterOperator.java @@ -9,13 +9,13 @@ import javax.el.ELException; public class NamedParameterOperator { + public static final Scanner.ExtensionToken TOKEN = new Scanner.ExtensionToken("="); public static final ExtensionHandler HANDLER = getHandler(false); public static ExtensionHandler getHandler(boolean eager) { return new ExtensionHandler(ExtensionPoint.ADD) { - @Override public AstNode createAstNode(AstNode... children) { if (!(children[0] instanceof AstIdentifier)) { diff --git a/src/main/java/com/hubspot/jinjava/el/ext/PowerOfOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/PowerOfOperator.java index 166a42cc8..1858e67c7 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/PowerOfOperator.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/PowerOfOperator.java @@ -10,6 +10,7 @@ import de.odysseus.el.tree.impl.ast.AstNode; public class PowerOfOperator extends SimpleOperator { + public static final Scanner.ExtensionToken TOKEN = new Scanner.ExtensionToken("**"); public static final PowerOfOperator OP = new PowerOfOperator(); @@ -50,7 +51,6 @@ public String toString() { public static ExtensionHandler getHandler(boolean eager) { return new ExtensionHandler(ExtensionPoint.MUL) { - @Override public AstNode createAstNode(AstNode... children) { return eager diff --git a/src/main/java/com/hubspot/jinjava/el/ext/ReturnTypeValidator.java b/src/main/java/com/hubspot/jinjava/el/ext/ReturnTypeValidator.java new file mode 100644 index 000000000..f60f0fe2a --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/ReturnTypeValidator.java @@ -0,0 +1,8 @@ +package com.hubspot.jinjava.el.ext; + +import javax.annotation.Nullable; + +public interface ReturnTypeValidator { + @Nullable + Object validateReturnType(Object o); +} diff --git a/src/main/java/com/hubspot/jinjava/el/ext/ReturnTypeValidatorConfig.java b/src/main/java/com/hubspot/jinjava/el/ext/ReturnTypeValidatorConfig.java new file mode 100644 index 000000000..8a885c519 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/ReturnTypeValidatorConfig.java @@ -0,0 +1,76 @@ +package com.hubspot.jinjava.el.ext; + +import com.google.common.collect.ImmutableSet; +import com.hubspot.jinjava.JinjavaImmutableStyle; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; +import org.immutables.value.Value; + +@Value.Immutable(singleton = true) +@JinjavaImmutableStyle +public abstract class ReturnTypeValidatorConfig { + + public abstract ImmutableSet allowedCanonicalClassPrefixes(); + + public abstract ImmutableSet allowedCanonicalClassNames(); + + @Value.Default + public Consumer> onRejectedClass() { + return m -> {}; + } + + @Value.Default + public boolean allowArrays() { + return false; + } + + @Value.Check + void banClassesAndMethods() { + List list = BannedAllowlistOptions.findBannedPrefixes( + Stream + .of( + allowedCanonicalClassPrefixes().stream(), + allowedCanonicalClassNames().stream() + ) + .flatMap(Function.identity()) + ); + if (!list.isEmpty()) { + throw new IllegalStateException( + "Banned classes or prefixes (Object.class, Class.class, java.lang.reflect, com.fasterxml.jackson.databind) are not allowed: " + + list + ); + } + } + + public static ReturnTypeValidatorConfig of() { + return ImmutableReturnTypeValidatorConfig.of(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends ImmutableReturnTypeValidatorConfig.Builder { + + Builder() {} + + public Builder addDefaultAllowlistGroups() { + return addAllowlistGroups(AllowlistGroup.values()); + } + + public Builder addAllowlistGroups(AllowlistGroup... allowlistGroups) { + for (AllowlistGroup allowlistGroup : allowlistGroups) { + this.addAllowedCanonicalClassPrefixes( + allowlistGroup.allowedReturnTypeCanonicalClassPrefixes() + ) + .addAllowedCanonicalClassNames(allowlistGroup.allowedReturnTypeClasses()); + if (allowlistGroup.enableArrays()) { + this.setAllowArrays(true); + } + } + return this; + } + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/ext/StringBuildingOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/StringBuildingOperator.java new file mode 100644 index 000000000..47296089f --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/StringBuildingOperator.java @@ -0,0 +1,16 @@ +package com.hubspot.jinjava.el.ext; + +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.util.LengthLimitingStringBuilder; + +public interface StringBuildingOperator { + default LengthLimitingStringBuilder getStringBuilder() { + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); + + long maxSize = (interpreter == null || interpreter.getConfig() == null) + ? 0 + : interpreter.getConfig().getMaxOutputSize(); + + return new LengthLimitingStringBuilder(maxSize); + } +} diff --git a/src/main/java/com/hubspot/jinjava/el/ext/StringConcatOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/StringConcatOperator.java index d00073eee..16ddc66bc 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/StringConcatOperator.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/StringConcatOperator.java @@ -9,14 +9,16 @@ import de.odysseus.el.tree.impl.ast.AstBinary.SimpleOperator; import de.odysseus.el.tree.impl.ast.AstNode; -public class StringConcatOperator extends SimpleOperator { +public class StringConcatOperator + extends SimpleOperator + implements StringBuildingOperator { @Override protected Object apply(TypeConverter converter, Object o1, Object o2) { String o1s = converter.convert(o1, String.class); String o2s = converter.convert(o2, String.class); - return new StringBuilder(o1s).append(o2s).toString(); + return getStringBuilder().append(o1s).append(o2s).toString(); } @Override @@ -31,7 +33,6 @@ public String toString() { public static ExtensionHandler getHandler(boolean eager) { return new ExtensionHandler(ExtensionPoint.ADD) { - @Override public AstNode createAstNode(AstNode... children) { return eager diff --git a/src/main/java/com/hubspot/jinjava/el/ext/TruncDivOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/TruncDivOperator.java index d8b4fd146..b28e4d0a1 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/TruncDivOperator.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/TruncDivOperator.java @@ -10,6 +10,7 @@ import de.odysseus.el.tree.impl.ast.AstNode; public class TruncDivOperator extends SimpleOperator { + public static final Scanner.ExtensionToken TOKEN = new Scanner.ExtensionToken("//"); public static final TruncDivOperator OP = new TruncDivOperator(); @@ -50,7 +51,6 @@ public String toString() { public static ExtensionHandler getHandler(boolean eager) { return new ExtensionHandler(ExtensionPoint.MUL) { - @Override public AstNode createAstNode(AstNode... children) { return eager diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBinary.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBinary.java index 95d92e917..ea8c27ef1 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBinary.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBinary.java @@ -2,6 +2,7 @@ import com.hubspot.jinjava.el.NoInvokeELContext; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import com.hubspot.jinjava.el.ext.OrOperator; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstBinary; @@ -9,6 +10,7 @@ import javax.el.ELContext; public class EagerAstBinary extends AstBinary implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; protected final EvalResultHolder left; @@ -48,7 +50,7 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { return ( EvalResultHolder.reconstructNode( @@ -56,7 +58,7 @@ public String getPartiallyResolved( context, left, deferredParsingException, - false + IdentifierPreservationStrategy.RESOLVING ) + String.format(" %s ", operator.toString()) + EvalResultHolder.reconstructNode( @@ -66,7 +68,7 @@ public String getPartiallyResolved( : context, right, deferredParsingException, - false + IdentifierPreservationStrategy.RESOLVING ) ); } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBracket.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBracket.java index 85bd00346..c02d71524 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBracket.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBracket.java @@ -1,12 +1,14 @@ package com.hubspot.jinjava.el.ext.eager; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstBracket; import de.odysseus.el.tree.impl.ast.AstNode; import javax.el.ELContext; public class EagerAstBracket extends AstBracket implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; @@ -63,7 +65,7 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { return String.format( "%s[%s]", @@ -72,14 +74,14 @@ public String getPartiallyResolved( context, (EvalResultHolder) prefix, deferredParsingException, - preserveIdentifier + identifierPreservationStrategy ), EvalResultHolder.reconstructNode( bindings, context, (EvalResultHolder) property, deferredParsingException, - false + IdentifierPreservationStrategy.RESOLVING ) ); } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstChoice.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstChoice.java index dd5d0472f..b4bbd43dc 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstChoice.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstChoice.java @@ -2,6 +2,7 @@ import com.hubspot.jinjava.el.NoInvokeELContext; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstChoice; import de.odysseus.el.tree.impl.ast.AstNode; @@ -9,6 +10,7 @@ import javax.el.ELException; public class EagerAstChoice extends AstChoice implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; protected final EvalResultHolder question; @@ -46,7 +48,12 @@ public Object eval(Bindings bindings, ELContext context) throws ELException { } throw new DeferredParsingException( this, - getPartiallyResolved(bindings, context, e, false) + getPartiallyResolved( + bindings, + context, + e, + IdentifierPreservationStrategy.RESOLVING + ) ); } } @@ -72,7 +79,7 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { return ( EvalResultHolder.reconstructNode( @@ -80,7 +87,7 @@ public String getPartiallyResolved( context, question, deferredParsingException, - false + IdentifierPreservationStrategy.RESOLVING ) + " ? " + EvalResultHolder.reconstructNode( @@ -88,7 +95,7 @@ public String getPartiallyResolved( new NoInvokeELContext(context), yes, deferredParsingException, - preserveIdentifier + identifierPreservationStrategy ) + " : " + EvalResultHolder.reconstructNode( @@ -96,7 +103,7 @@ public String getPartiallyResolved( new NoInvokeELContext(context), no, deferredParsingException, - preserveIdentifier + identifierPreservationStrategy ) ); } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDict.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDict.java index 48302a4a1..8bdff2434 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDict.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDict.java @@ -1,8 +1,9 @@ package com.hubspot.jinjava.el.ext.eager; +import com.hubspot.jinjava.el.HasInterpreter; import com.hubspot.jinjava.el.ext.AstDict; import com.hubspot.jinjava.el.ext.DeferredParsingException; -import com.hubspot.jinjava.el.ext.ExtendedParser; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.util.EagerExpressionResolver; import de.odysseus.el.tree.Bindings; @@ -13,6 +14,7 @@ import javax.el.ELContext; public class EagerAstDict extends AstDict implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; @@ -34,54 +36,50 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { - JinjavaInterpreter interpreter = (JinjavaInterpreter) context - .getELResolver() - .getValue(context, null, ExtendedParser.INTERPRETER); + JinjavaInterpreter interpreter = ((HasInterpreter) context).interpreter(); StringJoiner joiner = new StringJoiner(", "); - dict.forEach( - (key, value) -> { - StringJoiner kvJoiner = new StringJoiner(": "); - if (key instanceof AstIdentifier) { - kvJoiner.add(((AstIdentifier) key).getName()); - } else if (key instanceof EvalResultHolder) { - kvJoiner.add( - EvalResultHolder.reconstructNode( - bindings, - context, - (EvalResultHolder) key, - deferredParsingException, + dict.forEach((key, value) -> { + StringJoiner kvJoiner = new StringJoiner(": "); + if (key instanceof AstIdentifier) { + kvJoiner.add(((AstIdentifier) key).getName()); + } else if (key instanceof EvalResultHolder) { + kvJoiner.add( + EvalResultHolder.reconstructNode( + bindings, + context, + (EvalResultHolder) key, + deferredParsingException, + IdentifierPreservationStrategy.preserving( !interpreter.getConfig().getLegacyOverrides().isEvaluateMapKeys() ) - ); - } else { - kvJoiner.add( - EagerExpressionResolver.getValueAsJinjavaStringSafe( - key.eval(bindings, context) - ) - ); - } - if (value instanceof EvalResultHolder) { - kvJoiner.add( - EvalResultHolder.reconstructNode( - bindings, - context, - (EvalResultHolder) value, - deferredParsingException, - preserveIdentifier - ) - ); - } else { - kvJoiner.add( - EagerExpressionResolver.getValueAsJinjavaStringSafe( - value.eval(bindings, context) - ) - ); - } - joiner.add(kvJoiner.toString()); + ) + ); + } else { + kvJoiner.add( + EagerExpressionResolver.getValueAsJinjavaStringSafe(key.eval(bindings, context)) + ); } - ); + if (value instanceof EvalResultHolder) { + kvJoiner.add( + EvalResultHolder.reconstructNode( + bindings, + context, + (EvalResultHolder) value, + deferredParsingException, + identifierPreservationStrategy + ) + ); + } else { + kvJoiner.add( + EagerExpressionResolver.getValueAsJinjavaStringSafe( + value.eval(bindings, context) + ) + ); + } + joiner.add(kvJoiner.toString()); + }); String joined = joiner.toString(); if (joined.endsWith("}")) { // prevent 2 closing braces from being interpreted as a closing expression token diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDot.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDot.java index 862d9620d..67638b1c5 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDot.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDot.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.el.ext.eager; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstDot; import de.odysseus.el.tree.impl.ast.AstNode; @@ -8,6 +9,7 @@ import javax.el.ELException; public class EagerAstDot extends AstDot implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; protected final EvalResultHolder base; @@ -52,11 +54,17 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException e, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { return String.format( "%s.%s", - EvalResultHolder.reconstructNode(bindings, context, base, e, preserveIdentifier), + EvalResultHolder.reconstructNode( + bindings, + context, + base, + e, + identifierPreservationStrategy + ), property ); } 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..86ef61009 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,11 +1,13 @@ package com.hubspot.jinjava.el.ext.eager; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstIdentifier; import javax.el.ELContext; public class EagerAstIdentifier extends AstIdentifier implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; @@ -43,7 +45,7 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { return getName(); } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstList.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstList.java index 6ca90e4a1..fc1272366 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstList.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstList.java @@ -2,12 +2,14 @@ import com.hubspot.jinjava.el.ext.AstList; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstParameters; import java.util.StringJoiner; import javax.el.ELContext; public class EagerAstList extends AstList implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; @@ -45,7 +47,7 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { StringJoiner joiner = new StringJoiner(", "); for (int i = 0; i < elements.getCardinality(); i++) { @@ -55,7 +57,7 @@ public String getPartiallyResolved( context, (EvalResultHolder) elements.getChild(i), deferredParsingException, - preserveIdentifier + identifierPreservationStrategy ) ); } 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..85d2bb8fd 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 @@ -1,21 +1,22 @@ package com.hubspot.jinjava.el.ext.eager; +import com.hubspot.jinjava.el.HasInterpreter; import com.hubspot.jinjava.el.ext.AstMacroFunction; +import com.hubspot.jinjava.el.ext.DeferredInvocationResolutionException; import com.hubspot.jinjava.el.ext.DeferredParsingException; -import com.hubspot.jinjava.el.ext.ExtendedParser; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; import com.hubspot.jinjava.interpret.DeferredValueException; -import com.hubspot.jinjava.interpret.JinjavaInterpreter; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstParameters; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.StringJoiner; import javax.el.ELContext; import javax.el.ELException; public class EagerAstMacroFunction extends AstMacroFunction implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; // instanceof AstParameters @@ -53,7 +54,13 @@ public Object eval(Bindings bindings, ELContext context) { ); throw new DeferredParsingException( this, - getPartiallyResolved(bindings, context, e, true) // Need this to always be true because the macro function may modify the identifier + getPartiallyResolved( + bindings, + context, + e, + IdentifierPreservationStrategy.PRESERVING + ), // Need this to always be true because the macro function may modify the identifier + IdentifierPreservationStrategy.PRESERVING ); } } @@ -64,19 +71,16 @@ protected Object invoke( ELContext context, Object base, Method method - ) - throws InvocationTargetException, IllegalAccessException { + ) throws InvocationTargetException, IllegalAccessException { Class[] types = method.getParameterTypes(); Object[] params = null; if (types.length > 0) { // This is just the AstFunction.invoke, but surrounded with this try-with-resources try ( - TemporaryValueClosable c = ( - (JinjavaInterpreter) context - .getELResolver() - .getValue(context, null, ExtendedParser.INTERPRETER) - ).getContext() - .withPartialMacroEvaluation(false) + TemporaryValueClosable c = + ((HasInterpreter) context).interpreter() + .getContext() + .withPartialMacroEvaluation(false) ) { params = new Object[types.length]; int varargIndex; @@ -145,28 +149,25 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { - StringBuilder sb = new StringBuilder(); - sb.append(getName()); - try { - StringJoiner paramString = new StringJoiner(", "); - for (int i = 0; i < ((AstParameters) params).getCardinality(); i++) { - paramString.add( - EvalResultHolder.reconstructNode( - bindings, - context, - (EvalResultHolder) ((AstParameters) params).getChild(i), - deferredParsingException, - preserveIdentifier - ) + if (deferredParsingException instanceof DeferredInvocationResolutionException) { + return deferredParsingException.getDeferredEvalResult(); + } + String paramString; + if (EvalResultHolder.exceptionMatchesNode(deferredParsingException, params)) { + paramString = deferredParsingException.getDeferredEvalResult(); + } else { + paramString = + params.getPartiallyResolved( + bindings, + context, + deferredParsingException, + identifierPreservationStrategy ); - } - sb.append(String.format("(%s)", paramString)); - } catch (DeferredParsingException dpe) { - sb.append(String.format("(%s)", dpe.getDeferredEvalResult())); } - return sb.toString(); + + return (getName() + String.format("(%s)", paramString)); } @Override diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethod.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethod.java index 366195214..aec8fa234 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethod.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethod.java @@ -1,17 +1,18 @@ package com.hubspot.jinjava.el.ext.eager; +import com.hubspot.jinjava.el.ext.DeferredInvocationResolutionException; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import com.hubspot.jinjava.interpret.DeferredValueException; -import com.hubspot.jinjava.util.EagerExpressionResolver; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstMethod; -import de.odysseus.el.tree.impl.ast.AstNode; import de.odysseus.el.tree.impl.ast.AstParameters; import de.odysseus.el.tree.impl.ast.AstProperty; import javax.el.ELContext; import javax.el.ELException; public class EagerAstMethod extends AstMethod implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; // instanceof AstProperty @@ -43,7 +44,13 @@ public Object eval(Bindings bindings, ELContext context) { ); throw new DeferredParsingException( this, - getPartiallyResolved(bindings, context, e, true) // Need this to always be true because the method may modify the identifier + getPartiallyResolved( + bindings, + context, + e, + IdentifierPreservationStrategy.PRESERVING + ), // Need this to always be true because the method may modify the identifier + IdentifierPreservationStrategy.PRESERVING ); } } @@ -73,33 +80,30 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { + if (deferredParsingException instanceof DeferredInvocationResolutionException) { + return deferredParsingException.getDeferredEvalResult(); + } String propertyResult; propertyResult = (property).getPartiallyResolved( bindings, context, deferredParsingException, - preserveIdentifier + identifierPreservationStrategy ); String paramString; - if ( - deferredParsingException != null && - deferredParsingException.getSourceNode() == params - ) { + if (EvalResultHolder.exceptionMatchesNode(deferredParsingException, params)) { paramString = deferredParsingException.getDeferredEvalResult(); } else { - try { - paramString = - EagerExpressionResolver.getValueAsJinjavaStringSafe( - ((AstNode) params).eval(bindings, context) - ); - // remove brackets so they can get replaced with parentheses - paramString = paramString.substring(1, paramString.length() - 1); - } catch (DeferredParsingException e) { - paramString = e.getDeferredEvalResult(); - } + paramString = + params.getPartiallyResolved( + bindings, + context, + deferredParsingException, + identifierPreservationStrategy + ); } return (propertyResult + String.format("(%s)", paramString)); diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNamedParameter.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNamedParameter.java index 8d1a23c61..0cedd9839 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNamedParameter.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNamedParameter.java @@ -2,6 +2,7 @@ import com.hubspot.jinjava.el.ext.AstNamedParameter; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstIdentifier; import de.odysseus.el.tree.impl.ast.AstNode; @@ -10,6 +11,7 @@ public class EagerAstNamedParameter extends AstNamedParameter implements EvalResultHolder { + protected boolean hasEvalResult; protected Object evalResult; protected final AstIdentifier name; @@ -39,7 +41,7 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { return String.format( "%s=%s", @@ -49,7 +51,7 @@ public String getPartiallyResolved( context, value, deferredParsingException, - false + IdentifierPreservationStrategy.RESOLVING ) ); } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNested.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNested.java index 317167c11..03d016175 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNested.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNested.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.el.ext.eager; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.Node; import de.odysseus.el.tree.impl.ast.AstNode; @@ -11,6 +12,7 @@ * AstNested is final so this decorates AstRightValue. */ public class EagerAstNested extends AstRightValue implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; protected final AstNode child; @@ -71,7 +73,7 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { return String.format( "(%s)", @@ -80,7 +82,7 @@ public String getPartiallyResolved( context, (EvalResultHolder) child, deferredParsingException, - preserveIdentifier + identifierPreservationStrategy ) ); } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNodeDecorator.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNodeDecorator.java index 2b34b3071..8c978c506 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNodeDecorator.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNodeDecorator.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.el.ext.eager; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.Node; import de.odysseus.el.tree.impl.ast.AstNode; @@ -14,6 +15,7 @@ * be an EvalResultHolder or wrapped with this decorator. */ public class EagerAstNodeDecorator extends AstNode implements EvalResultHolder { + private final AstNode astNode; protected Object evalResult; protected boolean hasEvalResult; @@ -64,7 +66,7 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { return astNode.toString(); } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstParameters.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstParameters.java index 920bafff0..6cb9fc4ee 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstParameters.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstParameters.java @@ -1,9 +1,10 @@ package com.hubspot.jinjava.el.ext.eager; +import com.hubspot.jinjava.el.HasInterpreter; import com.hubspot.jinjava.el.ext.DeferredParsingException; -import com.hubspot.jinjava.el.ext.ExtendedParser; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; -import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.DeferredValueException; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstNode; import de.odysseus.el.tree.impl.ast.AstParameters; @@ -11,8 +12,10 @@ import java.util.StringJoiner; import java.util.stream.Collectors; import javax.el.ELContext; +import javax.el.ELException; public class EagerAstParameters extends AstParameters implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; protected final List nodes; @@ -36,18 +39,29 @@ private EagerAstParameters(List nodes, boolean convertedToEvalResultHol @Override public Object[] eval(Bindings bindings, ELContext context) { try ( - TemporaryValueClosable c = ( - (JinjavaInterpreter) context - .getELResolver() - .getValue(context, null, ExtendedParser.INTERPRETER) - ).getContext() - .withPartialMacroEvaluation(false) + TemporaryValueClosable c = + ((HasInterpreter) context).interpreter() + .getContext() + .withPartialMacroEvaluation(false) ) { - return (Object[]) EvalResultHolder.super.eval( - () -> super.eval(bindings, context), - bindings, - context - ); + try { + setEvalResult(super.eval(bindings, context)); + return (Object[]) checkEvalResultSize(context); + } catch (DeferredValueException | ELException originalException) { + DeferredParsingException e = EvalResultHolder.convertToDeferredParsingException( + originalException + ); + throw new DeferredParsingException( + this, + getPartiallyResolved( + bindings, + context, + e, + IdentifierPreservationStrategy.PRESERVING + ), // Need this to always be true because a function may modify the identifier + IdentifierPreservationStrategy.PRESERVING + ); + } } } @@ -56,23 +70,22 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { StringJoiner joiner = new StringJoiner(", "); nodes .stream() .map(node -> (EvalResultHolder) node) - .forEach( - node -> - joiner.add( - EvalResultHolder.reconstructNode( - bindings, - context, - node, - deferredParsingException, - false - ) + .forEach(node -> + joiner.add( + EvalResultHolder.reconstructNode( + bindings, + context, + node, + deferredParsingException, + identifierPreservationStrategy ) + ) ); return joiner.toString(); } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRangeBracket.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRangeBracket.java index 0f4f8d4f6..81771d3a4 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRangeBracket.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRangeBracket.java @@ -2,11 +2,13 @@ import com.hubspot.jinjava.el.ext.AstRangeBracket; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstNode; import javax.el.ELContext; public class EagerAstRangeBracket extends AstRangeBracket implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; @@ -42,7 +44,7 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { return ( EvalResultHolder.reconstructNode( @@ -50,7 +52,7 @@ public String getPartiallyResolved( context, (EvalResultHolder) prefix, deferredParsingException, - preserveIdentifier + identifierPreservationStrategy ) + "[" + EvalResultHolder.reconstructNode( @@ -58,7 +60,7 @@ public String getPartiallyResolved( context, (EvalResultHolder) property, deferredParsingException, - false + IdentifierPreservationStrategy.RESOLVING ) + ":" + EvalResultHolder.reconstructNode( @@ -66,7 +68,7 @@ public String getPartiallyResolved( context, (EvalResultHolder) rangeMax, deferredParsingException, - false + IdentifierPreservationStrategy.RESOLVING ) + "]" ); diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRoot.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRoot.java index d785cd63d..1b8f1314a 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRoot.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRoot.java @@ -8,6 +8,7 @@ import javax.el.ValueReference; public class EagerAstRoot extends AstNode { + private AstNode rootNode; public EagerAstRoot(AstNode rootNode) { diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstTuple.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstTuple.java index e0c4b86b4..46d6aa4d2 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstTuple.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstTuple.java @@ -2,12 +2,14 @@ import com.hubspot.jinjava.el.ext.AstTuple; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstParameters; import java.util.StringJoiner; import javax.el.ELContext; public class EagerAstTuple extends AstTuple implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; @@ -29,7 +31,7 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { StringJoiner joiner = new StringJoiner(", "); for (int i = 0; i < elements.getCardinality(); i++) { @@ -39,7 +41,7 @@ public String getPartiallyResolved( context, (EvalResultHolder) elements.getChild(i), deferredParsingException, - preserveIdentifier + identifierPreservationStrategy ) ); } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstUnary.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstUnary.java index ffdbdfc36..da38314b9 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstUnary.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstUnary.java @@ -1,12 +1,14 @@ package com.hubspot.jinjava.el.ext.eager; import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstNode; import de.odysseus.el.tree.impl.ast.AstUnary; import javax.el.ELContext; public class EagerAstUnary extends AstUnary implements EvalResultHolder { + protected Object evalResult; protected boolean hasEvalResult; protected final EvalResultHolder child; @@ -36,7 +38,7 @@ public String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ) { return ( operator.toString() + @@ -45,7 +47,7 @@ public String getPartiallyResolved( context, child, deferredParsingException, - false + IdentifierPreservationStrategy.RESOLVING ) ); } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerExtendedParser.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerExtendedParser.java index 5ca383afd..29f53e2b7 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerExtendedParser.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerExtendedParser.java @@ -198,4 +198,9 @@ protected AstList createAstList(AstParameters parameters) protected AstParameters createAstParameters(List nodes) { return new EagerAstParameters(nodes); } + + @Override + protected boolean shouldUseFilterChainOptimization() { + return false; + } } 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..f58d05d38 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EvalResultHolder.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EvalResultHolder.java @@ -1,9 +1,12 @@ package com.hubspot.jinjava.el.ext.eager; +import com.hubspot.jinjava.el.HasInterpreter; import com.hubspot.jinjava.el.ext.DeferredParsingException; import com.hubspot.jinjava.el.ext.ExtendedParser; +import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy; import com.hubspot.jinjava.interpret.DeferredValueException; -import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.MetaContextVariables; +import com.hubspot.jinjava.interpret.PartiallyDeferredValue; import com.hubspot.jinjava.util.EagerExpressionResolver; import de.odysseus.el.tree.Bindings; import de.odysseus.el.tree.impl.ast.AstIdentifier; @@ -34,7 +37,12 @@ default Object eval( ); throw new DeferredParsingException( this, - getPartiallyResolved(bindings, context, e, false) + getPartiallyResolved( + bindings, + context, + e, + IdentifierPreservationStrategy.RESOLVING + ) ); } } @@ -44,12 +52,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() + ((HasInterpreter) context).interpreter().getContext().isDeferLargeObjects() ) { throw new DeferredValueException("Collection too big"); } @@ -60,7 +63,7 @@ String getPartiallyResolved( Bindings bindings, ELContext context, DeferredParsingException deferredParsingException, - boolean preserveIdentifier + IdentifierPreservationStrategy identifierPreservationStrategy ); static String reconstructNode( @@ -68,19 +71,25 @@ static String reconstructNode( ELContext context, EvalResultHolder astNode, DeferredParsingException exception, - boolean preserveIdentifier + IdentifierPreservationStrategy preserveIdentifier ) { if (astNode == null) { return ""; } - preserveIdentifier |= + if ( astNode instanceof AstIdentifier && - ExtendedParser.INTERPRETER.equals(((AstIdentifier) astNode).getName()); + ExtendedParser.INTERPRETER.equals(((AstIdentifier) astNode).getName()) + ) { + return ExtendedParser.INTERPRETER; + } + preserveIdentifier = + IdentifierPreservationStrategy.preserving(preserveIdentifier.isPreserving()); if ( - preserveIdentifier && + preserveIdentifier.isPreserving() && !astNode.hasEvalResult() && - !(exception != null && exception.getSourceNode() == astNode) + !(exceptionMatchesNode(exception, astNode)) ) { + // Evaluate to determine if the result is primitive. If so, we don't need to preserve the identifier try { EagerExpressionResolver.getValueAsJinjavaStringSafe( ((AstNode) astNode).eval(bindings, context) @@ -89,10 +98,18 @@ static String reconstructNode( } Object evalResult = astNode.getEvalResult(); if ( - !preserveIdentifier || - (astNode.hasEvalResult() && EagerExpressionResolver.isPrimitive(evalResult)) + exceptionMatchesNode(exception, astNode) && + exception.getIdentifierPreservationStrategy().isPreserving() + ) { + return exception.getDeferredEvalResult(); + } + if ( + !preserveIdentifier.isPreserving() || + (astNode.hasEvalResult() && + (EagerExpressionResolver.isPrimitive(evalResult) || + evalResult instanceof PartiallyDeferredValue)) ) { - if (exception != null && exception.getSourceNode() == astNode) { + if (exceptionMatchesNode(exception, astNode)) { return exception.getDeferredEvalResult(); } if (!astNode.hasEvalResult()) { @@ -104,9 +121,27 @@ static String reconstructNode( } try { return EagerExpressionResolver.getValueAsJinjavaStringSafe(evalResult); - } catch (DeferredValueException ignored) {} + } catch (DeferredValueException e) { + if (astNode instanceof AstIdentifier) { + String name = ((AstIdentifier) astNode).getName(); + if ( + MetaContextVariables.isMetaContextVariable( + name, + ((HasInterpreter) context).interpreter().getContext() + ) + ) { + return name; + } + throw e; + } + } } - return astNode.getPartiallyResolved(bindings, context, exception, true); + return astNode.getPartiallyResolved( + bindings, + context, + exception, + IdentifierPreservationStrategy.PRESERVING + ); } static DeferredParsingException convertToDeferredParsingException( @@ -127,4 +162,14 @@ static DeferredParsingException convertToDeferredParsingException( } return null; } + + static boolean exceptionMatchesNode( + DeferredParsingException deferredParsingException, + Object astNode + ) { + return ( + deferredParsingException != null && + deferredParsingException.getSourceNode() == astNode + ); + } } 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..32fe8bd43 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/MacroFunctionTempVariable.java @@ -0,0 +1,45 @@ +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_%d_temp_variable_%d__"; + private final String deferredResult; + + public MacroFunctionTempVariable(String deferredResult) { + this.deferredResult = deferredResult; + } + + public static String getVarName(String macroFunctionName, int hashCode, int callCount) { + return String.format( + CONTEXT_KEY_PREFIX, + macroFunctionName, + Math.abs(hashCode), + 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/el/ext/eager/RenderFlatTempVariable.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/RenderFlatTempVariable.java new file mode 100644 index 000000000..682d3d5ca --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/RenderFlatTempVariable.java @@ -0,0 +1,40 @@ +package com.hubspot.jinjava.el.ext.eager; + +import com.hubspot.jinjava.objects.serialization.PyishBlockSetSerializable; +import java.util.Objects; + +public class RenderFlatTempVariable implements PyishBlockSetSerializable { + + private static final String CONTEXT_KEY_PREFIX = "__render_%d_temp_variable__"; + private final String deferredResult; + + public RenderFlatTempVariable(String deferredResult) { + this.deferredResult = deferredResult; + } + + public static String getVarName(String result) { + return String.format(CONTEXT_KEY_PREFIX, Math.abs(result.hashCode() >> 1)); + } + + @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; + } + RenderFlatTempVariable that = (RenderFlatTempVariable) o; + return deferredResult.equals(that.deferredResult); + } + + @Override + public int hashCode() { + return Objects.hash(deferredResult); + } +} diff --git a/src/main/java/com/hubspot/jinjava/features/BuiltInFeatures.java b/src/main/java/com/hubspot/jinjava/features/BuiltInFeatures.java new file mode 100644 index 000000000..87b34a6b8 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/features/BuiltInFeatures.java @@ -0,0 +1,12 @@ +package com.hubspot.jinjava.features; + +public interface BuiltInFeatures { + String WHITESPACE_REQUIRED_WITHIN_TOKENS = "whitespace_required_within_tokens"; + String FIXED_DATE_TIME_FILTER_NULL_ARG = "FIXED_DATE_TIME_FILTER_NULL_ARG"; + String ECHO_UNDEFINED = "echoUndefined"; + String PREVENT_ACCIDENTAL_EXPRESSIONS = "PREVENT_ACCIDENTAL_EXPRESSIONS"; + String IGNORE_NESTED_INTERPRETATION_PARSE_ERRORS = + "IGNORE_NESTED_INTERPRETATION_PARSE_ERRORS"; + String OUTPUT_UNDEFINED_VARIABLES_ERROR = "OUTPUT_UNDEFINED_VARIABLES_ERROR"; + String INTEGER_SET_TO_LONG_CONVERSION = "INTEGER_SET_TO_LONG_CONVERSION"; +} diff --git a/src/main/java/com/hubspot/jinjava/features/DateTimeFeatureActivationStrategy.java b/src/main/java/com/hubspot/jinjava/features/DateTimeFeatureActivationStrategy.java new file mode 100644 index 000000000..c50e4f679 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/features/DateTimeFeatureActivationStrategy.java @@ -0,0 +1,31 @@ +package com.hubspot.jinjava.features; + +import com.hubspot.jinjava.interpret.Context; +import java.time.ZonedDateTime; + +public class DateTimeFeatureActivationStrategy implements FeatureActivationStrategy { + + private final ZonedDateTime activateAt; + + public static DateTimeFeatureActivationStrategy of(ZonedDateTime activateAt) { + return new DateTimeFeatureActivationStrategy(activateAt); + } + + private DateTimeFeatureActivationStrategy(ZonedDateTime activateAt) { + this.activateAt = activateAt; + } + + @Override + public boolean isActive(Context context) { + return ZonedDateTime.now().isAfter(activateAt); + } + + @Override + public boolean isActive() { + return false; // Not usable without context + } + + public ZonedDateTime getActivateAt() { + return activateAt; + } +} diff --git a/src/main/java/com/hubspot/jinjava/features/FeatureActivationStrategy.java b/src/main/java/com/hubspot/jinjava/features/FeatureActivationStrategy.java new file mode 100644 index 000000000..6ba27b206 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/features/FeatureActivationStrategy.java @@ -0,0 +1,11 @@ +package com.hubspot.jinjava.features; + +import com.hubspot.jinjava.interpret.Context; + +public interface FeatureActivationStrategy { + default boolean isActive(Context context) { + return isActive(); + } + + boolean isActive(); +} diff --git a/src/main/java/com/hubspot/jinjava/features/FeatureConfig.java b/src/main/java/com/hubspot/jinjava/features/FeatureConfig.java new file mode 100644 index 000000000..7038592ce --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/features/FeatureConfig.java @@ -0,0 +1,57 @@ +package com.hubspot.jinjava.features; + +import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.interpret.Context; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public class FeatureConfig { + + Map features; + + private FeatureConfig(Map features) { + this.features = ImmutableMap.copyOf(features); + } + + public FeatureActivationStrategy getFeature(String name) { + return features.getOrDefault(name, FeatureStrategies.INACTIVE); + } + + public static FeatureConfig.Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + + private final Map features = new HashMap<>(); + + @Deprecated + public Builder add(String name, Function strategyFunction) { + features.put( + name, + new FeatureActivationStrategy() { + @Override + public boolean isActive(Context context) { + return strategyFunction.apply(context); + } + + @Override + public boolean isActive() { + return false; + } + } + ); + return this; + } + + public Builder add(String name, FeatureActivationStrategy strategy) { + features.put(name, strategy); + return this; + } + + public FeatureConfig build() { + return new FeatureConfig(features); + } + } +} diff --git a/src/main/java/com/hubspot/jinjava/features/FeatureStrategies.java b/src/main/java/com/hubspot/jinjava/features/FeatureStrategies.java new file mode 100644 index 000000000..8ae4025f4 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/features/FeatureStrategies.java @@ -0,0 +1,7 @@ +package com.hubspot.jinjava.features; + +public class FeatureStrategies { + + public static final FeatureActivationStrategy INACTIVE = () -> false; + public static final FeatureActivationStrategy ACTIVE = () -> true; +} diff --git a/src/main/java/com/hubspot/jinjava/features/Features.java b/src/main/java/com/hubspot/jinjava/features/Features.java new file mode 100644 index 000000000..5e311f322 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/features/Features.java @@ -0,0 +1,24 @@ +package com.hubspot.jinjava.features; + +import com.hubspot.jinjava.interpret.Context; + +public class Features { + + private final FeatureConfig featureConfig; + + public Features(FeatureConfig featureConfig) { + this.featureConfig = featureConfig; + } + + public boolean isActive(String featureName, Context context) { + return getActivationStrategy(featureName).isActive(context); + } + + public boolean isActive(String featureName) { + return getActivationStrategy(featureName).isActive(); + } + + public FeatureActivationStrategy getActivationStrategy(String featureName) { + return featureConfig.getFeature(featureName); + } +} diff --git a/src/main/java/com/hubspot/jinjava/interpret/AutoCloseableSupplier.java b/src/main/java/com/hubspot/jinjava/interpret/AutoCloseableSupplier.java new file mode 100644 index 000000000..12a7b8dfe --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/interpret/AutoCloseableSupplier.java @@ -0,0 +1,68 @@ +package com.hubspot.jinjava.interpret; + +import com.google.common.base.Suppliers; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public class AutoCloseableSupplier implements Supplier> { + + public static AutoCloseableSupplier of(T tSupplier) { + return of(() -> tSupplier, ignored -> {}); + } + + public static AutoCloseableSupplier of( + Supplier tSupplier, + Consumer closeConsumer + ) { + return new AutoCloseableSupplier<>( + Suppliers.memoize(() -> new AutoCloseableImpl<>(tSupplier.get(), closeConsumer)) + ); + } + + private final Supplier> autoCloseableImplWrapper; + + private AutoCloseableSupplier(Supplier> autoCloseableImplWrapper) { + this.autoCloseableImplWrapper = autoCloseableImplWrapper; + } + + @Override + public AutoCloseableImpl get() { + return autoCloseableImplWrapper.get(); + } + + public T dangerouslyGetWithoutClosing() { + return autoCloseableImplWrapper.get().value(); + } + + public AutoCloseableSupplier map(Function mapper) { + return new AutoCloseableSupplier<>(() -> { + T t = autoCloseableImplWrapper.get().value(); + return new AutoCloseableImpl<>( + mapper.apply(t), + r -> autoCloseableImplWrapper.get().closeConsumer.accept(t) + ); + }); + } + + public static class AutoCloseableImpl implements java.lang.AutoCloseable { + + private final T t; + private final Consumer closeConsumer; + + protected AutoCloseableImpl(T t, Consumer closeConsumer) { + this.t = t; + this.closeConsumer = closeConsumer; + } + + public T value() { + return t; + } + + @Override + public void close() { + closeConsumer.accept(t); + } + } +} diff --git a/src/main/java/com/hubspot/jinjava/interpret/CallStack.java b/src/main/java/com/hubspot/jinjava/interpret/CallStack.java index 1b876a4f9..f67d96d6b 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/CallStack.java +++ b/src/main/java/com/hubspot/jinjava/interpret/CallStack.java @@ -1,9 +1,11 @@ package com.hubspot.jinjava.interpret; +import com.hubspot.algebra.Result; import java.util.Optional; import java.util.Stack; public class CallStack { + private final CallStack parent; private final Class exceptionClass; private final Stack stack = new Stack<>(); @@ -110,6 +112,57 @@ public boolean isEmpty() { return stack.empty() && (parent == null || parent.isEmpty()); } + public AutoCloseableSupplier> closeablePush( + String path, + int lineNumber, + int startPosition + ) { + return AutoCloseableSupplier.of( + () -> { + try { + push(path, lineNumber, startPosition); + return Result.ok(path); + } catch (TagCycleException e) { + return Result.err(e); + } + }, + result -> result.ifOk(ok -> pop()) + ); + } + + public AutoCloseableSupplier closeablePushWithoutCycleCheck( + String path, + int lineNumber, + int startPosition + ) { + return AutoCloseableSupplier.of( + () -> { + pushWithoutCycleCheck(path, lineNumber, startPosition); + return path; + }, + ignored -> pop() + ); + } + + public AutoCloseableSupplier> closeablePushWithMaxDepth( + String path, + int maxDepth, + int lineNumber, + int startPosition + ) { + return AutoCloseableSupplier.of( + () -> { + try { + pushWithMaxDepth(path, maxDepth, lineNumber, startPosition); + return Result.ok(path); + } catch (TagCycleException e) { + return Result.err(e); + } + }, + result -> result.ifOk(ok -> pop()) + ); + } + private void pushToStack(String path, int lineNumber, int startPosition) { if (isEmpty()) { topLineNumber = lineNumber; 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..64a7e2d83 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/interpret/CannotReconstructValueException.java @@ -0,0 +1,13 @@ +package com.hubspot.jinjava.interpret; + +import com.google.common.annotations.Beta; + +@Beta +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/interpret/CollectionTooBigException.java b/src/main/java/com/hubspot/jinjava/interpret/CollectionTooBigException.java index 465be10aa..bae6f52a8 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/CollectionTooBigException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/CollectionTooBigException.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; public class CollectionTooBigException extends RuntimeException { + private final int maxSize; private final int size; diff --git a/src/main/java/com/hubspot/jinjava/interpret/Context.java b/src/main/java/com/hubspot/jinjava/interpret/Context.java index 8e4d2d30d..a5fbf8544 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/Context.java +++ b/src/main/java/com/hubspot/jinjava/interpret/Context.java @@ -16,11 +16,14 @@ package com.hubspot.jinjava.interpret; +import com.google.common.annotations.Beta; import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.SetMultimap; +import com.google.common.collect.Sets; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl; import com.hubspot.jinjava.lib.Importable; -import com.hubspot.jinjava.lib.expression.DefaultExpressionStrategy; import com.hubspot.jinjava.lib.expression.ExpressionStrategy; import com.hubspot.jinjava.lib.exptest.ExpTest; import com.hubspot.jinjava.lib.exptest.ExpTestLibrary; @@ -29,15 +32,16 @@ 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; +import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.tree.Node; import com.hubspot.jinjava.util.DeferredValueUtils; import com.hubspot.jinjava.util.ScopeMap; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -49,6 +53,7 @@ import java.util.stream.Collectors; public class Context extends ScopeMap { + public static final String GLOBAL_MACROS_SCOPE_KEY = "__macros__"; public static final String IMPORT_RESOURCE_PATH_KEY = "import_resource_path"; public static final String DEFERRED_IMPORT_RESOURCE_PATH_KEY = @@ -60,11 +65,11 @@ public class Context extends ScopeMap { private Map> disabled; public boolean isValidationMode() { - return validationMode; + return contextConfiguration.isValidationMode(); } public Context setValidationMode(boolean validationMode) { - this.validationMode = validationMode; + contextConfiguration = contextConfiguration.withValidationMode(validationMode); return this; } @@ -72,7 +77,7 @@ public enum Library { EXP_TEST, FILTER, FUNCTION, - TAG + TAG, } private final CallStack extendPathStack; @@ -87,6 +92,8 @@ public enum Library { private final Set resolvedFunctions = new HashSet<>(); private Set deferredNodes = new HashSet<>(); + + @Beta private Set deferredTokens = new HashSet<>(); private final ExpTestLibrary expTestLibrary; @@ -94,8 +101,6 @@ public enum Library { private final FunctionLibrary functionLibrary; private final TagLibrary tagLibrary; - private ExpressionStrategy expressionStrategy = new DefaultExpressionStrategy(); - private final Context parent; private int renderDepth = -1; @@ -103,15 +108,9 @@ public enum Library { private List superBlock; private final Stack renderStack = new Stack<>(); - - private boolean validationMode = false; - private boolean deferredExecutionMode = false; - private boolean deferLargeObjects = false; - private boolean throwInterpreterErrors = false; - private boolean partialMacroEvaluation = false; - private boolean unwrapRawOverride = false; - private DynamicVariableResolver dynamicVariableResolver = null; + private ContextConfiguration contextConfiguration = ContextConfiguration.of(); private final Set metaContextVariables; // These variable names aren't tracked in eager execution + private final Set overriddenNonMetaContextVariables; private Node currentNode; public Context() { @@ -193,7 +192,7 @@ public Context( : parent == null ? null : parent.getCurrentPathStack(); if (disabled == null) { - disabled = new HashMap<>(); + disabled = ImmutableMap.of(); } this.expTestLibrary = @@ -204,14 +203,10 @@ public Context( new FunctionLibrary(parent == null, disabled.get(Library.FUNCTION)); this.metaContextVariables = parent == null ? new HashSet<>() : parent.metaContextVariables; + this.overriddenNonMetaContextVariables = + parent == null ? new HashSet<>() : parent.overriddenNonMetaContextVariables; if (parent != null) { - this.expressionStrategy = parent.expressionStrategy; - this.partialMacroEvaluation = parent.partialMacroEvaluation; - this.unwrapRawOverride = parent.unwrapRawOverride; - this.dynamicVariableResolver = parent.dynamicVariableResolver; - this.deferredExecutionMode = parent.deferredExecutionMode; - this.deferLargeObjects = parent.deferLargeObjects; - this.throwInterpreterErrors = parent.throwInterpreterErrors; + this.contextConfiguration = parent.contextConfiguration; } } @@ -343,18 +338,79 @@ public void addResolvedFunction(String function) { } } + /** + * @deprecated Use {@link MetaContextVariables#isMetaContextVariable(String, Context)} + */ + @Deprecated + @Beta public Set getMetaContextVariables() { return metaContextVariables; } + @Beta + Set getComputedMetaContextVariables() { + return Sets.difference(metaContextVariables, overriddenNonMetaContextVariables); + } + + @Beta + public void addMetaContextVariables(Collection variables) { + metaContextVariables.addAll(variables); + } + + Set getNonMetaContextVariables() { + return overriddenNonMetaContextVariables; + } + + @Beta + public void addNonMetaContextVariables(Collection variables) { + overriddenNonMetaContextVariables.addAll( + variables + .stream() + .filter(var -> !EagerExecutionMode.STATIC_META_CONTEXT_VARIABLES.contains(var)) + .collect(Collectors.toList()) + ); + } + + @Beta + public void removeNonMetaContextVariables(Collection variables) { + overriddenNonMetaContextVariables.removeAll(variables); + } + public void handleDeferredNode(Node node) { + if ( + JinjavaInterpreter + .getCurrentMaybe() + .map(interpreter -> interpreter.getConfig().getExecutionMode().useEagerParser()) + .orElse(false) + ) { + addDeferredNodeRecursively(node); + } else { + handleDeferredNodeAndDeferVariables(node); + } + } + + private void addDeferredNodeRecursively(Node node) { deferredNodes.add(node); - Set deferredProps = DeferredValueUtils.findAndMarkDeferredProperties(this); if (getParent() != null) { Context parent = getParent(); - //Ignore global context + // Ignore global context + if (parent.getParent() != null) { + getParent().handleDeferredNode(node); + } + } + } + + private void handleDeferredNodeAndDeferVariables(Node node) { + deferredNodes.add(node); + Set deferredProps = DeferredValueUtils.findAndMarkDeferredProperties( + this, + node + ); + if (getParent() != null) { + Context parent = getParent(); + // Ignore global context if (parent.getParent() != null) { - //Place deferred values on the parent context + // Place deferred values on the parent context deferredProps .stream() .filter(key -> !parent.containsKey(key)) @@ -368,6 +424,7 @@ public Set getDeferredNodes() { return ImmutableSet.copyOf(deferredNodes); } + @Beta public void checkNumberOfDeferredTokens() { Context secondToLastContext = this; if (parent != null) { @@ -375,39 +432,25 @@ public void checkNumberOfDeferredTokens() { secondToLastContext = secondToLastContext.parent; } } - int maxNumDeferredTokens = JinjavaInterpreter + int currentNumDeferredTokens = secondToLastContext.deferredTokens.size(); + JinjavaInterpreter .getCurrentMaybe() .map(i -> i.getConfig().getMaxNumDeferredTokens()) - .orElse(1000); - if (secondToLastContext.deferredTokens.size() >= maxNumDeferredTokens) { - throw new DeferredValueException( - "Too many Deferred Tokens, max is " + maxNumDeferredTokens - ); - } + .filter(maxNumDeferredTokens -> currentNumDeferredTokens >= maxNumDeferredTokens) + .ifPresent(maxNumDeferredTokens -> { + throw new DeferredValueException( + "Too many Deferred Tokens, max is " + maxNumDeferredTokens + ); + }); } + @Beta public void handleDeferredToken(DeferredToken deferredToken) { - deferredTokens.add(deferredToken); - - if ( - deferredToken.getImportResourcePath() == null || - deferredToken.getImportResourcePath().equals(get(Context.IMPORT_RESOURCE_PATH_KEY)) - ) { - DeferredValueUtils.findAndMarkDeferredProperties(this, deferredToken); - } - if (getParent() != null) { - Context parent = getParent(); - //Ignore global context - if (parent.getParent() != null) { - parent.handleDeferredToken(deferredToken); - } else { - checkNumberOfDeferredTokens(); - } - } + deferredToken.addTo(this); } + @Beta public void removeDeferredTokens(Collection toRemove) { - deferredTokens.removeAll(toRemove); if (getParent() != null) { Context parent = getParent(); //Ignore global context @@ -415,8 +458,10 @@ public void removeDeferredTokens(Collection toRemove) { parent.removeDeferredTokens(toRemove); } } + deferredTokens.removeAll(toRemove); } + @Beta public Set getDeferredTokens() { return deferredTokens; } @@ -538,10 +583,11 @@ public void registerFilter(Filter f) { } public boolean isFunctionDisabled(String name) { - return ( - disabled != null && - disabled.getOrDefault(Library.FUNCTION, Collections.emptySet()).contains(name) - ); + if (disabled == null) { + return false; + } + Set disabledFunctions = disabled.get(Library.FUNCTION); + return disabledFunctions != null && disabledFunctions.contains(name); } public ELFunctionDefinition getFunction(String name) { @@ -561,10 +607,13 @@ public Collection getAllFunctions() { if (parent != null) { fns.addAll(parent.getAllFunctions()); } - - final Set disabledFunctions = disabled == null - ? new HashSet<>() - : disabled.getOrDefault(Library.FUNCTION, new HashSet<>()); + if (disabled == null) { + return fns; + } + Set disabledFunctions = disabled.get(Library.FUNCTION); + if (disabledFunctions == null) { + return fns; + } return fns .stream() .filter(f -> !disabledFunctions.contains(f.getName())) @@ -601,21 +650,23 @@ public void registerTag(Tag t) { } public DynamicVariableResolver getDynamicVariableResolver() { - return dynamicVariableResolver; + return contextConfiguration.getDynamicVariableResolver(); } public void setDynamicVariableResolver( final DynamicVariableResolver dynamicVariableResolver ) { - this.dynamicVariableResolver = dynamicVariableResolver; + contextConfiguration = + contextConfiguration.withDynamicVariableResolver(dynamicVariableResolver); } public ExpressionStrategy getExpressionStrategy() { - return expressionStrategy; + return contextConfiguration.getExpressionStrategy(); } public void setExpressionStrategy(ExpressionStrategy expressionStrategy) { - this.expressionStrategy = expressionStrategy; + contextConfiguration = + contextConfiguration.withExpressionStrategy(expressionStrategy); } public Optional getImportResourceAlias() { @@ -630,6 +681,10 @@ public CallStack getImportPathStack() { return importPathStack; } + public CallStack getFromPathStack() { + return fromStack; + } + public CallStack getIncludePathStack() { return includePathStack; } @@ -646,10 +701,12 @@ public CallStack getCurrentPathStack() { return currentPathStack; } + @Deprecated public void pushFromStack(String path, int lineNumber, int startPosition) { fromStack.push(path, lineNumber, startPosition); } + @Deprecated public void popFromStack() { fromStack.pop(); } @@ -670,10 +727,17 @@ public void setRenderDepth(int renderDepth) { this.renderDepth = renderDepth; } + public AutoCloseableSupplier closeablePushRenderStack(String template) { + renderStack.push(template); + return AutoCloseableSupplier.of(() -> template, t -> renderStack.pop()); + } + + @Deprecated public void pushRenderStack(String template) { renderStack.push(template); } + @Deprecated public String popRenderStack() { return renderStack.pop(); } @@ -701,20 +765,21 @@ public SetMultimap getDependencies() { } public boolean isDeferredExecutionMode() { - return deferredExecutionMode; + return contextConfiguration.isDeferredExecutionMode(); } public Context setDeferredExecutionMode(boolean deferredExecutionMode) { - this.deferredExecutionMode = deferredExecutionMode; + contextConfiguration = + contextConfiguration.withDeferredExecutionMode(deferredExecutionMode); return this; } public boolean isDeferLargeObjects() { - return deferLargeObjects; + return contextConfiguration.isDeferLargeObjects(); } public Context setDeferLargeObjects(boolean deferLargeObjects) { - this.deferLargeObjects = deferLargeObjects; + contextConfiguration = contextConfiguration.withDeferLargeObjects(deferLargeObjects); return this; } @@ -722,27 +787,80 @@ public TemporaryValueClosable withDeferLargeObjects( boolean deferLargeObjects ) { TemporaryValueClosable temporaryValueClosable = new TemporaryValueClosable<>( - this.deferLargeObjects, + isDeferLargeObjects(), this::setDeferLargeObjects ); - this.deferLargeObjects = deferLargeObjects; + setDeferLargeObjects(deferLargeObjects); return temporaryValueClosable; } + @Deprecated public boolean getThrowInterpreterErrors() { - return throwInterpreterErrors; + ErrorHandlingStrategy errorHandlingStrategy = getErrorHandlingStrategy(); + return ( + errorHandlingStrategy.getFatalErrorStrategy() == + ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy.THROW_EXCEPTION + ); } + @Deprecated public void setThrowInterpreterErrors(boolean throwInterpreterErrors) { - this.throwInterpreterErrors = throwInterpreterErrors; + contextConfiguration = + contextConfiguration.withErrorHandlingStrategy( + ErrorHandlingStrategy + .builder() + .setFatalErrorStrategy( + throwInterpreterErrors + ? ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy.THROW_EXCEPTION + : ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy.ADD_ERROR + ) + .setNonFatalErrorStrategy( + throwInterpreterErrors + ? ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy.IGNORE // Deprecated, warnings are ignored when doing eager expression resolving + : ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy.ADD_ERROR + ) + .build() + ); + } + + @Deprecated + public TemporaryValueClosable withThrowInterpreterErrors() { + TemporaryValueClosable temporaryValueClosable = new TemporaryValueClosable<>( + getThrowInterpreterErrors(), + this::setThrowInterpreterErrors + ); + setThrowInterpreterErrors(true); + return temporaryValueClosable; + } + + public ErrorHandlingStrategy getErrorHandlingStrategy() { + return contextConfiguration.getErrorHandlingStrategy(); + } + + public void setErrorHandlingStrategy(ErrorHandlingStrategy errorHandlingStrategy) { + contextConfiguration = + contextConfiguration.withErrorHandlingStrategy(errorHandlingStrategy); + } + + public TemporaryValueClosable withErrorHandlingStrategy( + ErrorHandlingStrategy errorHandlingStrategy + ) { + TemporaryValueClosable temporaryValueClosable = + new TemporaryValueClosable<>( + getErrorHandlingStrategy(), + this::setErrorHandlingStrategy + ); + setErrorHandlingStrategy(errorHandlingStrategy); + return temporaryValueClosable; } public boolean isPartialMacroEvaluation() { - return partialMacroEvaluation; + return contextConfiguration.isPartialMacroEvaluation(); } public void setPartialMacroEvaluation(boolean partialMacroEvaluation) { - this.partialMacroEvaluation = partialMacroEvaluation; + contextConfiguration = + contextConfiguration.withPartialMacroEvaluation(partialMacroEvaluation); } public TemporaryValueClosable withPartialMacroEvaluation() { @@ -753,42 +871,54 @@ public TemporaryValueClosable withPartialMacroEvaluation( boolean partialMacroEvaluation ) { TemporaryValueClosable temporaryValueClosable = new TemporaryValueClosable<>( - this.partialMacroEvaluation, + isPartialMacroEvaluation(), this::setPartialMacroEvaluation ); - this.partialMacroEvaluation = partialMacroEvaluation; + setPartialMacroEvaluation(partialMacroEvaluation); return temporaryValueClosable; } public boolean isUnwrapRawOverride() { - return unwrapRawOverride; + return contextConfiguration.isUnwrapRawOverride(); } public void setUnwrapRawOverride(boolean unwrapRawOverride) { - this.unwrapRawOverride = unwrapRawOverride; + contextConfiguration = contextConfiguration.withUnwrapRawOverride(unwrapRawOverride); } public TemporaryValueClosable withUnwrapRawOverride() { + return withUnwrapRawOverride(true); + } + + public TemporaryValueClosable withUnwrapRawOverride(boolean value) { TemporaryValueClosable temporaryValueClosable = new TemporaryValueClosable<>( - this.unwrapRawOverride, + isUnwrapRawOverride(), this::setUnwrapRawOverride ); - this.unwrapRawOverride = true; + setUnwrapRawOverride(value); return temporaryValueClosable; } - public static class TemporaryValueClosable implements AutoCloseable { - private final T previousValue; - private final Consumer resetValueConsumer; + public static class TemporaryValueClosable extends AutoCloseableImpl { private TemporaryValueClosable(T previousValue, Consumer resetValueConsumer) { - this.previousValue = previousValue; - this.resetValueConsumer = resetValueConsumer; + super(previousValue, resetValueConsumer); } - @Override - public void close() { - resetValueConsumer.accept(previousValue); + public static TemporaryValueClosable noOp() { + return new NoOpTemporaryValueClosable<>(); + } + + private static class NoOpTemporaryValueClosable extends TemporaryValueClosable { + + private NoOpTemporaryValueClosable() { + super(null, null); + } + + @Override + public void close() { + // No-op + } } } @@ -799,4 +929,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/interpret/ContextConfiguration.java b/src/main/java/com/hubspot/jinjava/interpret/ContextConfiguration.java new file mode 100644 index 000000000..b902efdef --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/interpret/ContextConfiguration.java @@ -0,0 +1,54 @@ +package com.hubspot.jinjava.interpret; + +import com.hubspot.jinjava.JinjavaImmutableStyle; +import com.hubspot.jinjava.lib.expression.DefaultExpressionStrategy; +import com.hubspot.jinjava.lib.expression.ExpressionStrategy; +import javax.annotation.Nullable; +import org.immutables.value.Value.Default; +import org.immutables.value.Value.Immutable; + +@Immutable(singleton = true) +@JinjavaImmutableStyle +public interface ContextConfiguration extends WithContextConfiguration { + @Default + default ExpressionStrategy getExpressionStrategy() { + return new DefaultExpressionStrategy(); + } + + @Nullable + DynamicVariableResolver getDynamicVariableResolver(); + + @Default + default boolean isValidationMode() { + return false; + } + + @Default + default boolean isDeferredExecutionMode() { + return false; + } + + @Default + default boolean isDeferLargeObjects() { + return false; + } + + @Default + default boolean isPartialMacroEvaluation() { + return false; + } + + @Default + default boolean isUnwrapRawOverride() { + return false; + } + + @Default + default ErrorHandlingStrategy getErrorHandlingStrategy() { + return ImmutableErrorHandlingStrategy.of(); + } + + static ContextConfiguration of() { + return ImmutableContextConfiguration.of(); + } +} diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReference.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReference.java index 5a1ce338c..43d0561bd 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReference.java +++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReference.java @@ -1,7 +1,17 @@ package com.hubspot.jinjava.interpret; -public class DeferredLazyReference implements DeferredValue { +import com.google.common.annotations.Beta; + +@Beta +public class DeferredLazyReference + implements DeferredValue, Cloneable, OneTimeReconstructible { + private final LazyReference lazyReference; + private boolean reconstructed; + + private DeferredLazyReference(LazyReference lazyReference) { + this.lazyReference = lazyReference; + } private DeferredLazyReference(Context referenceContext, String referenceKey) { lazyReference = LazyReference.of(referenceContext, referenceKey); @@ -15,7 +25,24 @@ public static DeferredLazyReference instance( } @Override - public Object getOriginalValue() { + public LazyReference getOriginalValue() { return lazyReference; } + + public boolean isReconstructed() { + return reconstructed; + } + + public void setReconstructed(boolean reconstructed) { + this.reconstructed = reconstructed; + } + + @Override + public DeferredLazyReference clone() { + try { + return (DeferredLazyReference) super.clone(); + } catch (CloneNotSupportedException e) { + return new DeferredLazyReference(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..f73c1f8ee --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReferenceSource.java @@ -0,0 +1,36 @@ +package com.hubspot.jinjava.interpret; + +import com.google.common.annotations.Beta; + +@Beta +public class DeferredLazyReferenceSource + extends DeferredValueImpl + implements OneTimeReconstructible { + + private static final DeferredLazyReferenceSource INSTANCE = + new DeferredLazyReferenceSource(); + + private boolean reconstructed; + + private DeferredLazyReferenceSource() {} + + private DeferredLazyReferenceSource(Object originalValue) { + super(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/DeferredMacroValueImpl.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredMacroValueImpl.java index 226757cc6..3aaaf20ec 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/DeferredMacroValueImpl.java +++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredMacroValueImpl.java @@ -1,6 +1,10 @@ package com.hubspot.jinjava.interpret; +import com.google.common.annotations.Beta; + +@Beta public class DeferredMacroValueImpl implements DeferredValue { + private static final DeferredValue INSTANCE = new DeferredMacroValueImpl(); private DeferredMacroValueImpl() {} diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredValue.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredValue.java index 19bc8995e..8cd6bfc58 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/DeferredValue.java +++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredValue.java @@ -15,4 +15,8 @@ static DeferredValue instance() { static DeferredValue instance(Object originalValue) { return DeferredValueImpl.instance(originalValue); } + + static DeferredValueShadow shadowInstance(Object originalValue) { + return DeferredValueShadow.instance(originalValue); + } } diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredValueException.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueException.java index a5a37da9f..884f59e7c 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/DeferredValueException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueException.java @@ -6,6 +6,7 @@ * and instead echo its contents to the output. */ public class DeferredValueException extends InterpretException { + public static final String MESSAGE_PREFIX = "Encountered a deferred value: "; public DeferredValueException(String message) { diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredValueImpl.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueImpl.java index a2b6e04b9..4b001ca94 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/DeferredValueImpl.java +++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueImpl.java @@ -3,25 +3,27 @@ import java.util.Objects; public class DeferredValueImpl implements DeferredValue { + private static final DeferredValue INSTANCE = new DeferredValueImpl(); private Object originalValue; - private DeferredValueImpl() {} + protected DeferredValueImpl() {} - private DeferredValueImpl(Object originalValue) { + protected DeferredValueImpl(Object originalValue) { this.originalValue = originalValue; } + @Override public Object getOriginalValue() { return originalValue; } - public static DeferredValue instance() { + protected static DeferredValue instance() { return INSTANCE; } - public static DeferredValue instance(Object originalValue) { + protected static DeferredValue instance(Object originalValue) { return new DeferredValueImpl(originalValue); } diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredValueShadow.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueShadow.java new file mode 100644 index 000000000..c1dddf72a --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueShadow.java @@ -0,0 +1,25 @@ +package com.hubspot.jinjava.interpret; + +import com.google.common.annotations.Beta; + +/** + * A deferred value which represents that a value was deferred within this context, + * but it is does not overwrite the actual key in which the original value resides on the context. + */ +@Beta +public class DeferredValueShadow extends DeferredValueImpl { + + protected DeferredValueShadow() {} + + protected DeferredValueShadow(Object originalValue) { + super(originalValue); + } + + protected static DeferredValueShadow instance() { + return new DeferredValueShadow(); + } + + protected static DeferredValueShadow instance(Object originalValue) { + return new DeferredValueShadow(originalValue); + } +} diff --git a/src/main/java/com/hubspot/jinjava/interpret/DisabledException.java b/src/main/java/com/hubspot/jinjava/interpret/DisabledException.java index 072110550..d1521a2b2 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/DisabledException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/DisabledException.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; public class DisabledException extends InterpretException { + private final String token; public DisabledException(String token) { diff --git a/src/main/java/com/hubspot/jinjava/interpret/DynamicVariableResolver.java b/src/main/java/com/hubspot/jinjava/interpret/DynamicVariableResolver.java index fe3b239ef..aaee1c324 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/DynamicVariableResolver.java +++ b/src/main/java/com/hubspot/jinjava/interpret/DynamicVariableResolver.java @@ -1,18 +1,18 @@ -/** - * 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.interpret; - -import java.util.function.Function; - -public interface DynamicVariableResolver extends Function {} +/** + * 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.interpret; + +import java.util.function.Function; + +public interface DynamicVariableResolver extends Function {} diff --git a/src/main/java/com/hubspot/jinjava/interpret/ErrorHandlingStrategy.java b/src/main/java/com/hubspot/jinjava/interpret/ErrorHandlingStrategy.java new file mode 100644 index 000000000..23df9c977 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/interpret/ErrorHandlingStrategy.java @@ -0,0 +1,46 @@ +package com.hubspot.jinjava.interpret; + +import com.hubspot.jinjava.JinjavaImmutableStyle; +import org.immutables.value.Value; + +@Value.Immutable(singleton = true) +@JinjavaImmutableStyle +public interface ErrorHandlingStrategy { + @Value.Default + default TemplateErrorTypeHandlingStrategy getFatalErrorStrategy() { + return TemplateErrorTypeHandlingStrategy.ADD_ERROR; + } + + @Value.Default + default TemplateErrorTypeHandlingStrategy getNonFatalErrorStrategy() { + return TemplateErrorTypeHandlingStrategy.ADD_ERROR; + } + + enum TemplateErrorTypeHandlingStrategy { + IGNORE, + ADD_ERROR, + THROW_EXCEPTION, + } + + class Builder extends ImmutableErrorHandlingStrategy.Builder {} + + static Builder builder() { + return new Builder(); + } + + static ErrorHandlingStrategy throwAll() { + return ErrorHandlingStrategy + .builder() + .setFatalErrorStrategy(TemplateErrorTypeHandlingStrategy.THROW_EXCEPTION) + .setNonFatalErrorStrategy(TemplateErrorTypeHandlingStrategy.THROW_EXCEPTION) + .build(); + } + + static ErrorHandlingStrategy ignoreAll() { + return ErrorHandlingStrategy + .builder() + .setFatalErrorStrategy(TemplateErrorTypeHandlingStrategy.IGNORE) + .setNonFatalErrorStrategy(TemplateErrorTypeHandlingStrategy.IGNORE) + .build(); + } +} diff --git a/src/main/java/com/hubspot/jinjava/interpret/ExtendsTagCycleException.java b/src/main/java/com/hubspot/jinjava/interpret/ExtendsTagCycleException.java index cc27f4fca..3c3a63431 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/ExtendsTagCycleException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/ExtendsTagCycleException.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; public class ExtendsTagCycleException extends TagCycleException { + private static final long serialVersionUID = 3183769038400532542L; public ExtendsTagCycleException(String path, int lineNumber, int startPosition) { diff --git a/src/main/java/com/hubspot/jinjava/interpret/FatalTemplateErrorsException.java b/src/main/java/com/hubspot/jinjava/interpret/FatalTemplateErrorsException.java index c3c7a7386..41c1f69e6 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/FatalTemplateErrorsException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/FatalTemplateErrorsException.java @@ -8,6 +8,7 @@ * @author jstehler */ public class FatalTemplateErrorsException extends InterpretException { + private static final long serialVersionUID = 1L; private final String template; diff --git a/src/main/java/com/hubspot/jinjava/interpret/FromTagCycleException.java b/src/main/java/com/hubspot/jinjava/interpret/FromTagCycleException.java index 20f395260..35715af8e 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/FromTagCycleException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/FromTagCycleException.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; public class FromTagCycleException extends TagCycleException { + private static final long serialVersionUID = -5487642459443650227L; public FromTagCycleException(String path, int lineNumber, int startPosition) { diff --git a/src/main/java/com/hubspot/jinjava/interpret/ImportTagCycleException.java b/src/main/java/com/hubspot/jinjava/interpret/ImportTagCycleException.java index 5dff0b6ca..4b37b9dc3 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/ImportTagCycleException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/ImportTagCycleException.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; public class ImportTagCycleException extends TagCycleException { + private static final long serialVersionUID = 1092085697026161185L; public ImportTagCycleException(String path, int lineNumber, int startPosition) { diff --git a/src/main/java/com/hubspot/jinjava/interpret/IncludeTagCycleException.java b/src/main/java/com/hubspot/jinjava/interpret/IncludeTagCycleException.java index 8ddbc6ae5..5060b3079 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/IncludeTagCycleException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/IncludeTagCycleException.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; public class IncludeTagCycleException extends TagCycleException { + private static final long serialVersionUID = -5487642459443650227L; public IncludeTagCycleException(String path, int lineNumber, int startPosition) { diff --git a/src/main/java/com/hubspot/jinjava/interpret/InterpretException.java b/src/main/java/com/hubspot/jinjava/interpret/InterpretException.java index 301a3a13c..476d0dd99 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/InterpretException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/InterpretException.java @@ -16,6 +16,7 @@ package com.hubspot.jinjava.interpret; public class InterpretException extends RuntimeException { + private static final long serialVersionUID = -3471306977643126138L; private int lineNumber = -1; diff --git a/src/main/java/com/hubspot/jinjava/interpret/InvalidArgumentException.java b/src/main/java/com/hubspot/jinjava/interpret/InvalidArgumentException.java index 72883c807..2c4272d85 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/InvalidArgumentException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/InvalidArgumentException.java @@ -3,6 +3,7 @@ import com.hubspot.jinjava.lib.Importable; public class InvalidArgumentException extends RuntimeException { + private final int lineNumber; private final int startPosition; private final String message; diff --git a/src/main/java/com/hubspot/jinjava/interpret/InvalidInputException.java b/src/main/java/com/hubspot/jinjava/interpret/InvalidInputException.java index dabbcd5a3..96d39f35a 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/InvalidInputException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/InvalidInputException.java @@ -3,6 +3,7 @@ import com.hubspot.jinjava.lib.Importable; public class InvalidInputException extends RuntimeException { + private final int lineNumber; private final int startPosition; private final String message; diff --git a/src/main/java/com/hubspot/jinjava/interpret/InvalidReason.java b/src/main/java/com/hubspot/jinjava/interpret/InvalidReason.java index 2ff1cb991..a662e493b 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/InvalidReason.java +++ b/src/main/java/com/hubspot/jinjava/interpret/InvalidReason.java @@ -17,7 +17,8 @@ public enum InvalidReason { "with value '%s' must be a valid attribute of every item in the list" ), ENUM("with value '%s' must be one of: %s"), - CIDR("with value '%s' must be a valid CIDR address"); + CIDR("with value '%s' must be a valid CIDR address"), + LENGTH("with length '%s' exceeds maximum allowed length of '%s'"); private final String errorMessage; diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index 564f9bdb8..4034ee43c 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -24,18 +24,23 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; +import com.hubspot.algebra.Result; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.el.ExpressionResolver; import com.hubspot.jinjava.el.ext.DeferredParsingException; import com.hubspot.jinjava.el.ext.ExtendedParser; +import com.hubspot.jinjava.features.BuiltInFeatures; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl; +import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; 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.loader.RelativePathResolver; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.objects.serialization.PyishSerializable; import com.hubspot.jinjava.random.ConstantZeroRandomNumberGenerator; @@ -46,14 +51,17 @@ import com.hubspot.jinjava.tree.TreeParser; import com.hubspot.jinjava.tree.output.BlockInfo; import com.hubspot.jinjava.tree.output.BlockPlaceholderOutputNode; +import com.hubspot.jinjava.tree.output.DynamicRenderedOutputNode; import com.hubspot.jinjava.tree.output.OutputList; import com.hubspot.jinjava.tree.output.OutputNode; import com.hubspot.jinjava.tree.output.RenderedOutputNode; +import com.hubspot.jinjava.util.DeferredValueUtils; import com.hubspot.jinjava.util.EagerReconstructionUtils; +import com.hubspot.jinjava.util.RenderLimitUtils; import com.hubspot.jinjava.util.Variable; 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; @@ -67,11 +75,20 @@ import java.util.Set; import java.util.Stack; import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Function; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; public class JinjavaInterpreter implements PyishSerializable { + + public static final String IGNORED_OUTPUT_FROM_EXTENDS_NOTE = + "ignored_output_from_extends"; + + public static final String OUTPUT_UNDEFINED_VARIABLES_ERROR = + BuiltInFeatures.OUTPUT_UNDEFINED_VARIABLES_ERROR; + public static final String IGNORE_NESTED_INTERPRETATION_PARSE_ERRORS = + BuiltInFeatures.IGNORE_NESTED_INTERPRETATION_PARSE_ERRORS; private final Multimap blocks = ArrayListMultimap.create(); private final LinkedList extendParentRoots = new LinkedList<>(); private final Map revertibleObjects = new HashMap<>(); @@ -87,7 +104,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( @@ -98,7 +117,6 @@ public JinjavaInterpreter( this.context = context; this.config = renderConfig; this.application = application; - this.config.getExecutionMode().prepareContext(this.context); switch (config.getRandomNumberGeneratorStrategy()) { @@ -126,6 +144,24 @@ public JinjavaInterpreter(JinjavaInterpreter orig) { scopeDepth = orig.getScopeDepth() + 1; } + public static void checkOutputSize(String string) { + if (isOutputTooLarge(string)) { + throw new OutputTooBigException( + getCurrent().getConfig().getMaxOutputSize(), + string.length() + ); + } + } + + public static boolean isOutputTooLarge(String string) { + Optional maxStringLength = getCurrentMaybe() + .map(interpreter -> interpreter.getConfig().getMaxOutputSize()) + .filter(max -> max > 0); + return ( + maxStringLength.map(max -> string != null && string.length() > max).orElse(false) + ); + } + /** * @deprecated use {{@link #getConfig()}} */ @@ -145,9 +181,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()) { @@ -213,6 +249,20 @@ public Node parse(String template) { * @return rendered result */ public String renderFlat(String template) { + return renderFlat(template, config.getMaxOutputSize()); + } + + /** + * Parse the given string into a root Node, and then render it without processing any extend parents. + * This method should be used when the template is known to not have any extends or block tags. + * + * @param template + * string to parse + * @param renderLimit + * stop rendering once this output length is reached + * @return rendered result + */ + public String renderFlat(String template, long renderLimit) { int depth = context.getRenderDepth(); try { @@ -221,13 +271,28 @@ public String renderFlat(String template) { return template; } else { context.setRenderDepth(depth + 1); - return render(parse(template), false); + Node parsedNode; + try ( + TemporaryValueClosable c = ignoreParseErrorsIfActivated() + ) { + parsedNode = parse(template); + } + return render(parsedNode, false, renderLimit); } } finally { context.setRenderDepth(depth); } } + private TemporaryValueClosable ignoreParseErrorsIfActivated() { + return config + .getFeatures() + .getActivationStrategy(BuiltInFeatures.IGNORE_NESTED_INTERPRETATION_PARSE_ERRORS) + .isActive(context) + ? context.withErrorHandlingStrategy(ErrorHandlingStrategy.ignoreAll()) + : TemporaryValueClosable.noOp(); + } + /** * Parse the given string into a root Node, and then renders it processing extend parents. * @@ -236,7 +301,20 @@ public String renderFlat(String template) { * @return rendered result */ public String render(String template) { - return render(parse(template), true); + return render(template, config.getMaxOutputSize()); + } + + /** + * Parse the given string into a root Node, and then renders it processing extend parents. + * + * @param template + * string to parse + * @param renderLimit + * stop rendering once this output length is reached + * @return rendered result + */ + public String render(String template, long renderLimit) { + return render(parse(template), true, renderLimit); } /** @@ -247,7 +325,19 @@ public String render(String template) { * @return rendered result */ public String render(Node root) { - return render(root, true); + return render(root, true, config.getMaxOutputSize()); + } + + /** + * Render the given root node with an option to process extend parents. + * Equivalent to render(root, processExtendRoots). + * @param root + * node to render + * @param processExtendRoots + * @return + */ + public String render(Node root, boolean processExtendRoots) { + return render(root, processExtendRoots, config.getMaxOutputSize()); } /** @@ -257,145 +347,203 @@ public String render(Node root) { * node to render * @param processExtendRoots * if true, also render all extend parents + * @param renderLimit + * stop rendering once this output length is reached * @return rendered result */ - public String render(Node root, boolean processExtendRoots) { - OutputList output = new OutputList(config.getMaxOutputSize()); - - for (Node node : root.getChildren()) { - lineNumber = node.getLineNumber(); - position = node.getStartPosition(); - String renderStr = node.getMaster().getImage(); - try { - if (node instanceof ExpressionNode && context.doesRenderStackContain(renderStr)) { - // This is a circular rendering. Stop rendering it here. + private String render(Node root, boolean processExtendRoots, long renderLimit) { + boolean pushed = false; + //noinspection ErrorProne + if (JinjavaInterpreter.getCurrent() != this) { + JinjavaInterpreter.pushCurrent(this); + pushed = true; + } + try { + OutputList output = new OutputList( + RenderLimitUtils.clampProvidedRenderLimitToConfig(renderLimit, config) + ); + for (Node node : root.getChildren()) { + lineNumber = node.getLineNumber(); + position = node.getStartPosition(); + String renderStr = node.getMaster().getImage(); + try { + if ( + node instanceof ExpressionNode && context.doesRenderStackContain(renderStr) + ) { + // This is a circular rendering. Stop rendering it here. + addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.EXCEPTION, + ErrorItem.TAG, + "Rendering cycle detected: '" + renderStr + "'", + null, + getLineNumber(), + node.getStartPosition(), + null, + BasicTemplateErrorCategory.IMPORT_CYCLE_DETECTED, + ImmutableMap.of("string", renderStr) + ) + ); + output.addNode(new RenderedOutputNode(renderStr)); + } else { + OutputNode out; + try ( + AutoCloseableImpl closeable = context + .closeablePushRenderStack(renderStr) + .get() + ) { + try { + out = node.render(this); + } catch (DeferredValueException e) { + context.handleDeferredNode(node); + out = new RenderedOutputNode(node.getMaster().getImage()); + } + } + output.addNode(out); + } + } catch (OutputTooBigException e) { + addError(TemplateError.fromOutputTooBigException(e)); + return output.getValue(); + } catch (CollectionTooBigException e) { addError( new TemplateError( - ErrorType.WARNING, - ErrorReason.EXCEPTION, - ErrorItem.TAG, - "Rendering cycle detected: '" + renderStr + "'", + ErrorType.FATAL, + ErrorReason.COLLECTION_TOO_BIG, + ErrorItem.OTHER, + ExceptionUtils.getMessage(e), null, - getLineNumber(), - node.getStartPosition(), - null, - BasicTemplateErrorCategory.IMPORT_CYCLE_DETECTED, - ImmutableMap.of("string", renderStr) + -1, + -1, + e, + BasicTemplateErrorCategory.UNKNOWN, + ImmutableMap.of() ) ); - output.addNode(new RenderedOutputNode(renderStr)); - } else { - OutputNode out; - context.pushRenderStack(renderStr); - try { - out = node.render(this); - } catch (DeferredValueException e) { - context.handleDeferredNode(node); - out = new RenderedOutputNode(node.getMaster().getImage()); - } - context.popRenderStack(); - output.addNode(out); + return output.getValue(); } - } catch (OutputTooBigException e) { - addError(TemplateError.fromOutputTooBigException(e)); - return output.getValue(); - } catch (CollectionTooBigException e) { - addError( - new TemplateError( - ErrorType.FATAL, - ErrorReason.COLLECTION_TOO_BIG, - ErrorItem.OTHER, - ExceptionUtils.getMessage(e), - null, - -1, - -1, - e, - BasicTemplateErrorCategory.UNKNOWN, - ImmutableMap.of() - ) - ); - return output.getValue(); } - } - StringBuilder ignoredOutput = new StringBuilder(); - - // render all extend parents, keeping the last as the root output - if (processExtendRoots) { - Set extendPaths = new HashSet<>(); - Optional extendPath = context.getExtendPathStack().peek(); - int numDeferredTokensBefore = 0; - while (!extendParentRoots.isEmpty()) { - if (extendPaths.contains(extendPath.orElse(""))) { - addError( - TemplateError.fromException( - new ExtendsTagCycleException( - extendPath.orElse(""), - context.getExtendPathStack().getTopLineNumber(), - context.getExtendPathStack().getTopStartPosition() + DynamicRenderedOutputNode pathSetter = new DynamicRenderedOutputNode(); + output.addNode(pathSetter); + Optional basePath = context.getCurrentPathStack().peek(); + StringBuilder ignoredOutput = new StringBuilder(); + boolean preserveBlocks = false; + // render all extend parents, keeping the last as the root output + if (processExtendRoots) { + Set extendPaths = new HashSet<>(); + Optional extendPath = context.getExtendPathStack().peek(); + int numDeferredTokensBefore = 0; + while (!extendParentRoots.isEmpty()) { + if (extendPaths.contains(extendPath.orElse(""))) { + addError( + TemplateError.fromException( + new ExtendsTagCycleException( + extendPath.orElse(""), + context.getExtendPathStack().getTopLineNumber(), + context.getExtendPathStack().getTopStartPosition() + ) ) - ) - ); - break; - } - extendPaths.add(extendPath.orElse("")); - context - .getCurrentPathStack() - .push( - extendPath.orElse(""), - context.getExtendPathStack().getTopLineNumber(), - context.getExtendPathStack().getTopStartPosition() - ); - Node parentRoot = extendParentRoots.removeFirst(); - if (context.getDeferredTokens().size() > numDeferredTokensBefore) { - ignoredOutput.append( - output - .getNodes() - .stream() - .filter(node -> node instanceof RenderedOutputNode) - .map(OutputNode::getValue) - .collect(Collectors.joining()) - ); - } - numDeferredTokensBefore = context.getDeferredTokens().size(); - output = new OutputList(config.getMaxOutputSize()); - - boolean hasNestedExtends = false; - for (Node node : parentRoot.getChildren()) { - lineNumber = node.getLineNumber() - 1; // The line number is off by one when rendering the extend parent - position = node.getStartPosition(); - try { - OutputNode out = node.render(this); - output.addNode(out); - if (isExtendsTag(node)) { - hasNestedExtends = true; + ); + break; + } + extendPaths.add(extendPath.orElse("")); + try ( + AutoCloseableImpl> closeableCurrentPath = + context + .getCurrentPathStack() + .closeablePush( + extendPath.orElse(""), + context.getExtendPathStack().getTopLineNumber(), + context.getExtendPathStack().getTopStartPosition() + ) + .get() + ) { + String currentPath = closeableCurrentPath + .value() + .unwrapOrElseThrow(Function.identity()); + Node parentRoot = extendParentRoots.removeFirst(); + if (context.getDeferredTokens().size() > numDeferredTokensBefore) { + ignoredOutput.append( + output + .getNodes() + .stream() + .filter(node -> node instanceof RenderedOutputNode) + .map(OutputNode::getValue) + .collect(Collectors.joining()) + ); } - } catch (OutputTooBigException e) { - addError(TemplateError.fromOutputTooBigException(e)); - return output.getValue(); + numDeferredTokensBefore = context.getDeferredTokens().size(); + output = new OutputList(config.getMaxOutputSize()); + output.addNode(pathSetter); + boolean hasNestedExtends = false; + for (Node node : parentRoot.getChildren()) { + lineNumber = node.getLineNumber() - 1; // The line number is off by one when rendering the extend parent + position = node.getStartPosition(); + try { + OutputNode out = node.render(this); + output.addNode(out); + if (isExtendsTag(node)) { + hasNestedExtends = true; + } + } catch (OutputTooBigException e) { + addError(TemplateError.fromOutputTooBigException(e)); + return output.getValue(); + } + } + Optional currentExtendPath = context.getExtendPathStack().pop(); + extendPath = + hasNestedExtends ? currentExtendPath : context.getExtendPathStack().peek(); + basePath = Optional.of(currentPath); } } + preserveBlocks = (context.getDeferredTokens().size() > numDeferredTokensBefore); + } - Optional currentExtendPath = context.getExtendPathStack().pop(); - extendPath = - hasNestedExtends ? currentExtendPath : context.getExtendPathStack().peek(); - context.getCurrentPathStack().pop(); + int numDeferredTokensBefore = context.getDeferredTokens().size(); + resolveBlockStubs(output); + if (preserveBlocks) { + for (BlockPlaceholderOutputNode blockPlaceholder : output.getBlocks()) { + blockPlaceholder.resolve( + EagerReconstructionUtils.wrapInTag( + blockPlaceholder.getValue(), + "block %s".formatted(blockPlaceholder.getBlockName()), + this, + false + ) + ); + } + } + if (context.getDeferredTokens().size() > numDeferredTokensBefore) { + pathSetter.setValue( + EagerReconstructionUtils.buildBlockOrInlineSetTag( + RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, + basePath, + this + ) + ); } - } - resolveBlockStubs(output); - if (ignoredOutput.length() > 0) { - return ( - EagerReconstructionUtils.buildBlockSetTag( - SetTag.IGNORED_VARIABLE_NAME, - ignoredOutput.toString(), - this, - false - ) + - output.getValue() - ); + if (ignoredOutput.length() > 0) { + return ( + EagerReconstructionUtils.labelWithNotes( + EagerReconstructionUtils.wrapInTag( + ignoredOutput.toString(), + DoTag.TAG_NAME, + this, + false + ), + IGNORED_OUTPUT_FROM_EXTENDS_NOTE, + this + ) + + output.getValue() + ); + } + return output.getValue(); + } finally { + if (pushed) { + JinjavaInterpreter.popCurrent(); + } } - - return output.getValue(); } private void resolveBlockStubs(OutputList output) { @@ -405,10 +553,8 @@ private void resolveBlockStubs(OutputList output) { private boolean isExtendsTag(Node node) { return ( node instanceof TagNode && - ( - ((TagNode) node).getTag() instanceof ExtendsTag || - isEagerExtendsTag((TagNode) node) - ) + (((TagNode) node).getTag() instanceof ExtendsTag || + isEagerExtendsTag((TagNode) node)) ); } @@ -419,10 +565,6 @@ private boolean isEagerExtendsTag(TagNode node) { ); } - @SuppressFBWarnings( - justification = "Iterables#getFirst DOES allow null for default value", - value = "NP_NONNULL_PARAM_VIOLATION" - ) private void resolveBlockStubs(OutputList output, Stack blockNames) { for (BlockPlaceholderOutputNode blockPlaceholder : output.getBlocks()) { if (!blockNames.contains(blockPlaceholder.getBlockName())) { @@ -438,34 +580,32 @@ private void resolveBlockStubs(OutputList output, Stack blockNames) { currentBlock = block; OutputList blockValueBuilder = new OutputList(config.getMaxOutputSize()); - - for (Node child : block.getNodes()) { - lineNumber = child.getLineNumber(); - position = child.getStartPosition(); - - boolean pushedParentPathOntoStack = false; - if ( - block.getParentPath().isPresent() && - !getContext().getCurrentPathStack().contains(block.getParentPath().get()) - ) { - getContext() - .getCurrentPathStack() - .push( - block.getParentPath().get(), - block.getParentLineNo(), - block.getParentPosition() - ); - pushedParentPathOntoStack = true; + DynamicRenderedOutputNode prefix = new DynamicRenderedOutputNode(); + blockValueBuilder.addNode(prefix); + int numDeferredTokensBefore = context.getDeferredTokens().size(); + + try ( + AutoCloseableImpl parentPathPush = conditionallyPushParentPath(block) + .get() + ) { + if (parentPathPush.value()) { lineNumber--; // The line number is off by one when rendering the block from the parent template } - blockValueBuilder.addNode(child.render(this)); + for (Node child : block.getNodes()) { + lineNumber = child.getLineNumber(); + position = child.getStartPosition(); - if (pushedParentPathOntoStack) { - getContext().getCurrentPathStack().pop(); + blockValueBuilder.addNode(child.render(this)); + } + if (context.getDeferredTokens().size() > numDeferredTokensBefore) { + EagerReconstructionUtils.reconstructPathAroundBlock( + prefix, + blockValueBuilder, + this + ); } } - blockNames.push(blockPlaceholder.getBlockName()); resolveBlockStubs(blockValueBuilder, blockNames); blockNames.pop(); @@ -483,6 +623,24 @@ private void resolveBlockStubs(OutputList output, Stack blockNames) { } } + private AutoCloseableSupplier conditionallyPushParentPath(BlockInfo block) { + if ( + block.getParentPath().isPresent() && + !getContext().getCurrentPathStack().contains(block.getParentPath().get()) + ) { + return getContext() + .getCurrentPathStack() + .closeablePush( + block.getParentPath().get(), + block.getParentLineNo(), + block.getParentPosition() + ) + .map(path -> true); + } else { + return AutoCloseableSupplier.of(false); + } + } + /** * Resolve a variable from the interpreter context, returning null if not found. This method updates the template error accumulators when a variable is not found. * @@ -505,7 +663,7 @@ public Object retraceVariable(String variable, int lineNumber, int startPosition obj = context.getDynamicVariableResolver().apply(varName); } if (obj != null) { - if (obj instanceof DeferredValue && !(obj instanceof PartiallyDeferredValue)) { + if (DeferredValueUtils.isFullyDeferred(obj)) { if (config.getExecutionMode().useEagerParser()) { throw new DeferredParsingException(this, variable); } else { @@ -513,6 +671,28 @@ public Object retraceVariable(String variable, int lineNumber, int startPosition } } obj = var.resolve(obj); + } else { + if ( + getConfig() + .getFeatures() + .getActivationStrategy(BuiltInFeatures.OUTPUT_UNDEFINED_VARIABLES_ERROR) + .isActive(context) + ) { + addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.UNKNOWN, + ErrorItem.TOKEN, + "Undefined variable: '" + variable + "'", + null, + lineNumber, + startPosition, + null, + BasicTemplateErrorCategory.UNKNOWN, + ImmutableMap.of("variable", variable) + ) + ); + } } return obj; } @@ -537,7 +717,7 @@ public Object resolveObject(String variable, int lineNumber, int startPosition) return ""; } if (WhitespaceUtils.isQuoted(variable)) { - return WhitespaceUtils.unquote(variable); + return WhitespaceUtils.unquoteAndUnescape(variable); } else { Object val = retraceVariable(variable, lineNumber, startPosition); if (val == null) { @@ -600,6 +780,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.resolveExpressionSilently(expression); + } + /** * Resolve expression against current context. * @@ -685,46 +876,61 @@ public BlockInfo getCurrentBlock() { } public void addError(TemplateError templateError) { - if (context.getThrowInterpreterErrors()) { - if (templateError.getSeverity() == ErrorType.FATAL) { - // Throw fatal errors when locating deferred words. + if (templateError == null) { + return; + } + ErrorHandlingStrategy errorHandlingStrategy = context.getErrorHandlingStrategy(); + ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy errorTypeHandlingStrategy = + templateError.getSeverity() == ErrorType.FATAL + ? errorHandlingStrategy.getFatalErrorStrategy() + : errorHandlingStrategy.getNonFatalErrorStrategy(); + switch (errorTypeHandlingStrategy) { + case IGNORE: + return; + case THROW_EXCEPTION: throw new TemplateSyntaxException( this, templateError.getFieldName(), templateError.getMessage() ); - } else { - // Hide warning errors when locating deferred words. - return; - } - } - // fix line numbers not matching up with source template - if (!context.getCurrentPathStack().isEmpty()) { - if ( - !templateError.getSourceTemplate().isPresent() && - context.getCurrentPathStack().peek().isPresent() - ) { - templateError.setMessage( - getWrappedErrorMessage( - context.getCurrentPathStack().peek().get(), - templateError - ) - ); - templateError.setSourceTemplate(context.getCurrentPathStack().peek().get()); - } - templateError.setStartPosition(context.getCurrentPathStack().getTopStartPosition()); - templateError.setLineno(context.getCurrentPathStack().getTopLineNumber()); - } + case ADD_ERROR: + default: // Checkstyle + // fix line numbers not matching up with source template + if (!context.getCurrentPathStack().isEmpty()) { + if ( + !templateError.getSourceTemplate().isPresent() && + context.getCurrentPathStack().peek().isPresent() + ) { + templateError.setMessage( + getWrappedErrorMessage( + context.getCurrentPathStack().peek().get(), + templateError + ) + ); + templateError.setSourceTemplate(context.getCurrentPathStack().peek().get()); + } + templateError.setStartPosition( + context.getCurrentPathStack().getTopStartPosition() + ); + templateError.setLineno(context.getCurrentPathStack().getTopLineNumber()); + } - // Limit the number of error. - if (errors.size() < MAX_ERROR_SIZE) { - this.errors.add(templateError.withScopeDepth(scopeDepth)); + // Limit the number of errors and filter duplicates + if (errors.size() < MAX_ERROR_SIZE) { + templateError = templateError.withScopeDepth(scopeDepth); + int errorCode = templateError.hashCode(); + if (!errorSet.contains(errorCode)) { + this.errors.add(templateError); + this.errorSet.add(errorCode); + } + } } } public void removeLastError() { if (!errors.isEmpty()) { - errors.remove(errors.size() - 1); + TemplateError error = errors.remove(errors.size() - 1); + errorSet.remove(error.hashCode()); } } @@ -760,17 +966,15 @@ public void addAllChildErrors( childErrors .stream() .limit(MAX_ERROR_SIZE - errors.size()) - .forEach( - error -> { - if (!error.getSourceTemplate().isPresent()) { - error.setMessage(getWrappedErrorMessage(childTemplateName, error)); - error.setSourceTemplate(childTemplateName); - } - error.setStartPosition(this.getPosition()); - error.setLineno(this.getLineNumber()); - this.addError(error); + .forEach(error -> { + if (!error.getSourceTemplate().isPresent()) { + error.setMessage(getWrappedErrorMessage(childTemplateName, error)); + error.setSourceTemplate(childTemplateName); } - ); + error.setStartPosition(this.getPosition()); + error.setLineno(this.getLineNumber()); + this.addError(error); + }); } // We cannot just remove this, other projects may depend on it. @@ -783,26 +987,35 @@ public List getErrorsCopy() { return Lists.newArrayList(errors); } - private static final ThreadLocal> CURRENT_INTERPRETER = ThreadLocal.withInitial( - Stack::new - ); + private static final ThreadLocal> CURRENT_INTERPRETER = + ThreadLocal.withInitial(Stack::new); public static JinjavaInterpreter getCurrent() { - if (CURRENT_INTERPRETER.get().isEmpty()) { + Stack stack = CURRENT_INTERPRETER.get(); + if (stack.isEmpty()) { return null; } - - return CURRENT_INTERPRETER.get().peek(); + return stack.peek(); } public static Optional getCurrentMaybe() { return Optional.ofNullable(getCurrent()); } + public static AutoCloseableSupplier closeablePushCurrent( + JinjavaInterpreter interpreter + ) { + Stack stack = CURRENT_INTERPRETER.get(); + stack.push(interpreter); + return AutoCloseableSupplier.of(() -> interpreter, i -> stack.pop()); + } + + @Deprecated public static void pushCurrent(JinjavaInterpreter interpreter) { CURRENT_INTERPRETER.get().push(interpreter); } + @Deprecated public static void popCurrent() { if (!CURRENT_INTERPRETER.get().isEmpty()) { CURRENT_INTERPRETER.get().pop(); @@ -860,7 +1073,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/LazyExpression.java b/src/main/java/com/hubspot/jinjava/interpret/LazyExpression.java index 7ce13fc35..ee248f05a 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/LazyExpression.java +++ b/src/main/java/com/hubspot/jinjava/interpret/LazyExpression.java @@ -4,6 +4,7 @@ import java.util.function.Supplier; public class LazyExpression implements Supplier { + private final Supplier supplier; private final String image; private final Memoization memoization; @@ -11,7 +12,7 @@ public class LazyExpression implements Supplier { public enum Memoization { ON, - OFF + OFF, } protected LazyExpression(Supplier supplier, String image, Memoization memoization) { diff --git a/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java b/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java index 456b12997..06ffe2398 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java +++ b/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java @@ -1,9 +1,11 @@ package com.hubspot.jinjava.interpret; import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; 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,8 +21,14 @@ public String getReferenceKey() { return referenceKey; } + public void setReferenceKey(String referenceKey) { + this.referenceKey = referenceKey; + } + @Override - public String 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/interpret/MacroTagCycleException.java b/src/main/java/com/hubspot/jinjava/interpret/MacroTagCycleException.java index ef17412b5..f71bfb6a2 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/MacroTagCycleException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/MacroTagCycleException.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; public class MacroTagCycleException extends TagCycleException { + private static final long serialVersionUID = -7552850581260771832L; public MacroTagCycleException(String path, int lineNumber, int startPosition) { diff --git a/src/main/java/com/hubspot/jinjava/interpret/MetaContextVariables.java b/src/main/java/com/hubspot/jinjava/interpret/MetaContextVariables.java new file mode 100644 index 000000000..2630a41ac --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/interpret/MetaContextVariables.java @@ -0,0 +1,52 @@ +package com.hubspot.jinjava.interpret; + +import com.google.common.annotations.Beta; +import java.util.Objects; + +@Beta +public class MetaContextVariables { + + public static final String TEMPORARY_META_CONTEXT_PREFIX = "__temp_meta_"; + private static final String TEMPORARY_IMPORT_ALIAS_PREFIX = + TEMPORARY_META_CONTEXT_PREFIX + "import_alias_"; + + private static final String TEMPORARY_IMPORT_ALIAS_FORMAT = + TEMPORARY_IMPORT_ALIAS_PREFIX + "%d__"; + private static final String TEMP_CURRENT_PATH_PREFIX = + TEMPORARY_META_CONTEXT_PREFIX + "current_path_"; + private static final String TEMP_CURRENT_PATH_FORMAT = + TEMP_CURRENT_PATH_PREFIX + "%d__"; + + public static boolean isMetaContextVariable(String varName, Context context) { + if (isTemporaryMetaContextVariable(varName)) { + return true; + } + return ( + context.getMetaContextVariables().contains(varName) && + !context.getNonMetaContextVariables().contains(varName) + ); + } + + private static boolean isTemporaryMetaContextVariable(String varName) { + return varName.startsWith(TEMPORARY_META_CONTEXT_PREFIX); + } + + public static boolean isTemporaryImportAlias(String varName) { + // This is just faster than checking a regex + return varName.startsWith(TEMPORARY_IMPORT_ALIAS_PREFIX); + } + + public static String getTemporaryImportAlias(String fullAlias) { + return String.format( + TEMPORARY_IMPORT_ALIAS_FORMAT, + Math.abs(Objects.hashCode(fullAlias)) + ); + } + + public static String getTemporaryCurrentPathVarName(String newPath) { + return String.format( + TEMP_CURRENT_PATH_FORMAT, + Math.abs(Objects.hash(newPath, TEMPORARY_META_CONTEXT_PREFIX) >> 1) + ); + } +} diff --git a/src/main/java/com/hubspot/jinjava/interpret/MissingEndTagException.java b/src/main/java/com/hubspot/jinjava/interpret/MissingEndTagException.java index 764c008fc..bdf154417 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/MissingEndTagException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/MissingEndTagException.java @@ -3,6 +3,7 @@ import org.apache.commons.lang3.StringUtils; public class MissingEndTagException extends TemplateSyntaxException { + private static final long serialVersionUID = 1L; private final String endTag; diff --git a/src/main/java/com/hubspot/jinjava/interpret/NotInLoopException.java b/src/main/java/com/hubspot/jinjava/interpret/NotInLoopException.java new file mode 100644 index 000000000..d4bd77eb9 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/interpret/NotInLoopException.java @@ -0,0 +1,14 @@ +package com.hubspot.jinjava.interpret; + +/** + * Exception thrown when `continue` or `break` is called outside of a loop + */ +public class NotInLoopException extends InterpretException { + + public static final String MESSAGE_PREFIX = "`"; + public static final String MESSAGE_SUFFIX = "` called while not in a for loop"; + + public NotInLoopException(String tagName) { + super(MESSAGE_PREFIX + tagName + MESSAGE_SUFFIX); + } +} diff --git a/src/main/java/com/hubspot/jinjava/interpret/NullValue.java b/src/main/java/com/hubspot/jinjava/interpret/NullValue.java new file mode 100644 index 000000000..bd7f115ce --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/interpret/NullValue.java @@ -0,0 +1,50 @@ +package com.hubspot.jinjava.interpret; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.hubspot.jinjava.interpret.NullValue.NullValueSerializer; +import java.io.IOException; + +/** + * Marker object of a `null` value. A null value in the map is usually considered + * the key does not exist. For example map = {"a": null}, if map.get("a") == null, + * we treat it as the there is not key "a" in the map. + */ +@JsonSerialize(using = NullValueSerializer.class) +public final class NullValue { + + public static final NullValue INSTANCE = new NullValue(); + + public static class NullValueSerializer extends StdSerializer { + + public NullValueSerializer() { + this(null); + } + + protected NullValueSerializer(Class t) { + super(t); + } + + @Override + public void serialize( + NullValue value, + JsonGenerator jgen, + SerializerProvider provider + ) throws IOException { + jgen.writeNull(); + } + } + + private NullValue() {} + + public static NullValue instance() { + return INSTANCE; + } + + @Override + public String toString() { + return "null"; + } +} diff --git a/src/main/java/com/hubspot/jinjava/interpret/OneTimeReconstructible.java b/src/main/java/com/hubspot/jinjava/interpret/OneTimeReconstructible.java new file mode 100644 index 000000000..197a7050c --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/interpret/OneTimeReconstructible.java @@ -0,0 +1,7 @@ +package com.hubspot.jinjava.interpret; + +public interface OneTimeReconstructible extends DeferredValue { + boolean isReconstructed(); + + void setReconstructed(boolean reconstructed); +} diff --git a/src/main/java/com/hubspot/jinjava/interpret/OutputTooBigException.java b/src/main/java/com/hubspot/jinjava/interpret/OutputTooBigException.java index 79ac4fe64..d13e59530 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/OutputTooBigException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/OutputTooBigException.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; public class OutputTooBigException extends RuntimeException { + private long maxSize; private final long size; diff --git a/src/main/java/com/hubspot/jinjava/interpret/PartiallyDeferredValue.java b/src/main/java/com/hubspot/jinjava/interpret/PartiallyDeferredValue.java index 4da2c3d12..73e355146 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/PartiallyDeferredValue.java +++ b/src/main/java/com/hubspot/jinjava/interpret/PartiallyDeferredValue.java @@ -1,7 +1,10 @@ package com.hubspot.jinjava.interpret; +import com.google.common.annotations.Beta; + /** * An interface for a type of DeferredValue that as a whole is not deferred, * but certain attributes or methods within it are deferred. */ +@Beta public interface PartiallyDeferredValue extends DeferredValue {} diff --git a/src/main/java/com/hubspot/jinjava/interpret/RenderResult.java b/src/main/java/com/hubspot/jinjava/interpret/RenderResult.java index 7fd811ee1..4c2d1fd7a 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/RenderResult.java +++ b/src/main/java/com/hubspot/jinjava/interpret/RenderResult.java @@ -6,6 +6,7 @@ import java.util.Optional; public class RenderResult { + private final String output; private final Context context; private final List errors; diff --git a/src/main/java/com/hubspot/jinjava/interpret/RevertibleObject.java b/src/main/java/com/hubspot/jinjava/interpret/RevertibleObject.java index 1b4d894d4..acee72604 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/RevertibleObject.java +++ b/src/main/java/com/hubspot/jinjava/interpret/RevertibleObject.java @@ -1,8 +1,11 @@ package com.hubspot.jinjava.interpret; +import com.google.common.annotations.Beta; import java.util.Optional; +@Beta public class RevertibleObject { + private final Object hashCode; private final Optional pyishString; diff --git a/src/main/java/com/hubspot/jinjava/interpret/TagCycleException.java b/src/main/java/com/hubspot/jinjava/interpret/TagCycleException.java index 4128f52ff..a27248840 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/TagCycleException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/TagCycleException.java @@ -3,6 +3,7 @@ import java.util.Optional; public class TagCycleException extends TemplateStateException { + private static final long serialVersionUID = -3058494056577268723L; private final String path; diff --git a/src/main/java/com/hubspot/jinjava/interpret/TemplateError.java b/src/main/java/com/hubspot/jinjava/interpret/TemplateError.java index ff143aadb..495f617ed 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/TemplateError.java +++ b/src/main/java/com/hubspot/jinjava/interpret/TemplateError.java @@ -10,6 +10,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils; public class TemplateError { + private static final Pattern GENERIC_TOSTRING_PATTERN = Pattern.compile( "@[0-9a-z]{4,}$" ); @@ -17,7 +18,7 @@ public class TemplateError { public enum ErrorType { FATAL, - WARNING + WARNING, } public enum ErrorReason { @@ -32,7 +33,7 @@ public enum ErrorReason { OUTPUT_TOO_BIG, OVER_LIMIT, COLLECTION_TOO_BIG, - OTHER + OTHER, } public enum ErrorItem { @@ -43,7 +44,7 @@ public enum ErrorItem { PROPERTY, FILTER, EXPRESSION_TEST, - OTHER + OTHER, } private final ErrorType severity; @@ -134,6 +135,19 @@ public static TemplateError fromInvalidInputException(InvalidInputException ex) ); } + public static TemplateError fromMissingFilterArgException(InvalidArgumentException ex) { + return new TemplateError( + ErrorType.WARNING, + ErrorReason.INVALID_ARGUMENT, + ErrorItem.FILTER, + ex.getMessage(), + ex.getName(), + ex.getLineNumber(), + ex.getStartPosition(), + ex + ); + } + public static TemplateError fromException(Exception ex) { int lineNumber = -1; int startPosition = -1; @@ -540,7 +554,8 @@ public int hashCode() { startPosition, category, categoryErrors, - scopeDepth + scopeDepth, + sourceTemplate ); } } diff --git a/src/main/java/com/hubspot/jinjava/interpret/TemplateStateException.java b/src/main/java/com/hubspot/jinjava/interpret/TemplateStateException.java index 1e1b75bf5..e98aaeb32 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/TemplateStateException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/TemplateStateException.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; public class TemplateStateException extends InterpretException { + private static final long serialVersionUID = 426925445445430522L; public TemplateStateException(String msg) { diff --git a/src/main/java/com/hubspot/jinjava/interpret/TemplateSyntaxException.java b/src/main/java/com/hubspot/jinjava/interpret/TemplateSyntaxException.java index c31a22d23..1ac4ff731 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/TemplateSyntaxException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/TemplateSyntaxException.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; public class TemplateSyntaxException extends InterpretException { + private static final long serialVersionUID = 1L; private final String code; diff --git a/src/main/java/com/hubspot/jinjava/interpret/UnexpectedTokenException.java b/src/main/java/com/hubspot/jinjava/interpret/UnexpectedTokenException.java index 1e231471b..1d1ae33a3 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/UnexpectedTokenException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/UnexpectedTokenException.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; public class UnexpectedTokenException extends TemplateSyntaxException { + private static final long serialVersionUID = 1L; private final String token; diff --git a/src/main/java/com/hubspot/jinjava/interpret/UnknownTagException.java b/src/main/java/com/hubspot/jinjava/interpret/UnknownTagException.java index 216c86110..a07621ce7 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/UnknownTagException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/UnknownTagException.java @@ -3,6 +3,7 @@ import com.hubspot.jinjava.tree.parse.TagToken; public class UnknownTagException extends TemplateSyntaxException { + private static final long serialVersionUID = 1L; private final String tag; diff --git a/src/main/java/com/hubspot/jinjava/interpret/UnknownTokenException.java b/src/main/java/com/hubspot/jinjava/interpret/UnknownTokenException.java index 3e0425a1e..243473826 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/UnknownTokenException.java +++ b/src/main/java/com/hubspot/jinjava/interpret/UnknownTokenException.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.interpret; public class UnknownTokenException extends InterpretException { + private static final long serialVersionUID = -388757722051666198L; private final String token; diff --git a/src/main/java/com/hubspot/jinjava/interpret/errorcategory/BasicTemplateErrorCategory.java b/src/main/java/com/hubspot/jinjava/interpret/errorcategory/BasicTemplateErrorCategory.java index ccfcaa912..e17932a44 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/errorcategory/BasicTemplateErrorCategory.java +++ b/src/main/java/com/hubspot/jinjava/interpret/errorcategory/BasicTemplateErrorCategory.java @@ -8,5 +8,5 @@ public enum BasicTemplateErrorCategory implements TemplateErrorCategory { UNKNOWN, UNKNOWN_DATE, UNKNOWN_LOCALE, - UNKNOWN_PROPERTY + UNKNOWN_PROPERTY, } diff --git a/src/main/java/com/hubspot/jinjava/lib/SimpleLibrary.java b/src/main/java/com/hubspot/jinjava/lib/SimpleLibrary.java index 5f6945a3d..93afea000 100644 --- a/src/main/java/com/hubspot/jinjava/lib/SimpleLibrary.java +++ b/src/main/java/com/hubspot/jinjava/lib/SimpleLibrary.java @@ -28,6 +28,7 @@ import java.util.stream.Collectors; public abstract class SimpleLibrary { + private Map lib = new HashMap<>(); private Set disabled = new HashSet<>(); diff --git a/src/main/java/com/hubspot/jinjava/lib/expression/DefaultExpressionStrategy.java b/src/main/java/com/hubspot/jinjava/lib/expression/DefaultExpressionStrategy.java index cc1c2b589..96140b745 100644 --- a/src/main/java/com/hubspot/jinjava/lib/expression/DefaultExpressionStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/expression/DefaultExpressionStrategy.java @@ -1,5 +1,7 @@ package com.hubspot.jinjava.lib.expression; +import com.hubspot.jinjava.features.BuiltInFeatures; +import com.hubspot.jinjava.features.FeatureActivationStrategy; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.filter.EscapeFilter; import com.hubspot.jinjava.objects.SafeString; @@ -9,7 +11,9 @@ import org.apache.commons.lang3.StringUtils; public class DefaultExpressionStrategy implements ExpressionStrategy { + private static final long serialVersionUID = 436239440273704843L; + public static final String ECHO_UNDEFINED = BuiltInFeatures.ECHO_UNDEFINED; public RenderedOutputNode interpretOutput( ExpressionToken master, @@ -19,15 +23,23 @@ public RenderedOutputNode interpretOutput( master.getExpr(), master.getLineNumber() ); + + final FeatureActivationStrategy feat = interpreter + .getConfig() + .getFeatures() + .getActivationStrategy(BuiltInFeatures.ECHO_UNDEFINED); + + if (var == null && feat.isActive(interpreter.getContext())) { + return new RenderedOutputNode(master.getImage()); + } + String result = interpreter.getAsString(var); if (interpreter.getConfig().isNestedInterpretationEnabled()) { if ( !StringUtils.equals(result, master.getImage()) && - ( - StringUtils.contains(result, master.getSymbols().getExpressionStart()) || - StringUtils.contains(result, master.getSymbols().getExpressionStartWithTag()) - ) + (StringUtils.contains(result, master.getSymbols().getExpressionStart()) || + StringUtils.contains(result, master.getSymbols().getExpressionStartWithTag())) ) { try { result = interpreter.renderFlat(result); 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..35a184f52 100644 --- a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java +++ b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java @@ -1,24 +1,26 @@ package com.hubspot.jinjava.lib.expression; +import com.google.common.annotations.Beta; import com.hubspot.jinjava.JinjavaConfig; -import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; +import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; +import com.hubspot.jinjava.interpret.ErrorHandlingStrategy; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; +import com.hubspot.jinjava.interpret.TemplateSyntaxException; 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; 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; +import com.hubspot.jinjava.util.PrefixToPreserveState; import org.apache.commons.lang3.StringUtils; +@Beta public class EagerExpressionStrategy implements ExpressionStrategy { + private static final long serialVersionUID = -6792345439237764193L; @Override @@ -34,25 +36,30 @@ private String eagerResolveExpression( JinjavaInterpreter interpreter ) { interpreter.getContext().checkNumberOfDeferredTokens(); - EagerExecutionResult eagerExecutionResult = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult eagerExecutionResult = EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResolver.resolveExpression(master.getExpr(), interpreter), interpreter, - EagerChildContextConfig + EagerContextWatcher.EagerChildContextConfig .newBuilder() .withTakeNewValue(true) .withPartialMacroEvaluation( interpreter.getConfig().isNestedInterpretationEnabled() ) - .withCheckForContextChanges(interpreter.getContext().isDeferredExecutionMode()) .build() ); - StringBuilder prefixToPreserveState = new StringBuilder(); - if (interpreter.getContext().isDeferredExecutionMode()) { - prefixToPreserveState.append(eagerExecutionResult.getPrefixToPreserveState()); + PrefixToPreserveState prefixToPreserveState = new PrefixToPreserveState(); + if ( + !eagerExecutionResult.getResult().isFullyResolved() || + interpreter.getContext().isDeferredExecutionMode() + ) { + prefixToPreserveState.putAll(eagerExecutionResult.getPrefixToPreserveState()); } else { - interpreter.getContext().putAll(eagerExecutionResult.getSpeculativeBindings()); + EagerReconstructionUtils.commitSpeculativeBindings( + interpreter, + eagerExecutionResult + ); } if (eagerExecutionResult.getResult().isFullyResolved()) { String result = eagerExecutionResult.getResult().toString(true); @@ -60,40 +67,28 @@ private String eagerResolveExpression( prefixToPreserveState.toString() + postProcessResult(master, result, interpreter) ); } - prefixToPreserveState.append( - EagerReconstructionUtils.reconstructFromContextBeforeDeferring( - eagerExecutionResult.getResult().getDeferredWords(), - interpreter - ) - ); - String helpers = wrapInExpression( + + String deferredExpressionImage = wrapInExpression( eagerExecutionResult.getResult().toString(), interpreter ); - interpreter - .getContext() - .handleDeferredToken( - new DeferredToken( - new ExpressionToken( - helpers, - master.getLineNumber(), - master.getStartPosition(), - master.getSymbols() - ), - eagerExecutionResult - .getResult() - .getDeferredWords() - .stream() - .filter( - word -> - !(interpreter.getContext().get(word) instanceof DeferredMacroValueImpl) - ) - .collect(Collectors.toSet()) - ) - ); + EagerReconstructionUtils.hydrateReconstructionFromContextBeforeDeferring( + prefixToPreserveState, + eagerExecutionResult.getResult().getDeferredWords(), + interpreter + ); + prefixToPreserveState.withAllInFront( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromImage(deferredExpressionImage, master) + .addUsedDeferredWords(eagerExecutionResult.getResult().getDeferredWords()) + .build() + ) + ); // There is only a preserving prefix because it couldn't be entirely evaluated. return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( - prefixToPreserveState.toString() + helpers, + prefixToPreserveState.toString() + deferredExpressionImage, interpreter ); } @@ -105,26 +100,26 @@ public static String postProcessResult( ) { if ( !StringUtils.equals(result, master.getImage()) && - ( - StringUtils.contains(result, master.getSymbols().getExpressionStart()) || - StringUtils.contains(result, master.getSymbols().getExpressionStartWithTag()) - ) + (StringUtils.contains(result, master.getSymbols().getExpressionStart()) || + StringUtils.contains(result, master.getSymbols().getExpressionStartWithTag())) ) { if (interpreter.getConfig().isNestedInterpretationEnabled()) { - long errorSizeStart = getParsingErrorsCount(interpreter); - - interpreter.parse(result); - - if (getParsingErrorsCount(interpreter) == errorSizeStart) { + try { + try ( + TemporaryValueClosable c = interpreter + .getContext() + .withErrorHandlingStrategy(ErrorHandlingStrategy.throwAll()) + ) { + interpreter.parse(result); + } try { result = interpreter.renderFlat(result); } catch (Exception e) { Logging.ENGINE_LOG.warn("Error rendering variable node result", e); } - } + } catch (TemplateSyntaxException ignored) {} } else { - // Possible macro/set tag in front of this one. Includes result - result = wrapInRawOrExpressionIfNeeded(result, interpreter); + result = EagerReconstructionUtils.wrapInRawIfNeeded(result, interpreter); } } @@ -134,37 +129,6 @@ public static String postProcessResult( return result; } - private static long getParsingErrorsCount(JinjavaInterpreter interpreter) { - return interpreter - .getErrors() - .stream() - .filter(Objects::nonNull) - .filter( - error -> - "Unclosed comment".equals(error.getMessage()) || - error.getReason() == ErrorReason.DISABLED - ) - .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/exptest/CollectionExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/CollectionExpTest.java index 05062b693..d2190bca7 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/CollectionExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/CollectionExpTest.java @@ -4,6 +4,8 @@ import com.hubspot.jinjava.el.ext.CollectionMembershipOperator; public abstract class CollectionExpTest implements ExpTest { + protected static final TruthyTypeConverter TYPE_CONVERTER = new TruthyTypeConverter(); - protected static final CollectionMembershipOperator COLLECTION_MEMBERSHIP_OPERATOR = new CollectionMembershipOperator(); + protected static final CollectionMembershipOperator COLLECTION_MEMBERSHIP_OPERATOR = + new CollectionMembershipOperator(); } diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsBooleanExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsBooleanExpTest.java index 89b053481..daa4c5574 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsBooleanExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsBooleanExpTest.java @@ -13,7 +13,7 @@ code = "{% if true is boolean %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsBooleanExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsDefinedExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsDefinedExpTest.java index ca4429803..3b850322f 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsDefinedExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsDefinedExpTest.java @@ -13,7 +13,7 @@ code = "{% if variable is defined %}\n" + "\n" + "{% endif %}" - ) + ), } ) public class IsDefinedExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsDivisibleByExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsDivisibleByExpTest.java index ffcd9f073..1beb4a0f8 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsDivisibleByExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsDivisibleByExpTest.java @@ -24,7 +24,7 @@ "{% else %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsDivisibleByExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsEqualToExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsEqualToExpTest.java index a14c3220a..d3095b139 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsEqualToExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsEqualToExpTest.java @@ -18,7 +18,7 @@ type = "object", desc = "Another object to check equality against", required = true - ) + ), }, snippets = { @JinjavaSnippet( @@ -29,10 +29,11 @@ @JinjavaSnippet( desc = "Usage with the selectattr filter", code = "{{ users|selectattr(\"email\", \"equalto\", \"foo@bar.invalid\") }}" - ) + ), } ) public class IsEqualToExpTest implements ExpTest { + private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter(); @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsEvenExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsEvenExpTest.java index de0fa2842..707ffb875 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsEvenExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsEvenExpTest.java @@ -15,7 +15,7 @@ "{% else %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsEvenExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsFalseExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsFalseExpTest.java index 54b6711e3..e3c15fd0c 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsFalseExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsFalseExpTest.java @@ -13,7 +13,7 @@ code = "{% if false is false %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsFalseExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTest.java index e77cf544e..7b0af240a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTest.java @@ -14,7 +14,7 @@ code = "{% if num is float %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsFloatExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsGeTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsGeTest.java index 7e9a9f260..dbcf6279b 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsGeTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsGeTest.java @@ -18,7 +18,7 @@ type = "object", desc = "Another object to compare against", required = true - ) + ), }, snippets = { @JinjavaSnippet( @@ -29,10 +29,11 @@ @JinjavaSnippet( desc = "Usage with the selectattr filter", code = "{{ users|selectattr(\"num\", \"ge\", \"2\") }}" - ) + ), } ) public class IsGeTest implements ExpTest { + private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter(); @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsGtTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsGtTest.java index 0e24de3ec..c88dcd481 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsGtTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsGtTest.java @@ -18,7 +18,7 @@ type = "object", desc = "Another object to compare against", required = true - ) + ), }, snippets = { @JinjavaSnippet( @@ -29,10 +29,11 @@ @JinjavaSnippet( desc = "Usage with the selectattr filter", code = "{{ users|selectattr(\"num\", \"gt\", \"2\") }}" - ) + ), } ) public class IsGtTest implements ExpTest { + private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter(); @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsInExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsInExpTest.java index 6b6c4fe8b..f40d2b37e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsInExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsInExpTest.java @@ -21,7 +21,7 @@ snippets = { @JinjavaSnippet(code = "{{ 2 is in [1, 2, 3] }}"), @JinjavaSnippet(code = "{{ 'b' is in 'abc' }}"), - @JinjavaSnippet(code = "{{ 'k2' is in {'k1':'v1', 'k2':'v2'} }}") + @JinjavaSnippet(code = "{{ 'k2' is in {'k1':'v1', 'k2':'v2'} }}"), } ) public class IsInExpTest extends CollectionExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsIntegerExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsIntegerExpTest.java index 79f646ee4..1500abad3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsIntegerExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsIntegerExpTest.java @@ -15,7 +15,7 @@ code = "{% if num is integer %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsIntegerExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsIterableExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsIterableExpTest.java index 9c2bb2b85..61b34face 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsIterableExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsIterableExpTest.java @@ -13,7 +13,7 @@ code = "{% if variable is iterable %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsIterableExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLeTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLeTest.java index 956f3a2b5..e1423cf10 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLeTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLeTest.java @@ -18,7 +18,7 @@ type = "object", desc = "Another object to compare against", required = true - ) + ), }, snippets = { @JinjavaSnippet( @@ -29,10 +29,11 @@ @JinjavaSnippet( desc = "Usage with the selectattr filter", code = "{{ users|selectattr(\"num\", \"le\", \"2\") }}" - ) + ), } ) public class IsLeTest implements ExpTest { + private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter(); @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLessThanOrEqualToSymbolExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLessThanOrEqualToSymbolExpTest.java index b3bb1e148..b9c19a5c5 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLessThanOrEqualToSymbolExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLessThanOrEqualToSymbolExpTest.java @@ -3,7 +3,7 @@ import com.hubspot.jinjava.doc.annotations.JinjavaDoc; @JinjavaDoc(value = "", aliasOf = "le") -public class IsLessThanOrEqualToSymbolExpTest extends IsLtTest { +public class IsLessThanOrEqualToSymbolExpTest extends IsLeTest { @Override public String getName() { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLowerExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLowerExpTest.java index c9be8e4ef..810067a49 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLowerExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLowerExpTest.java @@ -14,7 +14,7 @@ code = "{% if variable is lower %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsLowerExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLtTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLtTest.java index afea491f3..ebcc184ce 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLtTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLtTest.java @@ -18,7 +18,7 @@ type = "object", desc = "Another object to compare against", required = true - ) + ), }, snippets = { @JinjavaSnippet( @@ -29,10 +29,11 @@ @JinjavaSnippet( desc = "Usage with the selectattr filter", code = "{{ users|selectattr(\"num\", \"lt\", \"2\") }}" - ) + ), } ) public class IsLtTest implements ExpTest { + private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter(); @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsMappingExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsMappingExpTest.java index cb0c415f2..5cd697544 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsMappingExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsMappingExpTest.java @@ -14,7 +14,7 @@ code = "{% if variable is mapping %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsMappingExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsNeExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsNeExpTest.java index 311a4e192..057382094 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsNeExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsNeExpTest.java @@ -18,7 +18,7 @@ type = "object", desc = "Another object to check inequality against", required = true - ) + ), }, snippets = { @JinjavaSnippet( @@ -29,10 +29,11 @@ @JinjavaSnippet( desc = "Usage with the selectattr filter", code = "{{ users|selectattr(\"email\", \"ne\", \"foo@bar.invalid\") }}" - ) + ), } ) public class IsNeExpTest implements ExpTest { + private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter(); @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsNoneExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsNoneExpTest.java index 87a2b2683..93ff43f1e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsNoneExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsNoneExpTest.java @@ -13,7 +13,7 @@ code = "{% unless variable is none %}\n" + " \n" + "{% endunless %}" - ) + ), } ) public class IsNoneExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsNumberExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsNumberExpTest.java index 9d03e6133..a024a8c06 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsNumberExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsNumberExpTest.java @@ -15,7 +15,7 @@ "{% else %}\n" + " The variable is not a number.\n" + "{% endif %}" - ) + ), } ) public class IsNumberExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsOddExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsOddExpTest.java index c22de8edb..287d15374 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsOddExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsOddExpTest.java @@ -15,7 +15,7 @@ "{% else %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsOddExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsSameAsExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsSameAsExpTest.java index 1c13627a9..c357210d0 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsSameAsExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsSameAsExpTest.java @@ -20,7 +20,7 @@ code = "{% if var_one is sameas var_two %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsSameAsExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsSequenceExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsSequenceExpTest.java index e2e2cfa53..c48c5e133 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsSequenceExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsSequenceExpTest.java @@ -15,7 +15,7 @@ code = "{% if variable is sequence %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsSequenceExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringContainingExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringContainingExpTest.java index c5fa1fd2b..01d17d707 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringContainingExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringContainingExpTest.java @@ -20,7 +20,7 @@ code = "{% if variable is string_containing 'foo' %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsStringContainingExpTest extends IsStringExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringExpTest.java index 47a49c3ab..b9cb56cdb 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringExpTest.java @@ -14,7 +14,7 @@ code = "{% if variable is string %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsStringExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringStartingWithExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringStartingWithExpTest.java index 398115035..8a7b527c0 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringStartingWithExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringStartingWithExpTest.java @@ -20,7 +20,7 @@ code = "{% if variable is string_startingwith 'foo' %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsStringStartingWithExpTest extends IsStringExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsTrueExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsTrueExpTest.java index c1c5407d3..31338ee4e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsTrueExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsTrueExpTest.java @@ -13,7 +13,7 @@ code = "{% if false is true %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsTrueExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsTruthyExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsTruthyExpTest.java index 911dbb816..8a7d09875 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsTruthyExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsTruthyExpTest.java @@ -14,7 +14,7 @@ code = "{% if variable is truthy %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsTruthyExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsUndefinedExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsUndefinedExpTest.java index 13c481cbb..f1cb78e4b 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsUndefinedExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsUndefinedExpTest.java @@ -13,7 +13,7 @@ code = "{% if variable is undefined %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsUndefinedExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsUpperExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsUpperExpTest.java index e7ca084e1..61d8246fe 100644 --- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsUpperExpTest.java +++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsUpperExpTest.java @@ -15,7 +15,7 @@ code = "{% if variable is upper %}\n" + " \n" + "{% endif %}" - ) + ), } ) public class IsUpperExpTest implements ExpTest { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AbsFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AbsFilter.java index ea860b024..578a92407 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/AbsFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AbsFilter.java @@ -33,7 +33,7 @@ required = true ), snippets = { - @JinjavaSnippet(code = "{% set my_number = -53 %}\n" + "{{ my_number|abs }}") + @JinjavaSnippet(code = "{% set my_number = -53 %}\n" + "{{ my_number|abs }}"), } ) public class AbsFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractFilter.java index 978ce559a..b39344c05 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractFilter.java @@ -21,7 +21,6 @@ import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.interpret.InvalidInputException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; @@ -43,8 +42,11 @@ * @see JinjavaParam */ public abstract class AbstractFilter implements Filter { - private static final Map> NAMED_ARGUMENTS_CACHE = new ConcurrentHashMap<>(); - private static final Map> DEFAULT_VALUES_CACHE = new ConcurrentHashMap<>(); + + private static final Map> NAMED_ARGUMENTS_CACHE = + new ConcurrentHashMap<>(); + private static final Map> DEFAULT_VALUES_CACHE = + new ConcurrentHashMap<>(); private final Map namedArguments; private final Map defaultValues; @@ -66,10 +68,6 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) return filter(var, interpreter, args, Collections.emptyMap()); } - @SuppressFBWarnings( - value = "UC_USELESS_OBJECT", - justification = "FB bug prevents forEach() method call counting `namedArgs` as used (fixed in next release)" - ) public Object filter( Object var, JinjavaInterpreter interpreter, @@ -114,8 +112,8 @@ public Object filter( //Parse args based on their declared types Map parsedArgs = new HashMap<>(); - namedArgs.forEach( - (k, v) -> parsedArgs.put(k, parseArg(interpreter, namedArguments.get(k), v)) + namedArgs.forEach((k, v) -> + parsedArgs.put(k, parseArg(interpreter, namedArguments.get(k), v)) ); validateArgs(interpreter, parsedArgs); @@ -204,9 +202,8 @@ public String getIndexedArgumentName(int position) { .ofNullable(namedArguments) .map(Map::keySet) .map(ArrayList::new) - .flatMap( - argNames -> - Optional.ofNullable(argNames.size() > position ? argNames.get(position) : null) + .flatMap(argNames -> + Optional.ofNullable(argNames.size() > position ? argNames.get(position) : null) ) .orElse(null); } @@ -220,12 +217,13 @@ public Map initNamedArguments() { } if (jinjavaDoc != null) { - ImmutableMap.Builder namedArgsBuilder = ImmutableMap.builder(); + ImmutableMap.Builder namedArgsBuilder = + ImmutableMap.builder(); Arrays .stream(jinjavaDoc.params()) - .forEachOrdered( - jinjavaParam -> namedArgsBuilder.put(jinjavaParam.value(), jinjavaParam) + .forEachOrdered(jinjavaParam -> + namedArgsBuilder.put(jinjavaParam.value(), jinjavaParam) ); return namedArgsBuilder.build(); 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..664f82760 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java @@ -1,10 +1,14 @@ package com.hubspot.jinjava.lib.filter; +import com.hubspot.jinjava.features.BuiltInFeatures; 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; import com.hubspot.jinjava.util.ObjectIterator; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; public abstract class AbstractSetFilter implements AdvancedFilter { @@ -29,4 +33,112 @@ 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)); + + if (!varSet.isEmpty() && !argSet.isEmpty()) { + Object oneVar = varSet.iterator().next(); + Object oneArg = argSet.iterator().next(); + + boolean featureActive = interpreter + .getConfig() + .getFeatures() + .isActive( + BuiltInFeatures.INTEGER_SET_TO_LONG_CONVERSION, + interpreter.getContext() + ); + if (featureActive) { + if (oneVar instanceof Integer && oneArg instanceof Long) { + varSet = convertIntegersToLongs(varSet); + } else if (oneArg instanceof Integer && oneVar instanceof Long) { + argSet = convertIntegersToLongs(argSet); + } + } + + attachMismatchedTypesWarning(interpreter, varSet, argSet, oneVar, oneArg); + } + + return filter(varSet, argSet); + } + + public abstract Object filter(Set varSet, Set argSet); + + protected void attachMismatchedTypesWarning( + JinjavaInterpreter interpreter, + Set varSet, + Set argSet + ) { + if (varSet.isEmpty() || argSet.isEmpty()) { + return; + } + attachMismatchedTypesWarning( + interpreter, + varSet, + argSet, + varSet.iterator().next(), + argSet.iterator().next() + ); + } + + private void attachMismatchedTypesWarning( + JinjavaInterpreter interpreter, + Set varSet, + Set argSet, + Object oneVarObj, + Object oneArgObj + ) { + if (getTypeOfSetElements(varSet).equals(getTypeOfSetElements(argSet))) { + return; + } + if (potentiallyConvertibleNumbers(oneVarObj, oneArgObj)) { + 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(), + interpreter.getPosition(), + null + ) + ); + } + + private boolean potentiallyConvertibleNumbers(Object oneVarObj, Object oneArgObj) { + return ( + (oneArgObj instanceof Integer && oneVarObj instanceof Long) || + (oneVarObj instanceof Integer && oneArgObj instanceof Long) + ); + } + + private Set convertIntegersToLongs(Set set) { + Set result = new LinkedHashSet<>(); + for (Object element : set) { + if (element instanceof Integer integer) { + result.add(integer.longValue()); + } else { + result.add(element); + } + } + return result; + } + + private String getTypeOfSetElements(Set set) { + return TypeFunction.type(set.iterator().next()); + } } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AddFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AddFilter.java index 100f5a390..9670f6946 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/AddFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AddFilter.java @@ -39,10 +39,10 @@ type = "number", desc = "The number added to the base number", required = true - ) + ), }, snippets = { - @JinjavaSnippet(code = "{% set my_num = 40 %} \n" + "{{ my_num|add(13) }}") + @JinjavaSnippet(code = "{% set my_num = 40 %} \n" + "{{ my_num|add(13) }}"), } ) public class AddFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AdvancedFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AdvancedFilter.java index f8bb532a5..fa365e7c9 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/AdvancedFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AdvancedFilter.java @@ -43,6 +43,6 @@ Object filter( // Default implementation to maintain backward-compatibility with old Filters default Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - return filter(var, interpreter, (Object[]) args, new HashMap<>()); + return filter(var, interpreter, args, new HashMap<>()); } } 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..044df68f8 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilter.java @@ -0,0 +1,44 @@ +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/AttrFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AttrFilter.java index d21f24a86..fef32e1a3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/AttrFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AttrFilter.java @@ -20,13 +20,13 @@ value = "name", desc = "The dictionary attribute name to access", required = true - ) + ), }, snippets = { @JinjavaSnippet( desc = "The filter example below is equivalent to rendering a variable that exists within a dictionary, such as content.absolute_url.", code = "{{ content|attr('absolute_url') }}" - ) + ), } ) public class AttrFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/Base64DecodeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/Base64DecodeFilter.java index e43080e80..ca4dae9dc 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/Base64DecodeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/Base64DecodeFilter.java @@ -24,7 +24,7 @@ type = "string", desc = "The string encoding charset to use.", defaultValue = "UTF-8" - ) + ), }, snippets = { @JinjavaSnippet( @@ -34,10 +34,11 @@ @JinjavaSnippet( desc = "Decode a Base 64-encoded ASCII string into a UTF-16 Little Endian string", code = "{{ 'Adg33A=='|b64decode(encoding='utf-16le') }}" - ) + ), } ) public class Base64DecodeFilter implements Filter { + public static final String NAME = "b64decode"; @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/Base64EncodeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/Base64EncodeFilter.java index cb504483b..9dbfeb3f3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/Base64EncodeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/Base64EncodeFilter.java @@ -30,7 +30,7 @@ type = "string", desc = "The string encoding charset to use.", defaultValue = "UTF-8" - ) + ), }, snippets = { @JinjavaSnippet( @@ -40,10 +40,11 @@ @JinjavaSnippet( desc = "Encode a value with UTF-16 Little Endian encoding into a Base 64 ASCII string", code = "{{ '\uD801\uDC37'|b64encode(encoding='utf-16le') }}" - ) + ), } ) public class Base64EncodeFilter implements Filter { + public static final String NAME = "b64encode"; public static final String AVAILABLE_CHARSETS = Joiner .on(", ") diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/BaseDateFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/BaseDateFilter.java index 52ec2c9bd..1ff22d089 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/BaseDateFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/BaseDateFilter.java @@ -11,6 +11,7 @@ import java.util.stream.Collectors; public abstract class BaseDateFilter implements AdvancedFilter { + private static final Map unitMap = Arrays .stream(ChronoUnit.values()) .collect(Collectors.toMap(u -> u.toString().toLowerCase(), u -> u)); diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/BatchFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/BatchFilter.java index 1714c734f..e58f7fc58 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/BatchFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/BatchFilter.java @@ -25,7 +25,7 @@ desc = "Number of items to include in the batch", defaultValue = "0" ), - @JinjavaParam(value = "fill_with", desc = "Value used to fill up missing items") + @JinjavaParam(value = "fill_with", desc = "Value used to fill up missing items"), }, snippets = { @JinjavaSnippet( @@ -51,7 +51,7 @@ "  \n" + " \n" + "" - ) + ), } ) public class BatchFilter implements Filter { 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..878fe2533 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/BetweenTimesFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/BetweenTimesFilter.java @@ -3,9 +3,13 @@ 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.features.BuiltInFeatures; +import com.hubspot.jinjava.features.DateTimeFeatureActivationStrategy; +import com.hubspot.jinjava.features.FeatureActivationStrategy; import com.hubspot.jinjava.interpret.InvalidArgumentException; import com.hubspot.jinjava.interpret.InvalidReason; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.fn.Functions; import com.hubspot.jinjava.objects.date.PyishDate; @@ -31,9 +35,9 @@ desc = "Datetime object or timestamp at the end of the period", required = true ), - @JinjavaParam(value = "unit", desc = "Which temporal unit to use", required = true) + @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 { @@ -52,8 +56,8 @@ public Object filter( ); } - ZonedDateTime start = getZonedDateTime(var); - ZonedDateTime end = getZonedDateTime(args[0]); + ZonedDateTime start = getZonedDateTime(var, "begin"); + ZonedDateTime end = getZonedDateTime(args[0], "end"); Object args1 = args[1]; if (args1 == null) { @@ -64,12 +68,35 @@ public Object filter( return temporalUnit.between(start, end); } - private ZonedDateTime getZonedDateTime(Object var) { + private ZonedDateTime getZonedDateTime(Object var, String position) { if (var instanceof ZonedDateTime) { return (ZonedDateTime) var; } else if (var instanceof PyishDate) { return ((PyishDate) var).toDateTime(); } else { + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); + + if (var == null) { + interpreter.addError( + TemplateError.fromMissingFilterArgException( + new InvalidArgumentException( + interpreter, + getName() + " filter called with null " + position, + getName() + ) + ) + ); + + FeatureActivationStrategy feat = interpreter + .getConfig() + .getFeatures() + .getActivationStrategy(BuiltInFeatures.FIXED_DATE_TIME_FILTER_NULL_ARG); + + if (feat.isActive(interpreter.getContext())) { + var = ((DateTimeFeatureActivationStrategy) feat).getActivateAt(); + } + } + return Functions.getDateTimeArg(var, ZoneOffset.UTC); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/BoolFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/BoolFilter.java index e2483dd26..a31ffdaec 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/BoolFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/BoolFilter.java @@ -33,7 +33,7 @@ @JinjavaSnippet( desc = "This example converts a text string value to a boolean", code = "{% if \"true\"|bool == true %}hello world{% endif %}" - ) + ), } ) public class BoolFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/CapitalizeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/CapitalizeFilter.java index 2ad6b2c9c..692aca07c 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/CapitalizeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/CapitalizeFilter.java @@ -17,7 +17,7 @@ @JinjavaSnippet( code = "{% set sentence = \"the first letter of a sentence should always be capitalized.\" %}\n" + "{{ sentence|capitalize }}" - ) + ), } ) public class CapitalizeFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/CenterFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/CenterFilter.java index 5ba86fdbd..4c1f90c98 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/CenterFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/CenterFilter.java @@ -16,7 +16,7 @@ type = "number", defaultValue = "80", desc = "Width of field to center value in" - ) + ), }, snippets = { @JinjavaSnippet( @@ -25,7 +25,7 @@ " {% set var = \"string to center\" %}\n" + " {{ var|center(80) }}\n" + "" - ) + ), } ) public class CenterFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/CloseHtmlFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/CloseHtmlFilter.java new file mode 100644 index 000000000..f15315bd8 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/CloseHtmlFilter.java @@ -0,0 +1,26 @@ +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.jsoup.Jsoup; + +@JinjavaDoc( + value = "Closes open HTML tags in a string", + input = @JinjavaParam(value = "s", desc = "String to close", required = true), + snippets = { @JinjavaSnippet(code = "{{ \"

Hello, world\"|closehtml }}") } +) +public class CloseHtmlFilter implements Filter { + + @Override + public String getName() { + return "closehtml"; + } + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + return Jsoup.parseBodyFragment(Objects.toString(var)).body().html(); + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/CutFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/CutFilter.java index 04a4c8138..d8f64843f 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/CutFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/CutFilter.java @@ -31,12 +31,12 @@ value = "to_remove", desc = "String to remove from the original string", required = true - ) + ), }, snippets = { @JinjavaSnippet( code = "{% set my_string = \"Hello world.\" %}\n" + "{{ my_string|cut(' world') }}" - ) + ), } ) public class CutFilter implements Filter { 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..1098c5da4 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilter.java @@ -7,11 +7,14 @@ 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( value = "value", - defaultValue = "current time", desc = "The date variable or UNIX timestamp to format", required = true ), @@ -31,14 +34,15 @@ type = "string", defaultValue = "us", desc = "The language code to use when formatting the datetime" - ) + ), }, snippets = { @JinjavaSnippet(code = "{% content.updated|datetimeformat('%B %e, %Y') %}"), @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 { @@ -49,10 +53,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/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/DefaultFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/DefaultFilter.java index 00766619b..d93170d6a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/DefaultFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/DefaultFilter.java @@ -44,7 +44,7 @@ type = "boolean", defaultValue = "False", desc = "Set to True to use with variables which evaluate to false" - ) + ), }, snippets = { @JinjavaSnippet( @@ -54,10 +54,11 @@ @JinjavaSnippet( desc = "If you want to use default with variables that evaluate to false you have to set the second parameter to true", code = "{{ ''|default('the string was empty', true) }}" - ) + ), } ) public class DefaultFilter extends AbstractFilter implements AdvancedFilter { + public static final String DEFAULT_VALUE_PARAM = "default_value"; public static final String TRUTHY_PARAM = "truthy"; @@ -86,7 +87,7 @@ public Object filter( return object; } - return defaultValue instanceof PyWrapper + return (defaultValue instanceof PyWrapper) || (defaultValue == null) ? defaultValue : Objects.toString(defaultValue); } 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..3f3f3736f 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/DictSortFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/DictSortFilter.java @@ -27,7 +27,7 @@ type = "enum key|value", defaultValue = "key", desc = "Sort by dict key or value" - ) + ), }, snippets = { @JinjavaSnippet( @@ -35,7 +35,7 @@ code = "{% for item in contact|dictsort(false, 'value') %}\n" + " {{item}}\n" + "{% endfor %}" - ) + ), } ) public class DictSortFilter implements Filter { @@ -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") @@ -72,6 +72,7 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) private static class MapEntryComparator implements Comparator>, Serializable { + private static final long serialVersionUID = 1L; private final boolean caseSensitive; @@ -87,6 +88,9 @@ private static class MapEntryComparator public int compare(Entry o1, Entry o2) { Object sVal1 = sortByKey ? o1.getKey() : o1.getValue(); Object sVal2 = sortByKey ? o2.getKey() : o2.getValue(); + if (sVal1 == null || sVal2 == null) { + return 0; + } int result = 0; 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..2cc71f83d 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,8 @@ 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( value = "Returns a list containing elements present in the first list but not the second list", @@ -22,22 +21,15 @@ type = "sequence", desc = "The second list", required = true - ) + ), }, snippets = { @JinjavaSnippet(code = "{{ [1, 2, 3]|difference([2, 3, 4]) }}") } ) public class DifferenceFilter extends AbstractSetFilter { @Override - public Object filter( - Object var, - JinjavaInterpreter interpreter, - Object[] args, - Map kwargs - ) { - return new ArrayList<>( - Sets.difference(objectToSet(var), objectToSet(parseArgs(interpreter, args))) - ); + public Object filter(Set varSet, Set argSet) { + return new ArrayList<>(Sets.difference(varSet, argSet)); } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/DivideFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/DivideFilter.java index 12ad0e752..b950a0c76 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/DivideFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/DivideFilter.java @@ -41,13 +41,14 @@ type = "number", desc = "The divisor to divide the value", required = true - ) + ), }, snippets = { - @JinjavaSnippet(code = "{% set numerator = 106 %}\n" + "{% numerator|divide(2) %}") + @JinjavaSnippet(code = "{% set numerator = 106 %}\n" + "{% numerator|divide(2) %}"), } ) public class DivideFilter implements AdvancedFilter { + private static final TruthyTypeConverter TYPE_CONVERTER = new TruthyTypeConverter(); @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/DivisibleFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/DivisibleFilter.java index 8e4f12346..c7471cc4c 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/DivisibleFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/DivisibleFilter.java @@ -35,7 +35,7 @@ type = "number", desc = "The divisor to check if the value is divisible by", required = true - ) + ), }, snippets = { @JinjavaSnippet( @@ -44,7 +44,7 @@ "{% if num|divisible(2) %}\n" + " The number is divisble by 2\n" + "{% endif %}" - ) + ), } ) public class DivisibleFilter implements Filter { 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..b48891a0b 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/EscapeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/EscapeFilter.java @@ -31,10 +31,11 @@ @JinjavaSnippet( code = "{% set escape_string = \"
This markup is printed as text
\" %}\n" + "{{ escape_string|escape }}" - ) + ), } ) public class EscapeFilter implements Filter { + private static final String SAMP = "&"; private static final String BAMP = "&"; private static final String SGT = ">"; @@ -48,6 +49,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/main/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java index d94c1e804..57823de6d 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilter.java @@ -27,14 +27,23 @@ "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" + "{{ escape_string|escape_jinjava }}" - ) + ), } ) public class EscapeJinjavaFilter implements Filter { + private static final String SLBRACE = "{"; private static final String BLBRACE = "{"; private static final String SRBRACE = "}"; @@ -47,8 +56,19 @@ 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) + .replace("}}", BRBRACE + BRBRACE) + .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/main/java/com/hubspot/jinjava/lib/filter/EscapeJsFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJsFilter.java index 540f4bca6..0b41d5421 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJsFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/EscapeJsFilter.java @@ -30,7 +30,7 @@ @JinjavaSnippet( code = "{% set escape_string = \"This string can safely be inserted into JavaScript\" %}\n" + "{{ escape_string|escapejs }}" - ) + ), } ) public class EscapeJsFilter implements Filter { 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..0653c0891 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilter.java @@ -21,10 +21,10 @@ type = "boolean", defaultValue = "False", desc = "Use binary prefixes (Mebi, Gibi)" - ) + ), }, snippets = { - @JinjavaSnippet(code = "{% set bytes = 100000 %}\n" + "{{ bytes|filesizeformat }}") + @JinjavaSnippet(code = "{% set bytes = 100000 %}\n" + "{{ bytes|filesizeformat }}"), } ) public class FileSizeFormatFilter implements Filter { @@ -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 = { @@ -76,7 +76,7 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) "PiB", "EiB", "ZiB", - "YiB" + "YiB", }; private static final String[] DECIMAL_SIZES = { "KB", @@ -86,6 +86,6 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) "PB", "EB", "ZB", - "YB" + "YB", }; } 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..f5dfc30e3 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 { @@ -27,95 +30,101 @@ public FilterLibrary(boolean registerDefaults, Set disabled) { @Override protected void registerDefaults() { registerClasses( + AbsFilter.class, + AddFilter.class, + AllowSnakeCaseFilter.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, + FormatDateFilter.class, + FormatDatetimeFilter.class, + FormatNumberFilter.class, + FormatTimeFilter.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, + UnescapeHtmlFilter.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 ); } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FirstFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/FirstFilter.java index d2d3dde5f..2f9ff5e1a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FirstFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FirstFilter.java @@ -19,7 +19,7 @@ @JinjavaSnippet( code = "{% set my_sequence = ['Item 1', 'Item 2', 'Item 3'] %}\n" + "{{ my_sequence|first }}" - ) + ), } ) public class FirstFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FloatFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/FloatFilter.java index 867f8f134..ffbca6231 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FloatFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FloatFilter.java @@ -22,14 +22,14 @@ type = "float", defaultValue = "0.0", desc = "Value to return if conversion fails" - ) + ), }, snippets = { @JinjavaSnippet( desc = "This example converts a text field string value to a float", code = "{% text \"my_text\" value='25', export_to_template_context=True %}\n" + "{% widget_data.my_text.value|float + 28 %}" - ) + ), } ) public class FloatFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/ForceEscapeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/ForceEscapeFilter.java index fa13305c2..c6acfea55 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/ForceEscapeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/ForceEscapeFilter.java @@ -14,7 +14,7 @@ @JinjavaSnippet( code = "{% set escape_string = \"
This markup is printed as text
\" %}\n" + "{{ escape_string|forceescape }}\n" - ) + ), } ) public class ForceEscapeFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FormatFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/FormatFilter.java index 3051248cb..a22fa6f22 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FormatFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FormatFilter.java @@ -24,16 +24,17 @@ value = "args", type = "String...", desc = "Values to insert into string" - ) + ), }, snippets = { @JinjavaSnippet( desc = "%s can be replaced with other variables or values", code = "{{ \"Hi %s %s\"|format(contact.firstname, contact.lastname) }} " - ) + ), } ) public class FormatFilter implements AdvancedFilter { + public static final String NAME = "format"; @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FormatNumberFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/FormatNumberFilter.java new file mode 100644 index 000000000..7a721b56c --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FormatNumberFilter.java @@ -0,0 +1,115 @@ +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; +import java.util.Optional; + +@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 = "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." + ), + }, + snippets = { + @JinjavaSnippet(code = "{{ number|format_number }}"), + @JinjavaSnippet(code = "{{ number|format_number(\"en-US\") }}"), + @JinjavaSnippet(code = "{{ number|format_number(\"en-US\", 3) }}"), + } +) +public class FormatNumberFilter 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; + } + + Optional maxDecimalPrecision = args.length > 1 + ? Optional.of(Integer.parseInt(args[1])) + : Optional.empty(); + + return formatNumber(locale, number, maxDecimalPrecision); + } + + 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, + Optional maxDecimalPrecision + ) { + NumberFormat numberFormat = NumberFormat.getNumberInstance(locale); + int numDecimalPlacesInInput = Math.max(0, number.scale()); + + numberFormat.setMaximumFractionDigits( + Math.min( + numDecimalPlacesInInput, + maxDecimalPrecision.isPresent() + ? maxDecimalPrecision.get() + : numDecimalPlacesInInput + ) + ); + + return numberFormat.format(number); + } +} 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); } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FromYamlFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/FromYamlFilter.java index 5b1c732a9..7f79e709e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FromYamlFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FromYamlFilter.java @@ -19,6 +19,7 @@ snippets = { @JinjavaSnippet(code = "{{object|fromYaml}}") } ) public class FromYamlFilter implements Filter { + private static final YAMLMapper OBJECT_MAPPER = new YAMLMapper(); @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/GroupByFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/GroupByFilter.java index f425f78d3..d19c7620c 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/GroupByFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/GroupByFilter.java @@ -28,7 +28,7 @@ value = "attribute", desc = "The common attribute to group by", required = true - ) + ), }, snippets = { @JinjavaSnippet( @@ -41,7 +41,7 @@ " {% endfor %}\n" + " {% endfor %}\n" + "" - ) + ), } ) public class GroupByFilter implements Filter { @@ -89,6 +89,7 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) } public static class Group { + private final String grouper; private final Object grouperObject; private final List list; diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/IndentFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/IndentFilter.java index edab3b037..d40745594 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/IndentFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/IndentFilter.java @@ -27,7 +27,7 @@ type = "boolean", defaultValue = "False", desc = "If True, first line will be indented" - ) + ), }, snippets = { @JinjavaSnippet( @@ -36,10 +36,11 @@ " {% set var = \"string to indent\" %}\n" + " {{ var|indent(2, true) }}\n" + "" - ) + ), } ) public class IndentFilter extends AbstractFilter { + public static final String INDENT_FIRST_PARAM = "indentfirst"; public static final String WIDTH_PARAM = "width"; 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..59d2176e9 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/IntFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/IntFilter.java @@ -25,14 +25,14 @@ type = "number", defaultValue = "0", desc = "Value to return if the conversion fails" - ) + ), }, snippets = { @JinjavaSnippet( desc = "This example converts a text field string value to a integer", code = "{% text \"my_text\" value='25', export_to_template_context=True %}\n" + "{% widget_data.my_text.value|int + 28 %}" - ) + ), } ) public class IntFilter implements Filter { @@ -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/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/IntersectFilter.java index b7d1b9601..c08453b29 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,8 @@ 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( value = "Returns a list containing elements present in both lists", @@ -22,22 +21,15 @@ type = "sequence", desc = "The second list", required = true - ) + ), }, snippets = { @JinjavaSnippet(code = "{{ [1, 2, 3]|intersect([2, 3, 4]) }}") } ) public class IntersectFilter extends AbstractSetFilter { @Override - public Object filter( - Object var, - JinjavaInterpreter interpreter, - Object[] args, - Map kwargs - ) { - return new ArrayList<>( - Sets.intersection(objectToSet(var), objectToSet(parseArgs(interpreter, args))) - ); + public Object filter(Set varSet, Set argSet) { + return new ArrayList<>(Sets.intersection(varSet, argSet)); } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/IpAddrFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/IpAddrFilter.java index 4412dc30f..9fa3bb257 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/IpAddrFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/IpAddrFilter.java @@ -35,7 +35,7 @@ type = "string", defaultValue = "prefix", desc = "Name of function. Supported functions: 'prefix', 'netmask', 'network', 'address', 'broadcast'" - ) + ), }, snippets = { @JinjavaSnippet( @@ -49,10 +49,11 @@ desc = "This example shows how to filter list of ip addresses", code = "{{ ['192.108.0.1', null, True, 13, '2000::'] | ipaddr }}", output = "['192.108.0.1', '2000::']" - ) + ), } ) public class IpAddrFilter implements Filter { + private static final Pattern IP4_PATTERN = Pattern.compile( "(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])" ); @@ -193,10 +194,8 @@ private Object getValue( List parts = PREFIX_SPLITTER.splitToList(fullAddress); if ( parts.size() == 1 && - ( - parameter.equalsIgnoreCase(PUBLIC_STRING) || - parameter.equalsIgnoreCase(PRIVATE_STRING) - ) + (parameter.equalsIgnoreCase(PUBLIC_STRING) || + parameter.equalsIgnoreCase(PRIVATE_STRING)) ) { parts = new ArrayList<>(parts); parts.add("0"); diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/Ipv4Filter.java b/src/main/java/com/hubspot/jinjava/lib/filter/Ipv4Filter.java index ddb3f7f2b..361b74fd7 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/Ipv4Filter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/Ipv4Filter.java @@ -19,7 +19,7 @@ defaultValue = "prefix", desc = "Name of function. " + "Supported functions: 'prefix', 'netmask', 'network', 'address', 'broadcast'" - ) + ), }, snippets = { @JinjavaSnippet( @@ -33,7 +33,7 @@ desc = "This example shows how to filter list of ipv4 addresses", code = "{{ ['192.108.0.1', null, True, 13, '2000::'] | ipv4 }}", output = "['192.108.0.']" - ) + ), } ) public class Ipv4Filter extends IpAddrFilter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/Ipv6Filter.java b/src/main/java/com/hubspot/jinjava/lib/filter/Ipv6Filter.java index c0cf68d63..2032f95f9 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/Ipv6Filter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/Ipv6Filter.java @@ -19,7 +19,7 @@ defaultValue = "prefix", desc = "Name of function. " + "Supported functions: 'prefix', 'netmask', 'network', 'address', 'broadcast'" - ) + ), }, snippets = { @JinjavaSnippet( @@ -33,7 +33,7 @@ desc = "This example shows how to filter list of ipv6 addresses", code = "{{ ['192.108.0.1', null, True, 13, '2000::'] | ipv6 }}", output = "['2000::']" - ) + ), } ) public class Ipv6Filter extends IpAddrFilter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/JoinFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/JoinFilter.java index 210c3f5fe..853966e1e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/JoinFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/JoinFilter.java @@ -26,7 +26,7 @@ @JinjavaParam( value = "attr", desc = "Optional dict object attribute to use in joining" - ) + ), }, snippets = { @JinjavaSnippet(code = "{{ [1, 2, 3]|join('|') }}", output = "1|2|3"), @@ -34,7 +34,7 @@ @JinjavaSnippet( desc = "It is also possible to join certain attributes of an object", code = "{{ users|join(', ', attribute='username') }}" - ) + ), } ) public class JoinFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/LastFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/LastFilter.java index f0d22a0f8..51d197289 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/LastFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/LastFilter.java @@ -19,7 +19,7 @@ @JinjavaSnippet( code = "{% set my_sequence = ['Item 1', 'Item 2', 'Item 3'] %}\n" + "{{ my_sequence|last }}" - ) + ), } ) public class LastFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/LengthFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/LengthFilter.java index 445c8523b..28269113e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/LengthFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/LengthFilter.java @@ -35,7 +35,7 @@ @JinjavaSnippet( code = "{% set services = ['Web design', 'SEO', 'Inbound Marketing', 'PPC'] %}\n" + "{{ services|length }}" - ) + ), } ) public class LengthFilter implements Filter { 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..764a3719d 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.", @@ -23,7 +25,7 @@ "{% set three = 3 %}\n" + "{% set list_num = one|list + two|list + three|list %}\n" + "{{ list_num|list }}" - ) + ), } ) public class ListFilter implements Filter { @@ -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/main/java/com/hubspot/jinjava/lib/filter/LogFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/LogFilter.java index 2d889dd86..23c879463 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/LogFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/LogFilter.java @@ -31,6 +31,7 @@ snippets = { @JinjavaSnippet(code = "{{ 25|log(5) }}") } ) public class LogFilter implements Filter { + private static final MathContext PRECISION = new MathContext(50); @Override 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 { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/MapFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/MapFilter.java index b0a1515a7..ec5db716a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/MapFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/MapFilter.java @@ -28,7 +28,7 @@ value = "attribute", desc = "Filter to apply to an object or dict attribute to lookup", required = true - ) + ), }, snippets = { @JinjavaSnippet( @@ -38,10 +38,11 @@ @JinjavaSnippet( desc = "Alternatively you can let it invoke a filter by passing the name of the filter and the arguments afterwards. A good example would be applying a text conversion filter on a sequence", code = "{% set seq = ['item1', 'item2', 'item3'] %}\n" + "{{ seq|map('upper') }}" - ) + ), } ) public class MapFilter implements AdvancedFilter { + private static final String ATTRIBUTE_ARGUMENT = "attribute"; @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/Md5Filter.java b/src/main/java/com/hubspot/jinjava/lib/filter/Md5Filter.java index 1817dfe18..9f1e54e00 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/Md5Filter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/Md5Filter.java @@ -35,6 +35,7 @@ snippets = { @JinjavaSnippet(code = "{{ content.absolute_url|md5 }}") } ) public class Md5Filter implements Filter { + private static final String[] NOSTR = { "0", "1", @@ -51,7 +52,7 @@ public class Md5Filter implements Filter { "c", "d", "e", - "f" + "f", }; private static final String MD5 = "MD5"; diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/MinusTimeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/MinusTimeFilter.java index d5b00f1a2..30b812c40 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/MinusTimeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/MinusTimeFilter.java @@ -32,7 +32,7 @@ desc = "The amount to subtract from the datetime", required = true ), - @JinjavaParam(value = "unit", desc = "Which temporal unit to use", required = true) + @JinjavaParam(value = "unit", desc = "Which temporal unit to use", required = true), }, snippets = { @JinjavaSnippet(code = "{% mydatetime|minus_time(3, 'days') %}") } ) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/MultiplyFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/MultiplyFilter.java index d8cf7fb85..269ecd2d2 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/MultiplyFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/MultiplyFilter.java @@ -41,11 +41,12 @@ type = "number", desc = "The multiplier", required = true - ) + ), }, snippets = { @JinjavaSnippet(code = "{% set n = 20 %}\n" + "{{ n|multiply(3) }}") } ) public class MultiplyFilter implements AdvancedFilter { + private static final TruthyTypeConverter TYPE_CONVERTER = new TruthyTypeConverter(); @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/PlusTimeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/PlusTimeFilter.java index 9cfab8e7a..733e4818f 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/PlusTimeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/PlusTimeFilter.java @@ -32,7 +32,7 @@ desc = "The amount to add to the datetime", required = true ), - @JinjavaParam(value = "unit", desc = "Which temporal unit to use", required = true) + @JinjavaParam(value = "unit", desc = "Which temporal unit to use", required = true), }, snippets = { @JinjavaSnippet(code = "{% mydatetime|plus_time(3, 'days') %}") } ) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/PrettyPrintFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/PrettyPrintFilter.java index 81af674d3..8132aaeae 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/PrettyPrintFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/PrettyPrintFilter.java @@ -1,25 +1,16 @@ package com.hubspot.jinjava.lib.filter; -import static com.hubspot.jinjava.util.Logging.ENGINE_LOG; - +import com.fasterxml.jackson.core.JsonProcessingException; 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.DeferredValueException; +import com.hubspot.jinjava.interpret.InvalidInputException; +import com.hubspot.jinjava.interpret.InvalidReason; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.objects.date.PyishDate; -import java.beans.BeanInfo; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.Objects; -import org.apache.commons.lang3.StringEscapeUtils; -import org.apache.commons.lang3.StringUtils; +import java.util.TreeMap; @JinjavaDoc( value = "Pretty print a variable. Useful for debugging.", @@ -33,7 +24,7 @@ @JinjavaSnippet( code = "{% set this_var =\"Variable that I want to debug\" %}\n" + "{{ this_var|pprint }}" - ) + ), } ) public class PrettyPrintFilter implements Filter { @@ -55,55 +46,27 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) var instanceof String || var instanceof Number || var instanceof PyishDate || - var instanceof Iterable || - var instanceof Map + var instanceof Iterable ) { varStr = Objects.toString(var); + } else if (var instanceof Map) { + TreeMap map = new TreeMap((Map) var); + varStr = Objects.toString(map); } else { - varStr = objPropsToString(var); + try { + varStr = + interpreter + .getConfig() + .getObjectMapper() + .writerWithDefaultPrettyPrinter() + .writeValueAsString(var); + } catch (JsonProcessingException e) { + throw new InvalidInputException(interpreter, this, InvalidReason.JSON_WRITE); + } } - return StringEscapeUtils.escapeHtml4( + return EscapeFilter.escapeHtmlEntities( "{% raw %}(" + var.getClass().getSimpleName() + ": " + varStr + "){% endraw %}" ); } - - private String objPropsToString(Object var) { - List props = new LinkedList<>(); - - try { - BeanInfo beanInfo = Introspector.getBeanInfo(var.getClass()); - - for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) { - try { - if (pd.getPropertyType() != null && pd.getPropertyType().equals(Class.class)) { - continue; - } - - Method readMethod = pd.getReadMethod(); - if ( - readMethod != null && !readMethod.getDeclaringClass().equals(Object.class) - ) { - props.add(pd.getName() + "=" + readMethod.invoke(var)); - } - } catch (Exception e) { - if ( - e instanceof InvocationTargetException && - ( - (InvocationTargetException) e - ).getTargetException() instanceof DeferredValueException - ) { - throw (DeferredValueException) ( - (InvocationTargetException) e - ).getTargetException(); - } - ENGINE_LOG.error("Error reading bean value", e); - } - } - } catch (IntrospectionException e) { - ENGINE_LOG.error("Error inspecting bean", e); - } - - return '{' + StringUtils.join(props, ", ") + '}'; - } } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/RandomFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/RandomFilter.java index 77a4b798c..1a4fef65f 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/RandomFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/RandomFilter.java @@ -39,7 +39,7 @@ code = "{% for content in contents|random %}\n" + "
Post item markup
" + "{% endfor %}" - ) + ), } ) public class RandomFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/RegexReplaceFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/RegexReplaceFilter.java index 63f98e212..387cf78ab 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/RegexReplaceFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/RegexReplaceFilter.java @@ -1,5 +1,7 @@ package com.hubspot.jinjava.lib.filter; +import static com.hubspot.jinjava.lib.filter.ReplaceFilter.checkLength; + import com.google.re2j.Matcher; import com.google.re2j.Pattern; import com.google.re2j.PatternSyntaxException; @@ -31,13 +33,13 @@ value = "new", desc = "The new string that you replace the matched substring", required = true - ) + ), }, snippets = { @JinjavaSnippet( code = "{{ \"It costs $300\"|regex_replace(\"[^a-zA-Z]\", \"\") }}", output = "Itcosts" - ) + ), } ) public class RegexReplaceFilter implements Filter { @@ -71,6 +73,10 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) if (var instanceof String) { String s = (String) var; + // Minor optimization, avoid checking short strings + if (s.length() > 100) { + checkLength(interpreter, s, this); + } String toReplace = args[0]; String replaceWith = args[1]; diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/RejectAttrFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/RejectAttrFilter.java index 664e5d157..c4c780a64 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/RejectAttrFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/RejectAttrFilter.java @@ -26,7 +26,7 @@ type = "name of expression test", defaultValue = "truthy", desc = "Specify which expression test to run for making the rejection" - ) + ), }, snippets = { @JinjavaSnippet( @@ -34,7 +34,7 @@ code = "{% for content in contents|rejectattr('post_list_summary_featured_image') %}\n" + "
Post in listing markup
\n" + "{% endfor %}" - ) + ), } ) public class RejectAttrFilter extends SelectAttrFilter implements AdvancedFilter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/RejectFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/RejectFilter.java index 0807e27d6..d14f6cfd7 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/RejectFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/RejectFilter.java @@ -15,13 +15,13 @@ type = "name of expression test", desc = "Specify which expression test to run for making the selection", required = true - ) + ), }, snippets = { @JinjavaSnippet( code = "{% set some_numbers = [10, 12, 13, 3, 5, 17, 22] %}\n" + "{% some_numbers|reject('even') %}" - ) + ), } ) public class RejectFilter extends SelectFilter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/RenderFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/RenderFilter.java index c257815a3..3cdf41db7 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/RenderFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/RenderFilter.java @@ -3,8 +3,11 @@ 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.el.ext.DeferredInvocationResolutionException; +import com.hubspot.jinjava.el.ext.eager.RenderFlatTempVariable; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.util.Objects; +import org.apache.commons.lang3.math.NumberUtils; @JinjavaDoc( value = "Renders a template string early to be used by other filters and functions", @@ -12,7 +15,7 @@ snippets = { @JinjavaSnippet( code = "{{ \"{% if my_val %} Hello {% else %} world {% endif %}\"|render }}" - ) + ), } ) public class RenderFilter implements Filter { @@ -24,6 +27,26 @@ public String getName() { @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - return interpreter.render(Objects.toString(var)); + int numDeferredTokensStart = interpreter.getContext().getDeferredTokens().size(); + String result; + if (args.length > 0) { + String firstArg = args[0]; + result = + interpreter.renderFlat( + Objects.toString(var), + NumberUtils.toLong(firstArg, interpreter.getConfig().getMaxOutputSize()) + ); + } else { + result = interpreter.renderFlat(Objects.toString(var)); + } + if (interpreter.getContext().getDeferredTokens().size() > numDeferredTokensStart) { + String tempVarName = RenderFlatTempVariable.getVarName(result); + interpreter + .getContext() + .getParent() + .put(tempVarName, new RenderFlatTempVariable(result)); + throw new DeferredInvocationResolutionException(tempVarName); + } + return result; } } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/ReplaceFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/ReplaceFilter.java index 094ee0170..dd0a0927e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/ReplaceFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/ReplaceFilter.java @@ -3,8 +3,11 @@ 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.InvalidInputException; +import com.hubspot.jinjava.interpret.InvalidReason; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateSyntaxException; +import com.hubspot.jinjava.lib.Importable; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; @@ -32,7 +35,7 @@ value = "count", type = "number", desc = "Replace only the first N occurrences" - ) + ), }, snippets = { @JinjavaSnippet( @@ -42,7 +45,7 @@ @JinjavaSnippet( code = "{{ \"aaaaargh\"|replace(\"a\", \"d'oh, \", 2) }}", output = "d'oh, d'oh, aaargh" - ) + ), } ) public class ReplaceFilter implements Filter { @@ -66,6 +69,11 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) } String s = var.toString(); + // Minor optimization, avoid checking short strings + if (s.length() > 100) { + checkLength(interpreter, s, this); + } + String toReplace = args[0]; String replaceWith = args[1]; Integer count = null; @@ -80,4 +88,21 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) return StringUtils.replace(s, toReplace, replaceWith, count); } } + + static void checkLength( + JinjavaInterpreter interpreter, + String s, + Importable importable + ) { + long maxStringLength = interpreter.getConfig().getMaxStringLength(); + if (maxStringLength > 0 && s.length() > maxStringLength) { + throw new InvalidInputException( + interpreter, + importable, + InvalidReason.LENGTH, + s.length(), + maxStringLength + ); + } + } } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/ReverseFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/ReverseFilter.java index cd5e29ba7..53ad2d0a3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/ReverseFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/ReverseFilter.java @@ -19,8 +19,13 @@ 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.ArrayBacked; import java.lang.reflect.Array; +import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; @JinjavaDoc( value = "Reverse the object or return an iterator the iterates over it the other way round.", @@ -36,7 +41,7 @@ "{% for num in nums|reverse %}\n" + " {{ num }}\n" + "{% endfor %}" - ) + ), } ) public class ReverseFilter implements Filter { @@ -48,24 +53,14 @@ public Object filter(Object object, JinjavaInterpreter interpreter, String... ar } // collection if (object instanceof Collection) { - Object[] origin = ((Collection) object).toArray(); - int length = origin.length; - Object[] res = new Object[length]; - length--; - for (int i = 0; i <= length; i++) { - res[i] = origin[length - i]; - } - return res; + return maybeCollectToList( + ReverseArrayIterator.create(((Collection) object).toArray()), + interpreter + ); } // array if (object.getClass().isArray()) { - int length = Array.getLength(object); - Object[] res = new Object[length]; - length--; - for (int i = 0; i <= length; i++) { - res[i] = Array.get(object, length - i); - } - return res; + return maybeCollectToList(ReverseArrayIterator.create(object), interpreter); } // string if (object instanceof String) { @@ -82,8 +77,55 @@ public Object filter(Object object, JinjavaInterpreter interpreter, String... ar return object; } + private Object maybeCollectToList( + ReverseArrayIterator iterator, + JinjavaInterpreter interpreter + ) { + if (interpreter.getConfig().getLegacyOverrides().isIteratorOnlyReverseFilter()) { + return iterator; + } + List result = new ArrayList<>(); + while (iterator.hasNext()) { + result.add(iterator.next()); + } + return result; + } + @Override public String getName() { return "reverse"; } + + static class ReverseArrayIterator implements Iterator, ArrayBacked { + + private final Object array; + private int index; + + static ReverseArrayIterator create(Object array) { + return new ReverseArrayIterator(array); + } + + private ReverseArrayIterator(Object array) { + this.array = array; + index = Array.getLength(array) - 1; + } + + @Override + public Object next() { + if (index < 0) { + throw new NoSuchElementException(); + } + return Array.get(array, index--); + } + + @Override + public boolean hasNext() { + return index >= 0; + } + + @Override + public Object backingArray() { + return array; + } + } } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/RootFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/RootFilter.java index bacfe0c51..e6b718add 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/RootFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/RootFilter.java @@ -30,6 +30,7 @@ snippets = { @JinjavaSnippet(code = "{{ 125|root(3) }}") } ) public class RootFilter implements Filter { + private static final MathContext PRECISION = new MathContext(50); @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/RoundFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/RoundFilter.java index 9f72bd49f..0110a8309 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/RoundFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/RoundFilter.java @@ -30,7 +30,7 @@ type = "enum common|ceil|floor", defaultValue = "common", desc = "Method of rounding: 'common' rounds either up or down, 'ceil' always rounds up, and 'floor' always rounds down." - ) + ), }, snippets = { @JinjavaSnippet( @@ -43,7 +43,7 @@ code = "{{ 42.55|round|int }}", output = "43", desc = "If you need a real integer, pipe it through int" - ) + ), } ) public class RoundFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/SelectAttrFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/SelectAttrFilter.java index a71234e6e..8ee249135 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/SelectAttrFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/SelectAttrFilter.java @@ -35,7 +35,7 @@ type = "name of expression test", defaultValue = "truthy", desc = "Specify which expression test to run for making the selection" - ) + ), }, snippets = { @JinjavaSnippet( @@ -43,7 +43,7 @@ code = "{% for content in contents|selectattr('post_list_summary_featured_image') %}\n" + "
Post in listing markup
\n" + "{% endfor %}" - ) + ), } ) public class SelectAttrFilter implements AdvancedFilter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/SelectFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/SelectFilter.java index da4922922..131b9b1ad 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/SelectFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/SelectFilter.java @@ -29,13 +29,13 @@ type = "name of expression test", defaultValue = "truthy", desc = "Specify which expression test to run for making the selection" - ) + ), }, snippets = { @JinjavaSnippet( code = "{% set some_numbers = [10, 12, 13, 3, 5, 17, 22] %}\n" + "{% some_numbers|select('even') %}" - ) + ), } ) public class SelectFilter implements AdvancedFilter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/ShuffleFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/ShuffleFilter.java index bff1bd2ef..9eaecbf88 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/ShuffleFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/ShuffleFilter.java @@ -24,7 +24,7 @@ code = "{% for content in contents|shuffle %}\n" + "
Markup of each post
\n" + "{% endfor %}" - ) + ), } ) public class ShuffleFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/SliceFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/SliceFilter.java index 7f654b00b..2fe88baae 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/SliceFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/SliceFilter.java @@ -6,6 +6,7 @@ import com.hubspot.jinjava.interpret.InvalidArgumentException; import com.hubspot.jinjava.interpret.InvalidReason; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.util.ForLoop; import com.hubspot.jinjava.util.ObjectIterator; @@ -25,7 +26,9 @@ @JinjavaParam( value = "slices", type = "number", - desc = "Specifies how many items will be sliced", + desc = "Specifies how many items will be sliced. Maximum value is " + + SliceFilter.MAX_SLICES + + ". ", required = true ), @JinjavaParam( @@ -33,7 +36,7 @@ type = "object", desc = "Specifies which object to use to fill missing values on final iteration", required = false - ) + ), }, snippets = { @JinjavaSnippet( @@ -48,11 +51,13 @@ " \n" + " {% endfor %}\n" + "\n" - ) + ), } ) public class SliceFilter implements Filter { + public static final int MAX_SLICES = 1000; + @Override public String getName() { return "slice"; @@ -79,21 +84,45 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) 0, args[0] ); + } else if (slices > MAX_SLICES) { + interpreter.addError( + new TemplateError( + TemplateError.ErrorType.WARNING, + TemplateError.ErrorReason.OVER_LIMIT, + TemplateError.ErrorItem.FILTER, + String.format( + "The value of the 'slices' parameter is greater than %d. It's been reduced to %d", + MAX_SLICES, + MAX_SLICES + ), + null, + interpreter.getLineNumber(), + interpreter.getPosition(), + null + ) + ); + slices = MAX_SLICES; } - List> result = new ArrayList<>(); + List> result = new ArrayList<>(); List currentList = null; + int i = 0; while (loop.hasNext()) { - Object next = loop.next(); if (i % slices == 0) { - currentList = new ArrayList<>(slices); - result.add(currentList); + if (currentList != null) { + result.add(currentList); + } + currentList = new ArrayList<>(); } - currentList.add(next); + currentList.add(loop.next()); i++; } + if (currentList != null && !currentList.isEmpty()) { + result.add(currentList); + } + if (args.length > 1 && currentList != null) { Object fillWith = args[1]; while (currentList.size() < slices) { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/SortFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/SortFilter.java index ecdbb37ab..688a158d6 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/SortFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/SortFilter.java @@ -39,7 +39,7 @@ defaultValue = "False", desc = "Determines whether or not the sorting is case sensitive" ), - @JinjavaParam(value = "attribute", desc = "Specifies an attribute to sort by") + @JinjavaParam(value = "attribute", desc = "Specifies an attribute to sort by"), }, snippets = { @JinjavaSnippet( @@ -51,10 +51,11 @@ "{% for item in my_posts|sort(False, False,'name') %}\n" + " {{ item.name }}
\n" + "{% endfor %}" - ) + ), } ) public class SortFilter implements Filter { + private static final Splitter DOT_SPLITTER = Splitter.on('.').omitEmptyStrings(); private static final Joiner DOT_JOINER = Joiner.on('.'); @@ -118,6 +119,7 @@ private Object mapObject( } private static class ObjectComparator implements Comparator, Serializable { + private final boolean reverse; private final boolean caseSensitive; diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/SplitFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/SplitFilter.java index 2a6b5c58e..064e2a3b9 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/SplitFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/SplitFilter.java @@ -33,7 +33,7 @@ type = "number", defaultValue = "0", desc = "Limits resulting list by putting remainder of string into last list item" - ) + ), }, snippets = { @JinjavaSnippet( @@ -44,7 +44,7 @@ "
  • {{ name }}
  • \n" + " {% endfor %}\n" + "" - ) + ), } ) public class SplitFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/StringFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/StringFilter.java index a1087ee78..622800f5f 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/StringFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/StringFilter.java @@ -16,7 +16,7 @@ snippets = { @JinjavaSnippet( code = "{% set number_to_string = 45 %}\n" + "{{ number_to_string|string }}" - ) + ), } ) public class StringFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/StringToDateFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/StringToDateFilter.java index 28268dab5..1c5b7754b 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/StringToDateFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/StringToDateFilter.java @@ -15,7 +15,7 @@ value = "dateFormat", desc = "Format of the date string", required = true - ) + ), }, snippets = { @JinjavaSnippet(code = "{{ '3/3/21'|strtodate('M/d/yy') }}") } ) diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/StringToTimeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/StringToTimeFilter.java index 30b49fe73..b778cd33e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/StringToTimeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/StringToTimeFilter.java @@ -21,7 +21,7 @@ value = "datetimeFormat", desc = "Format of the datetime string", required = true - ) + ), }, snippets = { @JinjavaSnippet(code = "{% mydatetime|unixtimestamp %}") } ) 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..9d8db5a19 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. @@ -23,10 +23,11 @@ @JinjavaSnippet( code = "{% set some_html = \"
    Some text
    \" %}\n" + "{{ some_html|striptags }}" - ) + ), } ) public class StripTagsFilter implements Filter { + private static final Pattern WHITESPACE = Pattern.compile("\\s{2,}"); @Override @@ -36,20 +37,22 @@ public Object filter(Object object, JinjavaInterpreter interpreter, String... ar } int numDeferredTokensStart = interpreter.getContext().getDeferredTokens().size(); - String val = interpreter.renderFlat((String) object); - if (interpreter.getContext().getDeferredTokens().size() > numDeferredTokensStart) { - throw new DeferredValueException("Deferred in StripTagsFilter"); - } + try (JinjavaInterpreter.InterpreterScopeClosable c = interpreter.enterScope()) { + String val = interpreter.renderFlat((String) object); + if (interpreter.getContext().getDeferredTokens().size() > numDeferredTokensStart) { + throw new DeferredValueException("Deferred in StripTagsFilter"); + } - String cleanedVal = Jsoup.parse(val).text(); - cleanedVal = Jsoup.clean(cleanedVal, Whitelist.none()); + String cleanedVal = Jsoup.parse(val).text(); + cleanedVal = Jsoup.clean(cleanedVal, Safelist.none()); - // backwards compatibility with Jsoup.parse - cleanedVal = cleanedVal.replaceAll(" ", " "); + // backwards compatibility with Jsoup.parse + cleanedVal = cleanedVal.replaceAll(" ", " "); - String normalizedVal = WHITESPACE.matcher(cleanedVal).replaceAll(" "); + String normalizedVal = WHITESPACE.matcher(cleanedVal).replaceAll(" "); - return normalizedVal; + return normalizedVal; + } } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/SumFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/SumFilter.java index 43a1c0153..47962a2d1 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/SumFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/SumFilter.java @@ -28,7 +28,7 @@ @JinjavaParam( value = "attribute", desc = "Specify an optional attribute of dict to sum" - ) + ), }, snippets = { @JinjavaSnippet( @@ -37,7 +37,7 @@ @JinjavaSnippet( desc = "Sum up only certain attributes", code = "Total: {{ items|sum(attribute='price') }}" - ) + ), } ) public class SumFilter implements AdvancedFilter { 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..a329ede86 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,8 @@ 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( value = "Returns a list containing elements present in only one list.", @@ -22,25 +21,15 @@ type = "sequence", desc = "The second list", required = true - ) + ), }, snippets = { @JinjavaSnippet(code = "{{ [1, 2, 3]|symmetric_difference([2, 3, 4]) }}") } ) public class SymmetricDifferenceFilter extends AbstractSetFilter { @Override - public Object filter( - Object var, - JinjavaInterpreter interpreter, - Object[] args, - Map kwargs - ) { - return new ArrayList<>( - Sets.symmetricDifference( - objectToSet(var), - objectToSet(parseArgs(interpreter, args)) - ) - ); + public Object filter(Set varSet, Set argSet) { + return new ArrayList<>(Sets.symmetricDifference(varSet, argSet)); } @Override 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..4bc07c02e 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 (Character.isWhitespace(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/main/java/com/hubspot/jinjava/lib/filter/ToJsonFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/ToJsonFilter.java index acdf6ef91..9b6d45512 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/ToJsonFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/ToJsonFilter.java @@ -1,12 +1,18 @@ package com.hubspot.jinjava.lib.filter; -import com.fasterxml.jackson.core.JsonProcessingException; 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.DeferredValueException; import com.hubspot.jinjava.interpret.InvalidInputException; import com.hubspot.jinjava.interpret.InvalidReason; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.objects.serialization.LengthLimitingWriter; +import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.concurrent.atomic.AtomicInteger; @JinjavaDoc( value = "Writes object as a JSON string", @@ -22,8 +28,21 @@ public class ToJsonFilter implements Filter { @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { try { - return interpreter.getConfig().getObjectMapper().writeValueAsString(var); - } catch (JsonProcessingException e) { + if (interpreter.getConfig().getMaxOutputSize() > 0) { + AtomicInteger remainingLength = new AtomicInteger( + (int) Math.min(Integer.MAX_VALUE, interpreter.getConfig().getMaxOutputSize()) + ); + Writer writer = new LengthLimitingWriter(new CharArrayWriter(), remainingLength); + interpreter.getConfig().getObjectMapper().writeValue(writer, var); + return writer.toString(); + } else { + return interpreter.getConfig().getObjectMapper().writeValueAsString(var); + } + } catch (IOException e) { + if (e.getCause() instanceof DeferredValueException) { + throw (DeferredValueException) e.getCause(); + } + PyishObjectMapper.handleLengthLimitingException(e); throw new InvalidInputException(interpreter, this, InvalidReason.JSON_WRITE); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/ToYamlFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/ToYamlFilter.java index cce29dc19..a78d02876 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/ToYamlFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/ToYamlFilter.java @@ -20,8 +20,9 @@ snippets = { @JinjavaSnippet(code = "{{object|toyaml}}") } ) public class ToYamlFilter implements Filter { + private static final YAMLMapper OBJECT_MAPPER = new YAMLMapper() - .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER); + .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER); @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/TruncateFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/TruncateFilter.java index 9b008855b..1309ccf5d 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/TruncateFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/TruncateFilter.java @@ -48,7 +48,7 @@ value = "end", defaultValue = "...", desc = "The characters that will be added to indicate where the text was truncated" - ) + ), }, snippets = { @JinjavaSnippet( @@ -58,7 +58,7 @@ @JinjavaSnippet( code = "{{ \"I only want to show the first sentence. Not the second.\"|truncate(35, True, '..') }}", output = "I only want to show the first sente.." - ) + ), } ) public class TruncateFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilter.java index f1b3b45c3..defef913e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilter.java @@ -39,16 +39,17 @@ type = "boolean", defaultValue = "false", desc = "If set to true, text will be truncated in the middle of words" - ) + ), }, snippets = { @JinjavaSnippet( code = "{{ \"

    I want to truncate this text without breaking my HTML

    \"|truncatehtml(28, '..', false) }}", output = "

    I want to truncate this text without breaking my HTML

    " - ) + ), } ) public class TruncateHtmlFilter implements AdvancedFilter { + private static final int DEFAULT_TRUNCATE_LENGTH = 255; private static final String DEFAULT_END = "..."; private static final String LENGTH_KEY = "length"; @@ -156,6 +157,7 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) } private static class ContentTruncatingNodeVisitor implements NodeVisitor { + private final int maxTextLen; private int textLen; private final String ending; 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..fa7919469 --- /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/main/java/com/hubspot/jinjava/lib/filter/UnionFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/UnionFilter.java index 73adf565d..eb74124ef 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,8 @@ 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( value = "Returns a list containing elements present in either list", @@ -22,22 +21,15 @@ type = "sequence", desc = "The second list", required = true - ) + ), }, snippets = { @JinjavaSnippet(code = "{{ [1, 2, 3]|union([2, 3, 4]) }}") } ) public class UnionFilter extends AbstractSetFilter { @Override - public Object filter( - Object var, - JinjavaInterpreter interpreter, - Object[] args, - Map kwargs - ) { - return new ArrayList<>( - Sets.union(objectToSet(var), objectToSet(parseArgs(interpreter, args))) - ); + public Object filter(Set varSet, Set argSet) { + return new ArrayList<>(Sets.union(varSet, argSet)); } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/UniqueFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/UniqueFilter.java index 19439978c..908ece3dc 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/UniqueFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/UniqueFilter.java @@ -21,7 +21,7 @@ @JinjavaParam( value = "attr", type = "Optional attribute on object to use as unique identifier" - ) + ), }, snippets = { @JinjavaSnippet( @@ -32,7 +32,7 @@ @JinjavaSnippet( desc = "Filter out duplicate blog posts", code = "{% for content in contents|unique(attr='slug') %}\n" + "\n{% endfor %}" - ) + ), } ) public class UniqueFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilter.java index 332277671..e1fe8f298 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilter.java @@ -3,17 +3,17 @@ 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.features.BuiltInFeatures; +import com.hubspot.jinjava.features.DateTimeFeatureActivationStrategy; +import com.hubspot.jinjava.features.FeatureActivationStrategy; +import com.hubspot.jinjava.interpret.InvalidArgumentException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.lib.fn.Functions; @JinjavaDoc( value = "Gets the UNIX timestamp value (in milliseconds) of a date object", - input = @JinjavaParam( - value = "value", - defaultValue = "current time", - desc = "The date variable", - required = true - ), + input = @JinjavaParam(value = "value", desc = "The date variable", required = true), snippets = { @JinjavaSnippet(code = "{% mydatetime|unixtimestamp %}") } ) public class UnixTimestampFilter implements Filter { @@ -25,6 +25,27 @@ public String getName() { @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + if (var == null) { + interpreter.addError( + TemplateError.fromMissingFilterArgException( + new InvalidArgumentException( + interpreter, + "unixtimestamp", + "unixtimestamp filter called with null datetime" + ) + ) + ); + + FeatureActivationStrategy feat = interpreter + .getConfig() + .getFeatures() + .getActivationStrategy(BuiltInFeatures.FIXED_DATE_TIME_FILTER_NULL_ARG); + + if (feat.isActive(interpreter.getContext())) { + var = ((DateTimeFeatureActivationStrategy) feat).getActivateAt(); + } + } + return Functions.unixtimestamp(var); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/UrlDecodeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/UrlDecodeFilter.java index abf34e244..af1f279d5 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/UrlDecodeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/UrlDecodeFilter.java @@ -24,7 +24,7 @@ @JinjavaSnippet( code = "{{ \"http%3A%2F%2Ffoo.com%3Fbar%26food\"|urldecode }}", output = "http://foo.com?bar&food" - ) + ), } ) public class UrlDecodeFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/UrlEncodeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/UrlEncodeFilter.java index 1f0b2a357..e4ce1a1bc 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/UrlEncodeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/UrlEncodeFilter.java @@ -21,7 +21,7 @@ required = true ), snippets = { - @JinjavaSnippet(code = "{{ \"Escape & URL encode this string\"|urlencode }}") + @JinjavaSnippet(code = "{{ \"Escape & URL encode this string\"|urlencode }}"), } ) public class UrlEncodeFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/UrlizeFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/UrlizeFilter.java index 3b5c3bda4..fa2a339ac 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/UrlizeFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/UrlizeFilter.java @@ -31,7 +31,7 @@ defaultValue = "False", desc = "Adds nofollow to generated link tag" ), - @JinjavaParam(value = "target", desc = "Adds target attr to generated link tag") + @JinjavaParam(value = "target", desc = "Adds target attr to generated link tag"), }, snippets = { @JinjavaSnippet( @@ -41,7 +41,7 @@ @JinjavaSnippet( desc = "If target is specified, the target attribute will be added to the tag", code = "{{ \"http://www.hubspot.com\"|urlize(10, true, target='_blank') }}" - ) + ), } ) public class UrlizeFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/WordCountFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/WordCountFilter.java index 428a17ae4..9c504185f 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/WordCountFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/WordCountFilter.java @@ -20,7 +20,7 @@ @JinjavaSnippet( code = "{% set count_words = \"Count the number of words in this variable\" %}\n" + "{{ count_words|wordcount }}" - ) + ), } ) public class WordCountFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/WordWrapFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/WordWrapFilter.java index ea96d082d..47f3f9483 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/WordWrapFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/WordWrapFilter.java @@ -29,7 +29,7 @@ type = "boolean", defaultValue = "True", desc = "If true, long words will be broken when wrapped" - ) + ), }, snippets = { @JinjavaSnippet( @@ -37,7 +37,7 @@ code = "
    \n" +
           "    {{ \"Lorem ipsum dolor sit amet, consectetur adipiscing elit\"|wordwrap(10) }}\n" +
           "
    " - ) + ), } ) public class WordWrapFilter implements Filter { diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/XmlAttrFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/XmlAttrFilter.java index d09f41051..7c25766b1 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/XmlAttrFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/XmlAttrFilter.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.regex.Pattern; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; @@ -26,17 +27,22 @@ type = "boolean", defaultValue = "True", desc = "Automatically prepend a space in front of the item" - ) + ), }, snippets = { @JinjavaSnippet( code = "{% set html_attributes = {'class': 'bold', 'id': 'sidebar'} %}\n" + "
    " - ) + ), } ) public class XmlAttrFilter implements Filter { + // See https://html.spec.whatwg.org/#attribute-name-state Don't allow characters that would change the attribute name/value state + private static final Pattern ILLEGAL_ATTRIBUTE_KEY_PATTERN = Pattern.compile( + "[\\s/>=]" + ); + @Override public String getName() { return "xmlattr"; @@ -53,6 +59,11 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) List attrs = new ArrayList<>(); for (Map.Entry entry : dict.entrySet()) { + if (ILLEGAL_ATTRIBUTE_KEY_PATTERN.matcher(entry.getKey()).find()) { + throw new IllegalArgumentException( + String.format("Invalid character in attribute name: %s", entry.getKey()) + ); + } attrs.add( new StringBuilder(entry.getKey()) .append("=\"") 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..4fa1786db --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/time/DateTimeFormatHelper.java @@ -0,0 +1,128 @@ +package com.hubspot.jinjava.lib.filter.time; + +import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.features.BuiltInFeatures; +import com.hubspot.jinjava.features.DateTimeFeatureActivationStrategy; +import com.hubspot.jinjava.features.FeatureActivationStrategy; +import com.hubspot.jinjava.interpret.InvalidArgumentException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; +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; + +public final class DateTimeFormatHelper { + + public static final String FIXED_DATE_TIME_FILTER_NULL_ARG = + BuiltInFeatures.FIXED_DATE_TIME_FILTER_NULL_ARG; + private final String name; + private final Function cannedFormatterFunction; + + DateTimeFormatHelper( + String name, + Function cannedFormatterFunction + ) { + this.name = name; + this.cannedFormatterFunction = cannedFormatterFunction; + } + + 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(() -> + JinjavaInterpreter + .getCurrentMaybe() + .map(JinjavaInterpreter::getConfig) + .map(JinjavaConfig::getLocale) + .orElse(Locale.ENGLISH) + ); + + 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); + } + } + } + + public Object checkForNullVar(Object var, String name) { + if (var != null) { + return var; + } + + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); + + interpreter.addError( + TemplateError.fromMissingFilterArgException( + new InvalidArgumentException( + interpreter, + name, + name + " filter called with null datetime" + ) + ) + ); + + FeatureActivationStrategy feat = interpreter + .getConfig() + .getFeatures() + .getActivationStrategy(BuiltInFeatures.FIXED_DATE_TIME_FILTER_NULL_ARG); + + return feat.isActive(interpreter.getContext()) + ? ((DateTimeFeatureActivationStrategy) feat).getActivateAt() + : null; + } +} 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..5b3908b59 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilter.java @@ -0,0 +1,63 @@ +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 format(var, args); + } + + public static Object format(Object var, String... args) { + return HELPER.format(HELPER.checkForNullVar(var, NAME), 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..5de652fac --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilter.java @@ -0,0 +1,65 @@ +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 format(var, args); + } + + public static Object format(Object var, String... args) { + return HELPER.format(HELPER.checkForNullVar(var, NAME), 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..6133c457d --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilter.java @@ -0,0 +1,63 @@ +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 format(var, args); + } + + public static Object format(Object var, String... args) { + return HELPER.format(HELPER.checkForNullVar(var, NAME), args); + } + + @Override + public String getName() { + return NAME; + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/ELFunctionDefinition.java b/src/main/java/com/hubspot/jinjava/lib/fn/ELFunctionDefinition.java index 6f1f10ef6..a11b0d833 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/ELFunctionDefinition.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/ELFunctionDefinition.java @@ -5,6 +5,7 @@ import java.lang.reflect.Method; public class ELFunctionDefinition implements Importable { + private String namespace; private String localName; private Method method; 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/main/java/com/hubspot/jinjava/lib/fn/Functions.java b/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java index ef7eb835e..3bf154bfe 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java @@ -6,6 +6,9 @@ import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; import com.hubspot.jinjava.el.ext.NamedParameter; +import com.hubspot.jinjava.features.BuiltInFeatures; +import com.hubspot.jinjava.features.DateTimeFeatureActivationStrategy; +import com.hubspot.jinjava.features.FeatureActivationStrategy; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.InvalidArgumentException; @@ -13,6 +16,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; @@ -23,7 +27,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; @@ -39,6 +42,7 @@ import org.apache.commons.lang3.math.NumberUtils; public class Functions { + public static final String STRING_TO_TIME_FUNCTION = "stringToTime"; public static final String STRING_TO_DATE_FUNCTION = "stringToDate"; @@ -54,7 +58,7 @@ public class Functions { " ...\n" + " {{ super() }}\n" + "{% endblock %}" - ) + ), } ) public static String renderSuperBlock() { @@ -87,12 +91,12 @@ public static String renderSuperBlock() { value = "kwargs", type = "NamedParameter...", desc = "Keyword arguments to put into the namespace dictionary" - ) + ), }, snippets = { @JinjavaSnippet(code = "{% set ns = namespace() %}"), @JinjavaSnippet(code = "{% set ns = namespace(b=false) %}"), - @JinjavaSnippet(code = "{% set ns = namespace(my_map, b=false) %}") + @JinjavaSnippet(code = "{% set ns = namespace(my_map, b=false) %}"), } ) public static Namespace createNamespace(Object... parameters) { @@ -114,8 +118,8 @@ public static Namespace createNamespace(Object... parameters) { namespace.putAll( Arrays .stream(parameters) - .filter( - p -> p instanceof NamedParameter && ((NamedParameter) p).getValue() != null + .filter(p -> + p instanceof NamedParameter && ((NamedParameter) p).getValue() != null ) .map(p -> (NamedParameter) p) .collect(Collectors.toMap(NamedParameter::getName, NamedParameter::getValue)) @@ -131,7 +135,7 @@ public static List immutableListOf(Object... items) { value = "converts a key-value pair into a Map.Entry", params = { @JinjavaParam(value = "key", type = "object"), - @JinjavaParam(value = "value", type = "object") + @JinjavaParam(value = "value", type = "object"), }, hidden = true ) @@ -147,7 +151,7 @@ public static List immutableListOf(Object... items) { type = "string", defaultValue = "utc", desc = "timezone" - ) + ), } ) public static ZonedDateTime today(String... var) { @@ -164,15 +168,20 @@ public static ZonedDateTime today(String... var) { ); } } - - ZonedDateTime dateTime = getDateTimeArg(null, zoneOffset); + long currentMillis = JinjavaInterpreter + .getCurrentMaybe() + .map(JinjavaInterpreter::getConfig) + .map(JinjavaConfig::getDateTimeProvider) + .map(DateTimeProvider::getCurrentTimeMillis) + .orElse(System.currentTimeMillis()); + ZonedDateTime dateTime = getDateTimeArg(currentMillis, zoneOffset); return dateTime.toLocalDate().atStartOfDay(zoneOffset); } @JinjavaDoc( value = "formats a date to a string", params = { - @JinjavaParam(value = "var", type = "date", defaultValue = "current time"), + @JinjavaParam(value = "var", type = "date", required = true), @JinjavaParam( value = "format", defaultValue = StrftimeFormatter.DEFAULT_DATE_FORMAT @@ -181,11 +190,12 @@ public static ZonedDateTime today(String... var) { value = "timezone", defaultValue = "utc", desc = "Time zone of output date" - ) + ), } ) public static String dateTimeFormat(Object var, String... format) { ZoneId zoneOffset = ZoneId.of("UTC"); + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); if (format.length > 1 && format[1] != null) { String timezone = format[1]; @@ -193,7 +203,7 @@ public static String dateTimeFormat(Object var, String... format) { zoneOffset = ZoneId.of(timezone); } catch (DateTimeException e) { throw new InvalidArgumentException( - JinjavaInterpreter.getCurrent(), + interpreter, "datetimeformat", String.format("Invalid timezone: %s", timezone) ); @@ -204,6 +214,27 @@ public static String dateTimeFormat(Object var, String... format) { zoneOffset = ((PyishDate) var).toDateTime().getZone(); } + if (var == null) { + interpreter.addError( + TemplateError.fromMissingFilterArgException( + new InvalidArgumentException( + interpreter, + "datetimeformat", + "datetimeformat filter called with null datetime" + ) + ) + ); + + FeatureActivationStrategy feat = interpreter + .getConfig() + .getFeatures() + .getActivationStrategy(BuiltInFeatures.FIXED_DATE_TIME_FILTER_NULL_ARG); + + if (feat.isActive(interpreter.getContext())) { + var = ((DateTimeFeatureActivationStrategy) feat).getActivateAt(); + } + } + ZonedDateTime d = getDateTimeArg(var, zoneOffset); if (d == null) { return ""; @@ -246,7 +277,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( @@ -271,15 +309,12 @@ public static ZonedDateTime getDateTimeArg(Object var, ZoneId zoneOffset) { @JinjavaDoc( value = "gets the unix timestamp milliseconds value of a datetime", - params = { - @JinjavaParam(value = "var", type = "date", defaultValue = "current time") - } + params = { @JinjavaParam(value = "var", type = "date", required = true) } ) public static long unixtimestamp(Object... var) { - ZonedDateTime d = getDateTimeArg( - var == null || var.length == 0 ? null : var[0], - ZoneOffset.UTC - ); + Object filterVar = var == null || var.length == 0 ? null : var[0]; + + ZonedDateTime d = getDateTimeArg(filterVar, ZoneOffset.UTC); if (d == null) { return 0; @@ -296,7 +331,7 @@ public static long unixtimestamp(Object... var) { value = "var", type = "datetimeFormat", desc = "format of the datetime string" - ) + ), } ) public static PyishDate stringToTime(String datetimeString, String datetimeFormat) { @@ -311,9 +346,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( @@ -343,7 +380,7 @@ public static PyishDate stringToTime(String datetimeString, String datetimeForma value = "dateFormat", type = "string", desc = "format of the date string" - ) + ), } ) public static PyishDate stringToDate(String dateString, String dateFormat) { @@ -358,10 +395,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) ); @@ -413,7 +449,7 @@ public static PyishDate stringToDate(String dateString, String dateFormat) { value = "end", defaultValue = "...", desc = "The characters that will be added to indicate where the text was truncated" - ) + ), } ) public static Object truncate(Object var, Object... arg) { @@ -502,7 +538,7 @@ public static int movePointerToJustBeforeLastWord(int pointer, String s) { params = { @JinjavaParam(value = "start", type = "number", defaultValue = "0"), @JinjavaParam(value = "end", type = "number"), - @JinjavaParam(value = "step", type = "number", defaultValue = "1") + @JinjavaParam(value = "step", type = "number", defaultValue = "1"), } ) public static List range(Object arg1, Object... args) { diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/InjectedContextFunctionProxy.java b/src/main/java/com/hubspot/jinjava/lib/fn/InjectedContextFunctionProxy.java index 37eb9df7b..bc4f86ce0 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/InjectedContextFunctionProxy.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/InjectedContextFunctionProxy.java @@ -14,21 +14,25 @@ public class InjectedContextFunctionProxy { + private static final String GUICE_CLASS_INDICATOR = "$$EnhancerByGuice$$"; + public static ELFunctionDefinition defineProxy( String namespace, String name, Method m, Object injectedInstance ) { + Class injectedInstanceClass = removeGuiceWrapping(injectedInstance.getClass()); try { ClassPool pool = ClassPool.getDefault(); - String ccName = - InjectedContextFunctionProxy.class.getSimpleName() + - "$$" + - namespace + - "$$" + - name; + String ccName = String.format( + "%s$$%s$$%s$$%s", + injectedInstanceClass.getName(), + InjectedContextFunctionProxy.class.getSimpleName(), + namespace, + name + ); Class injectedClass = null; try { @@ -97,4 +101,13 @@ public static ELFunctionDefinition defineProxy( throw new RuntimeException(e); } } + + public static Class removeGuiceWrapping(Class clazz) { + if ( + clazz.getName().contains(GUICE_CLASS_INDICATOR) && clazz.getSuperclass() != null + ) { + clazz = clazz.getSuperclass(); + } + return clazz; + } } 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..8b51cbc47 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.interpret.AutoCloseableSupplier; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl; 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; @@ -24,17 +24,20 @@ * */ public class MacroFunction extends AbstractCallableMethod { - private final List content; - private final boolean caller; + public static final String KWARGS_KEY = "kwargs"; + public static final String VARARGS_KEY = "varargs"; + protected final List content; - private final Context localContextScope; + protected final boolean caller; - private final int definitionLineNumber; + protected final Context localContextScope; - private final int definitionStartPosition; + protected final int definitionLineNumber; - private boolean deferred; + protected final int definitionStartPosition; + + protected boolean deferred; public MacroFunction( List content, @@ -71,55 +74,39 @@ public Object doEvaluate( List varArgs ) { 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 ( - !interpreter.getContext().isPartialMacroEvaluation() && - ( - !interpreter.getContext().getDeferredNodes().isEmpty() || - !interpreter.getContext().getDeferredTokens().isEmpty() - ) - ) { + try ( + InterpreterScopeClosable c = interpreter.enterScope(); + AutoCloseableImpl> importFile = getImportFileWithWrapper( interpreter - .getContext() - .removeDeferredTokens( - ImmutableList.copyOf(interpreter.getContext().getDeferredTokens()) - ); - // If the macro function could not be fully evaluated, throw a DeferredValueException. - throw new DeferredValueException( - getName(), - interpreter.getLineNumber(), - interpreter.getPosition() - ); - } - - return result; - } finally { - importFile.ifPresent(path -> interpreter.getContext().getCurrentPathStack().pop()); + ) + .get() + ) { + return getEvaluationResult(argMap, kwargMap, varArgs, interpreter); } } public Optional getImportFile(JinjavaInterpreter interpreter) { + return getImportFileWithWrapper(interpreter).dangerouslyGetWithoutClosing(); + } + + public AutoCloseableSupplier> getImportFileWithWrapper( + JinjavaInterpreter interpreter + ) { Optional importFile = Optional.ofNullable( (String) localContextScope.get(Context.IMPORT_RESOURCE_PATH_KEY) ); - - // pushWithoutCycleCheck() is used to here so that macros calling macros from the same file will not throw a TagCycleException - importFile.ifPresent( - path -> - interpreter - .getContext() - .getCurrentPathStack() - .pushWithoutCycleCheck( - path, - interpreter.getLineNumber(), - interpreter.getPosition() - ) - ); - return importFile; + if (importFile.isEmpty()) { + return AutoCloseableSupplier.of(Optional.empty()); + } + return interpreter + .getContext() + .getCurrentPathStack() + .closeablePushWithoutCycleCheck( + importFile.get(), + interpreter.getLineNumber(), + interpreter.getPosition() + ) + .map(Optional::of); } public String getEvaluationResult( @@ -150,6 +137,11 @@ public String getEvaluationResult( ); } else { if (!alreadyDeferredInEarlierCall(scopeEntry.getKey(), interpreter)) { + if ( + interpreter.getContext().get(scopeEntry.getKey()) == scopeEntry.getValue() + ) { + continue; // don't override if it's the same object + } interpreter.getContext().put(scopeEntry.getKey(), scopeEntry.getValue()); } } @@ -161,9 +153,9 @@ public String getEvaluationResult( interpreter.getContext().put(argEntry.getKey(), argEntry.getValue()); } // parameter map - interpreter.getContext().put("kwargs", kwargMap); + interpreter.getContext().put(KWARGS_KEY, kwargMap); // varargs list - interpreter.getContext().put("varargs", varArgs); + interpreter.getContext().put(VARARGS_KEY, varArgs); LengthLimitingStringBuilder result = new LengthLimitingStringBuilder( interpreter.getConfig().getMaxOutputSize() @@ -225,6 +217,10 @@ public int hashCode() { ); } + public MacroFunction cloneWithNewName(String name) { + return new MacroFunction(this, name); + } + private boolean alreadyDeferredInEarlierCall( String key, JinjavaInterpreter interpreter @@ -237,17 +233,12 @@ private boolean alreadyDeferredInEarlierCall( return penultimateParent .getDeferredTokens() .stream() - .filter( - deferredToken -> - Objects.equals(importResourcePath, deferredToken.getImportResourcePath()) + .filter(deferredToken -> + Objects.equals(importResourcePath, deferredToken.getImportResourcePath()) ) - .anyMatch( - deferredToken -> - deferredToken.getSetDeferredWords().contains(key) || - deferredToken - .getUsedDeferredWords() - .stream() - .anyMatch(used -> key.equals(used.split("\\.", 2)[0])) + .anyMatch(deferredToken -> + deferredToken.getSetDeferredBases().contains(key) || + deferredToken.getUsedDeferredBases().contains(key) ); } return false; diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/TypeFunction.java b/src/main/java/com/hubspot/jinjava/lib/fn/TypeFunction.java index b5297b3a8..d2a387949 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/TypeFunction.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/TypeFunction.java @@ -16,6 +16,7 @@ value = "Get a string that describes the type of the object, similar to Python's type()" ) public class TypeFunction { + private static Map, String> CLASS_TYPE_TO_NAME = ImmutableMap ., String>builder() .put(AstDict.class, "dict") diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/eager/EagerMacroFunction.java b/src/main/java/com/hubspot/jinjava/lib/fn/eager/EagerMacroFunction.java index d4dfe0b38..18a649794 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/eager/EagerMacroFunction.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/eager/EagerMacroFunction.java @@ -1,8 +1,11 @@ package com.hubspot.jinjava.lib.fn.eager; -import com.google.common.collect.ImmutableMap; -import com.hubspot.jinjava.el.ext.AbstractCallableMethod; +import com.google.common.annotations.Beta; import com.hubspot.jinjava.el.ext.AstMacroFunction; +import com.hubspot.jinjava.el.ext.DeferredInvocationResolutionException; +import com.hubspot.jinjava.el.ext.eager.MacroFunctionTempVariable; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; import com.hubspot.jinjava.interpret.DeferredValue; @@ -11,42 +14,55 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; import com.hubspot.jinjava.lib.fn.MacroFunction; import com.hubspot.jinjava.lib.tag.MacroTag; +import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; +import com.hubspot.jinjava.tree.Node; +import com.hubspot.jinjava.util.EagerContextWatcher; +import com.hubspot.jinjava.util.EagerContextWatcher.EagerChildContextConfig; +import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; import com.hubspot.jinjava.util.EagerReconstructionUtils; +import com.hubspot.jinjava.util.PrefixToPreserveState; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; -public class EagerMacroFunction extends AbstractCallableMethod { - private String fullName; - private MacroFunction macroFunction; - private JinjavaInterpreter interpreter; +@Beta +public class EagerMacroFunction extends MacroFunction { + + private AtomicInteger callCount = new AtomicInteger(); + private AtomicBoolean reconstructing = new AtomicBoolean(); public EagerMacroFunction( - String fullName, - MacroFunction macroFunction, - JinjavaInterpreter interpreter + List content, + String name, + LinkedHashMap argNamesWithDefaults, + boolean caller, + Context localContextScope, + int lineNumber, + int startPosition ) { super( - macroFunction.getName(), - getLinkedHashmap(macroFunction.getArguments(), macroFunction.getDefaults()) + content, + name, + argNamesWithDefaults, + caller, + localContextScope, + lineNumber, + startPosition ); - this.fullName = fullName; - this.macroFunction = macroFunction; - this.interpreter = interpreter; } - private static LinkedHashMap getLinkedHashmap( - List args, - Map defaults - ) { - LinkedHashMap linkedHashMap = new LinkedHashMap<>(); - for (String arg : args) { - linkedHashMap.put(arg, defaults.get(arg)); - } - return linkedHashMap; + EagerMacroFunction(MacroFunction source, String name) { + super(source, name); } public Object doEvaluate( @@ -54,24 +70,130 @@ public Object doEvaluate( Map kwargMap, List varArgs ) { - Optional importFile = macroFunction.getImportFile(interpreter); - try (InterpreterScopeClosable c = interpreter.enterNonStackingScope()) { - interpreter.getContext().setDeferredExecutionMode(true); - return macroFunction.getEvaluationResult(argMap, kwargMap, varArgs, interpreter); - } finally { - importFile.ifPresent(path -> interpreter.getContext().getCurrentPathStack().pop()); + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); + if (reconstructing.get()) { + try ( + InterpreterScopeClosable c = interpreter.enterScope(); + AutoCloseableImpl> importFile = getImportFileWithWrapper( + interpreter + ) + .get() + ) { + EagerExecutionResult result = eagerEvaluateInDeferredExecutionMode( + () -> getEvaluationResultDirectly(argMap, kwargMap, varArgs, interpreter), + interpreter + ); + if (!result.getResult().isFullyResolved()) { + interpreter + .getContext() + .removeDeferredTokens(interpreter.getContext().getDeferredTokens()); + result = + eagerEvaluateInDeferredExecutionMode( + () -> getEvaluationResultDirectly(argMap, kwargMap, varArgs, interpreter), + interpreter + ); + } + return result.asTemplateString(); + } } + + int currentCallCount = callCount.getAndIncrement(); + EagerExecutionResult eagerExecutionResult = eagerEvaluate( + () -> super.doEvaluate(argMap, kwargMap, varArgs).toString(), + EagerChildContextConfig + .newBuilder() + .withCheckForContextChanges(!interpreter.getContext().isDeferredExecutionMode()) + .withTakeNewValue(true) + .build(), + interpreter + ); + if ( + !eagerExecutionResult.getResult().isFullyResolved() && + (!interpreter.getContext().isPartialMacroEvaluation() || + !eagerExecutionResult.getSpeculativeBindings().isEmpty() || + interpreter.getContext().isDeferredExecutionMode()) + ) { + PrefixToPreserveState prefixToPreserveState = + EagerReconstructionUtils.resetAndDeferSpeculativeBindings( + interpreter, + eagerExecutionResult + ); + + String tempVarName = MacroFunctionTempVariable.getVarName( + getName(), + hashCode(), + currentCallCount + ); + interpreter + .getContext() + .getParent() + .put( + tempVarName, + new MacroFunctionTempVariable( + prefixToPreserveState + eagerExecutionResult.asTemplateString() + ) + ); + throw new DeferredInvocationResolutionException(tempVarName); + } + if (!eagerExecutionResult.getResult().isFullyResolved()) { + return EagerReconstructionUtils.wrapInChildScope( + eagerExecutionResult.getResult().toString(true), + interpreter + ); + } + return eagerExecutionResult.getResult().toString(true); + } + + private String getEvaluationResultDirectly( + Map argMap, + Map kwargMap, + List varArgs, + JinjavaInterpreter interpreter + ) { + String evaluationResult = getEvaluationResult(argMap, kwargMap, varArgs, interpreter); + interpreter.getContext().getScope().remove(KWARGS_KEY); + interpreter.getContext().getScope().remove(VARARGS_KEY); + return evaluationResult; + } + + private EagerExecutionResult eagerEvaluateInDeferredExecutionMode( + Supplier stringSupplier, + JinjavaInterpreter interpreter + ) { + return eagerEvaluate( + stringSupplier, + EagerChildContextConfig + .newBuilder() + .withForceDeferredExecutionMode(true) + .withTakeNewValue(true) + .withCheckForContextChanges(true) + .build(), + interpreter + ); + } + + private EagerExecutionResult eagerEvaluate( + Supplier stringSupplier, + EagerChildContextConfig eagerChildContextConfig, + JinjavaInterpreter interpreter + ) { + return EagerContextWatcher.executeInChildContext( + eagerInterpreter -> + EagerExpressionResult.fromSupplier(stringSupplier, eagerInterpreter), + interpreter, + eagerChildContextConfig + ); } - public String getStartTag(JinjavaInterpreter interpreter) { + private String getStartTag(String fullName, JinjavaInterpreter interpreter) { StringJoiner argJoiner = new StringJoiner(", "); - for (String arg : macroFunction.getArguments()) { - if (macroFunction.getDefaults().get(arg) != null) { + for (String arg : getArguments()) { + if (getDefaults().get(arg) != null) { argJoiner.add( String.format( "%s=%s", arg, - PyishObjectMapper.getAsPyishString(macroFunction.getDefaults().get(arg)) + PyishObjectMapper.getAsPyishString(getDefaults().get(arg)) ) ); continue; @@ -86,7 +208,7 @@ public String getStartTag(JinjavaInterpreter interpreter) { .toString(); } - public String getEndTag(JinjavaInterpreter interpreter) { + private String getEndTag(JinjavaInterpreter interpreter) { return new StringJoiner(" ") .add(interpreter.getConfig().getTokenScannerSymbols().getExpressionStartWithTag()) .add(String.format("end%s", MacroTag.TAG_NAME)) @@ -94,6 +216,11 @@ public String getEndTag(JinjavaInterpreter interpreter) { .toString(); } + @Override + public String reconstructImage() { + return reconstructImage(getName()); + } + /** * Reconstruct the image of the macro function, @see MacroFunction#reconstructImage() * This image, however, may be partially or fully resolved depending on the @@ -103,13 +230,18 @@ public String getEndTag(JinjavaInterpreter interpreter) { * This image allows for the macro function to be recreated during a later * rendering pass. */ - public String reconstructImage() { + public String reconstructImage(String fullName) { String prefix = ""; + StringBuilder result = new StringBuilder(); + String setTagForAliasedVariables = getSetTagForAliasedVariables(fullName); String suffix = ""; - Optional importFile = macroFunction.getImportFile(interpreter); + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); + + Optional importFile = Optional.ofNullable( + (String) localContextScope.get(Context.IMPORT_RESOURCE_PATH_KEY) + ); Object currentDeferredImportResource = null; if (importFile.isPresent()) { - interpreter.getContext().getCurrentPathStack().pop(); currentDeferredImportResource = interpreter.getContext().get(Context.DEFERRED_IMPORT_RESOURCE_PATH_KEY); if (currentDeferredImportResource instanceof DeferredValue) { @@ -117,94 +249,137 @@ public String reconstructImage() { ((DeferredValue) currentDeferredImportResource).getOriginalValue(); } prefix = - EagerReconstructionUtils.buildSetTag( - ImmutableMap.of( - Context.DEFERRED_IMPORT_RESOURCE_PATH_KEY, - PyishObjectMapper.getAsPyishString(importFile.get()) - ), - interpreter, - false + EagerReconstructionUtils.buildBlockOrInlineSetTag( + Context.DEFERRED_IMPORT_RESOURCE_PATH_KEY, + importFile.get(), + interpreter ); interpreter .getContext() .put(Context.DEFERRED_IMPORT_RESOURCE_PATH_KEY, importFile.get()); suffix = - EagerReconstructionUtils.buildSetTag( - ImmutableMap.of( - Context.DEFERRED_IMPORT_RESOURCE_PATH_KEY, - PyishObjectMapper.getAsPyishString(currentDeferredImportResource) - ), - interpreter, - false + EagerReconstructionUtils.buildBlockOrInlineSetTag( + Context.DEFERRED_IMPORT_RESOURCE_PATH_KEY, + currentDeferredImportResource, + interpreter ); } - String result; if ( - ( - interpreter.getContext().getMacroStack().contains(macroFunction.getName()) && - !differentMacroWithSameNameExists() - ) || - ( - !macroFunction.isCaller() && - AstMacroFunction.checkAndPushMacroStack(interpreter, fullName) - ) + interpreter.getContext().getMacroStack().contains(getName()) && + !differentMacroWithSameNameExists(interpreter) ) { return ""; - } else { + } + + try ( + AutoCloseableImpl shouldReconstruct = shouldDoReconstruction( + fullName, + interpreter + ) + ) { + if (!shouldReconstruct.value()) { + return ""; + } + try (InterpreterScopeClosable c = interpreter.enterScope()) { + reconstructing.set(true); String evaluation = (String) evaluate( - macroFunction - .getArguments() - .stream() - .map(arg -> DeferredMacroValueImpl.instance()) - .toArray() + getArguments().stream().map(arg -> DeferredMacroValueImpl.instance()).toArray() ); - - if (!interpreter.getContext().getDeferredTokens().isEmpty()) { - evaluation = - (String) evaluate( - macroFunction - .getArguments() - .stream() - .map(arg -> DeferredMacroValueImpl.instance()) - .toArray() - ); - } - result = (getStartTag(interpreter) + evaluation + getEndTag(interpreter)); + result + .append(getStartTag(fullName, interpreter)) + .append(setTagForAliasedVariables) + .append(evaluation) + .append(getEndTag(interpreter)); } catch (DeferredValueException e) { // In case something not eager-supported encountered a deferred value - result = macroFunction.reconstructImage(); + if (StringUtils.isNotEmpty(setTagForAliasedVariables)) { + throw new DeferredValueException( + "Aliased variables in not eagerly reconstructible macro function" + ); + } + result.append(super.reconstructImage()); } finally { + reconstructing.set(false); interpreter .getContext() .put(Context.DEFERRED_IMPORT_RESOURCE_PATH_KEY, currentDeferredImportResource); - if (!macroFunction.isCaller()) { - interpreter.getContext().getMacroStack().pop(); - } } } + return prefix + result + suffix; } - private boolean differentMacroWithSameNameExists() { + private AutoCloseableImpl shouldDoReconstruction( + String fullName, + JinjavaInterpreter interpreter + ) { + return ( + isCaller() + ? AutoCloseableSupplier.of(true) + : AstMacroFunction + .checkAndPushMacroStackWithWrapper(interpreter, fullName) + .map(result -> + result.match( + err -> false, // cycle detected, don't do reconstruction + ok -> true // no cycle, proceed with reconstruction + ) + ) + ).get(); + } + + private String getSetTagForAliasedVariables(String fullName) { + int lastDotIdx = fullName.lastIndexOf('.'); + if (lastDotIdx > 0) { + String aliasName = fullName.substring(0, lastDotIdx + 1); + Map namesToAlias = localContextScope + .getCombinedScope() + .entrySet() + .stream() + .filter(entry -> entry.getValue() instanceof DeferredValue) + .map(Entry::getKey) + .collect(Collectors.toMap(Function.identity(), name -> aliasName + name)); + return EagerReconstructionUtils.buildSetTag( + namesToAlias, + JinjavaInterpreter.getCurrent(), + false + ); + } + return ""; + } + + private boolean differentMacroWithSameNameExists(JinjavaInterpreter interpreter) { Context context = interpreter.getContext(); if (context.getParent() == null) { return false; } - MacroFunction mostRecent = context.getGlobalMacro(macroFunction.getName()); - if (macroFunction != mostRecent) { + MacroFunction mostRecent = context.getGlobalMacro(getName()); + if (this != mostRecent) { return true; } while ( - !context.getGlobalMacros().containsKey(macroFunction.getName()) && + !context.getGlobalMacros().containsKey(getName()) && context.getParent().getParent() != null ) { context = context.getParent(); } - MacroFunction secondMostRecent = context - .getParent() - .getGlobalMacro(macroFunction.getName()); - return secondMostRecent != null && secondMostRecent != macroFunction; + MacroFunction secondMostRecent = context.getParent().getGlobalMacro(getName()); + return secondMostRecent != null && secondMostRecent != this; + } + + @Override + public boolean equals(Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public EagerMacroFunction cloneWithNewName(String name) { + return new EagerMacroFunction(this, name); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/AutoEscapeTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/AutoEscapeTag.java index 4f330e206..5804258e8 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/AutoEscapeTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/AutoEscapeTag.java @@ -16,10 +16,11 @@ snippets = { @JinjavaSnippet( code = "{% autoescape %}\n" + "
    Code to escape
    \n" + "{% endautoescape %}" - ) + ), } ) public class AutoEscapeTag implements Tag { + public static final String TAG_NAME = "autoescape"; private static final long serialVersionUID = 786006577642541285L; 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..24cabb2cd 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java @@ -19,6 +19,7 @@ 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; @@ -38,7 +39,7 @@ @JinjavaParam( value = "block_name", desc = "A unique name for the block that should be used in both the parent and child template" - ) + ), }, snippets = { @JinjavaSnippet( @@ -46,11 +47,13 @@ "{% block my_sidebar %}\n" + " \n" + "{% endblock %}" - ) + ), } ) @JinjavaHasCodeBody +@JinjavaTextMateSnippet(code = "{% block ${1:name} %}\n$0\n{% endblock $1 %}") public class BlockTag implements Tag { + public static final String TAG_NAME = "block"; private static final long serialVersionUID = -2362317415797088108L; @@ -67,7 +70,7 @@ public OutputNode interpretOutput(TagNode tagNode, JinjavaInterpreter interprete ); } - String blockName = WhitespaceUtils.unquote(tagData.next()); + String blockName = WhitespaceUtils.unquoteAndUnescape(tagData.next()); interpreter.addBlock( blockName, diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/BreakTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/BreakTag.java new file mode 100644 index 000000000..55a795f81 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/BreakTag.java @@ -0,0 +1,58 @@ +package com.hubspot.jinjava.lib.tag; + +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; +import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet; +import com.hubspot.jinjava.interpret.DeferredValue; +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.NotInLoopException; +import com.hubspot.jinjava.tree.TagNode; +import com.hubspot.jinjava.util.ForLoop; + +/** + * Implements the common loopcontrol `continue`, as in the jinja2.ext.loopcontrols extension + * @author ccutrer + */ + +@JinjavaDoc( + value = "Stops executing the current for loop, including any further iterations" +) +@JinjavaTextMateSnippet( + code = "{% for item in [1, 2, 3, 4] %}{% if item > 2 == 0 %}{% break %}{% endif %}{{ item }}{% endfor %}" +) +public class BreakTag implements Tag { + + public static final String TAG_NAME = "break"; + + @Override + public String getName() { + return TAG_NAME; + } + + @Override + public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { + Object loop = interpreter.getContext().get(ForTag.LOOP); + if (loop instanceof ForLoop) { + if (interpreter.getContext().isDeferredExecutionMode()) { + throw new DeferredValueException("Deferred break"); + } + ForLoop forLoop = (ForLoop) loop; + forLoop.doBreak(); + } else if (loop instanceof DeferredValue) { + throw new DeferredValueException("Deferred break"); + } else { + throw new NotInLoopException(TAG_NAME); + } + return ""; + } + + @Override + public String getEndTagName() { + return null; + } + + @Override + public boolean isRenderedInValidationMode() { + return true; + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/CallTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/CallTag.java index cfcc8d1be..fa4294e87 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/CallTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/CallTag.java @@ -46,13 +46,14 @@ "
    {{ user.description }}
    \n" + " \n" + " {% endcall %}" - ) + ), } ) @JinjavaTextMateSnippet( code = "{% call ${1:macro_name}(${2:argument_names}) %}\n" + "$0\n" + "{% endcall %}" ) public class CallTag implements Tag { + public static final String TAG_NAME = "call"; private static final long serialVersionUID = 7231253469979314727L; @@ -66,7 +67,7 @@ public String getName() { public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { String macroExpr = "{{" + tagNode.getHelpers().trim() + "}}"; - try (InterpreterScopeClosable c = interpreter.enterScope()) { + try (InterpreterScopeClosable c = interpreter.enterNonStackingScope()) { LinkedHashMap args = new LinkedHashMap<>(); MacroFunction caller = new MacroFunction( tagNode.getChildren(), diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/ContinueTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/ContinueTag.java new file mode 100644 index 000000000..4d74aad5f --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/ContinueTag.java @@ -0,0 +1,56 @@ +package com.hubspot.jinjava.lib.tag; + +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; +import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet; +import com.hubspot.jinjava.interpret.DeferredValue; +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.NotInLoopException; +import com.hubspot.jinjava.tree.TagNode; +import com.hubspot.jinjava.util.ForLoop; + +/** + * Implements the common loopcontrol `continue`, as in the jinja2.ext.loopcontrols extension + * @author ccutrer + */ + +@JinjavaDoc(value = "Stops executing the current iteration of the current for loop") +@JinjavaTextMateSnippet( + code = "{% for item in [1, 2, 3, 4] %}{% if item % 2 == 0 %}{% continue %}{% endif %}{{ item }}{% endfor %}" +) +public class ContinueTag implements Tag { + + public static final String TAG_NAME = "continue"; + + @Override + public String getName() { + return TAG_NAME; + } + + @Override + public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { + Object loop = interpreter.getContext().get(ForTag.LOOP); + if (loop instanceof ForLoop) { + if (interpreter.getContext().isDeferredExecutionMode()) { + throw new DeferredValueException("Deferred continue"); + } + ForLoop forLoop = (ForLoop) loop; + forLoop.doContinue(); + } else if (loop instanceof DeferredValue) { + throw new DeferredValueException("Deferred continue"); + } else { + throw new NotInLoopException(TAG_NAME); + } + return ""; + } + + @Override + public String getEndTagName() { + return null; + } + + @Override + public boolean isRenderedInValidationMode() { + return true; + } +} 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..a6f305936 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; @@ -37,7 +38,7 @@ @JinjavaParam( value = "string_to_print", desc = "A comma separated list of strings to print with each interation. The list will repeat if there are more iterations than string parameter values." - ) + ), }, snippets = { @JinjavaSnippet( @@ -45,10 +46,12 @@ code = "{% for content in contents %}\n" + "
    Blog post content
    \n" + "{% endfor %}" - ) + ), } ) +@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"; 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..3fce9a353 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,45 @@ 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/ElseIfTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/ElseIfTag.java index 0f76510b5..bc1063aa0 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/ElseIfTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/ElseIfTag.java @@ -13,12 +13,13 @@ value = "condition", type = "conditional expression", desc = "An expression that evaluates to either true or false" - ) + ), }, hidden = true ) @JinjavaHasCodeBody public class ElseIfTag implements Tag { + public static final String TAG_NAME = "elif"; private static final long serialVersionUID = -7988057025956316803L; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/ElseTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/ElseTag.java index 0e58ef664..bc08314a6 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/ElseTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/ElseTag.java @@ -23,6 +23,7 @@ @JinjavaDoc(value = "", hidden = true) @JinjavaHasCodeBody public class ElseTag implements Tag { + public static final String TAG_NAME = "else"; private static final long serialVersionUID = 1082768429113702148L; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/EndTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/EndTag.java index 223d92eaa..748a25c5e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/EndTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/EndTag.java @@ -6,6 +6,7 @@ @JinjavaDoc(value = "", hidden = true) public final class EndTag implements Tag { + private static final long serialVersionUID = -3309842733119867221L; private final String endTagName; 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..a17bbea02 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; @@ -31,7 +32,7 @@ value = "Template inheritance allows you to build a base “skeleton” template that contains all the " + "common elements of your site and defines blocks that child templates can override.", params = { - @JinjavaParam(value = "path", desc = "Design Manager file path to parent template") + @JinjavaParam(value = "path", desc = "Design Manager file path to parent template"), }, snippets = { @JinjavaSnippet( @@ -75,10 +76,12 @@ " Welcome to my awesome homepage.\n" + "

    \n" + "{% endblock %}" - ) + ), } ) +@JinjavaTextMateSnippet(code = "{% extends '${1:path}' %}") public class ExtendsTag implements Tag { + public static final String TAG_NAME = "extends"; private static final long serialVersionUID = 4692863362280761393L; 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..825a240b8 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java @@ -21,12 +21,12 @@ 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.el.ext.DeferredParsingException; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; +import com.hubspot.jinjava.interpret.NullValue; import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateSyntaxException; @@ -44,7 +44,6 @@ import java.beans.PropertyDescriptor; import java.util.ConcurrentModificationException; import java.util.List; -import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.regex.Matcher; @@ -64,7 +63,7 @@ @JinjavaParam( value = "items_to_iterate", desc = "Specifies the name of a single item in the sequence or dict." - ) + ), }, snippets = { @JinjavaSnippet( @@ -85,18 +84,20 @@ code = "{% for content in contents %}\n" + " Post content variables\n" + "{% endfor %}" - ) + ), } ) @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"; + 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"; @@ -141,15 +142,19 @@ public String interpretUnchecked(TagNode tagNode, JinjavaInterpreter interpreter List loopVars = loopVarsAndExpression.getLeft(); String loopExpression = loopVarsAndExpression.getRight(); - Object collection; - try { - collection = - interpreter.resolveELExpression(loopExpression, tagNode.getLineNumber()); - } catch (DeferredParsingException e) { - throw new DeferredParsingException( - String.format("%s in %s", String.join(", ", loopVars), e.getDeferredEvalResult()) - ); - } + Object collection = interpreter.resolveELExpression( + loopExpression, + tagNode.getLineNumber() + ); + return renderForCollection(tagNode, interpreter, loopVars, collection); + } + + public String renderForCollection( + TagNode tagNode, + JinjavaInterpreter interpreter, + List loopVars, + Object collection + ) { ForLoop loop = ObjectIterator.getLoop(collection); try (InterpreterScopeClosable c = interpreter.enterScope()) { @@ -185,12 +190,31 @@ public String interpretUnchecked(TagNode tagNode, JinjavaInterpreter interpreter // set item variables if (loopVars.size() == 1) { - interpreter.getContext().put(loopVars.get(0), val); + if ( + val == null && + interpreter.getContext().get(loopVars.get(0)) != null && + interpreter.getConfig().getLegacyOverrides().isKeepNullableLoopValues() + ) { + interpreter.getContext().put(loopVars.get(0), NullValue.INSTANCE); + } else { + interpreter.getContext().put(loopVars.get(0), val); + } } else { for (int loopVarIndex = 0; loopVarIndex < loopVars.size(); loopVarIndex++) { String loopVar = loopVars.get(loopVarIndex); - if (Map.Entry.class.isAssignableFrom(val.getClass())) { - Map.Entry entry = (Entry) val; + if (val == null) { + if ( + interpreter.getContext().get(loopVar) != null && + interpreter.getConfig().getLegacyOverrides().isKeepNullableLoopValues() + ) { + interpreter.getContext().put(loopVar, NullValue.INSTANCE); + } else { + interpreter.getContext().put(loopVar, null); + } + continue; + } + if (Entry.class.isAssignableFrom(val.getClass())) { + Entry entry = (Entry) val; Object entryVal = null; if (loopVars.indexOf(loopVar) == 0) { @@ -217,7 +241,7 @@ public String interpretUnchecked(TagNode tagNode, JinjavaInterpreter interpreter if (loopVar.equals(valProp.getName())) { interpreter .getContext() - .put(loopVar, valProp.getReadMethod().invoke(val)); + .put(loopVar, interpreter.resolveProperty(val, valProp.getName())); break; } } @@ -246,15 +270,16 @@ public String interpretUnchecked(TagNode tagNode, JinjavaInterpreter interpreter interpreter.addError(TemplateError.fromOutputTooBigException(e)); return checkLoopVariable(interpreter, buff); } + // continue in the body of the loop; ignore the rest of the body + if (loop.isContinued()) { + break; + } } } if ( interpreter.getConfig().getMaxNumDeferredTokens() < - ( - loop.getLength() * - interpreter.getContext().getDeferredTokens().size() / - loop.getIndex() - ) + ((loop.getLength() * interpreter.getContext().getDeferredTokens().size()) / + loop.getIndex()) ) { throw new DeferredValueException(TOO_LARGE_EXCEPTION_MESSAGE); } @@ -267,7 +292,7 @@ private String checkLoopVariable( JinjavaInterpreter interpreter, LengthLimitingStringBuilder buff ) { - if (interpreter.getContext().get("loop") instanceof DeferredValue) { + if (interpreter.getContext().get(LOOP) instanceof DeferredValue) { throw new DeferredValueException( "loop variable deferred", interpreter.getLineNumber(), diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/FromTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/FromTag.java index 562a02de7..0e2e89691 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/FromTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/FromTag.java @@ -3,16 +3,19 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterators; import com.google.common.collect.PeekingIterator; +import com.hubspot.algebra.Result; 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.AutoCloseableSupplier; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.DeferredValueException; -import com.hubspot.jinjava.interpret.FromTagCycleException; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TagCycleException; import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; @@ -29,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import org.apache.commons.lang3.StringUtils; @JinjavaDoc( value = "Alternative to the import tag that lets you import and use specific macros from one template to another", @@ -37,7 +41,7 @@ @JinjavaParam( value = "macro_name", desc = "Name of macro or comma separated macros to import (import macro_name)" - ) + ), }, snippets = { @JinjavaSnippet( @@ -53,11 +57,12 @@ desc = "The macro html file is accessed from a different template, but only the footer macro is imported and executed", code = "{% from 'custom/page/web_page_basic/my_macros.html' import footer %}\n" + "{{ footer('h2', 'My footer info') }}" - ) + ), } ) @JinjavaTextMateSnippet(code = "{% from '${1:path}' import ${2:macro_name} %}") public class FromTag implements Tag { + public static final String TAG_NAME = "from"; private static final long serialVersionUID = 6152691434172265022L; @@ -71,59 +76,71 @@ public String getName() { public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { List helper = getHelpers((TagToken) tagNode.getMaster()); - Optional maybeTemplateFile = getTemplateFile( - helper, - (TagToken) tagNode.getMaster(), - interpreter - ); - if (!maybeTemplateFile.isPresent()) { - return ""; - } - String templateFile = maybeTemplateFile.get(); - try { - Map imports = getImportMap(helper); + try ( + AutoCloseableImpl> templateFileResult = + getTemplateFileWithWrapper(helper, (TagToken) tagNode.getMaster(), interpreter) + .get() + ) { + return templateFileResult + .value() + .match( + err -> { + String path = StringUtils.trimToEmpty(helper.get(0)); + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.EXCEPTION, + ErrorItem.TAG, + "From cycle detected for path: '" + path + "'", + null, + ((TagToken) tagNode.getMaster()).getLineNumber(), + ((TagToken) tagNode.getMaster()).getStartPosition(), + err, + BasicTemplateErrorCategory.FROM_CYCLE_DETECTED, + ImmutableMap.of("path", path) + ) + ); + return ""; + }, + templateFile -> { + Map imports = getImportMap(helper); - try { - String template = interpreter.getResource(templateFile); - Node node = interpreter.parse(template); + try { + String template = interpreter.getResource(templateFile); + Node node = interpreter.parse(template); - JinjavaInterpreter child = interpreter - .getConfig() - .getInterpreterFactory() - .newInstance(interpreter); - child.getContext().put(Context.IMPORT_RESOURCE_PATH_KEY, templateFile); - JinjavaInterpreter.pushCurrent(child); - try { - child.render(node); - } finally { - JinjavaInterpreter.popCurrent(); - } + JinjavaInterpreter child = interpreter + .getConfig() + .getInterpreterFactory() + .newInstance(interpreter); + child.getContext().put(Context.IMPORT_RESOURCE_PATH_KEY, templateFile); + child.render(node); - interpreter.addAllChildErrors(templateFile, child.getErrorsCopy()); + interpreter.addAllChildErrors(templateFile, child.getErrorsCopy()); - boolean importsDeferredValue = integrateChild(imports, child, interpreter); + boolean importsDeferredValue = integrateChild(imports, child, interpreter); - if (importsDeferredValue) { - handleDeferredNodesDuringImport( - (TagToken) tagNode.getMaster(), - templateFile, - imports, - child, - interpreter - ); - } + if (importsDeferredValue) { + handleDeferredNodesDuringImport( + (TagToken) tagNode.getMaster(), + templateFile, + imports, + child, + interpreter + ); + } - return ""; - } catch (IOException e) { - throw new InterpretException( - e.getMessage(), - e, - tagNode.getLineNumber(), - tagNode.getStartPosition() + return ""; + } catch (IOException e) { + throw new InterpretException( + e.getMessage(), + e, + tagNode.getLineNumber(), + tagNode.getStartPosition() + ); + } + } ); - } - } finally { - interpreter.getContext().popFromStack(); } } @@ -167,7 +184,11 @@ public static boolean integrateChild( Object val = child.getContext().getGlobalMacro(importMapping.getKey()); if (val != null) { - interpreter.getContext().addGlobalMacro((MacroFunction) val); + MacroFunction toImport = (MacroFunction) val; + if (!importMapping.getKey().equals(importMapping.getValue())) { + toImport = toImport.cloneWithNewName(importMapping.getValue()); + } + interpreter.getContext().addGlobalMacro(toImport); } else { val = child.getContext().get(importMapping.getKey()); @@ -203,7 +224,7 @@ public static Map getImportMap(List helper) { return imports; } - public static Optional getTemplateFile( + public static AutoCloseableSupplier> getTemplateFileWithWrapper( List helper, TagToken tagToken, JinjavaInterpreter interpreter @@ -215,32 +236,40 @@ public static Optional getTemplateFile( ); templateFile = interpreter.resolveResourceLocation(templateFile); interpreter.getContext().addDependency("coded_files", templateFile); - try { - interpreter - .getContext() - .pushFromStack( - templateFile, - tagToken.getLineNumber(), - tagToken.getStartPosition() - ); - } catch (FromTagCycleException e) { - interpreter.addError( - new TemplateError( - ErrorType.WARNING, - ErrorReason.EXCEPTION, - ErrorItem.TAG, - "From cycle detected for path: '" + templateFile + "'", - null, - tagToken.getLineNumber(), - tagToken.getStartPosition(), - e, - BasicTemplateErrorCategory.FROM_CYCLE_DETECTED, - ImmutableMap.of("path", templateFile) - ) + return interpreter + .getContext() + .getFromPathStack() + .closeablePush(templateFile, tagToken.getLineNumber(), tagToken.getStartPosition()); + } + + @Deprecated + public static Optional getTemplateFile( + List helper, + TagToken tagToken, + JinjavaInterpreter interpreter + ) { + return getTemplateFileWithWrapper(helper, tagToken, interpreter) + .dangerouslyGetWithoutClosing() + .match( + err -> { + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.EXCEPTION, + ErrorItem.TAG, + "From cycle detected for path: '" + err.getPath() + "'", + null, + tagToken.getLineNumber(), + tagToken.getStartPosition(), + err, + BasicTemplateErrorCategory.FROM_CYCLE_DETECTED, + ImmutableMap.of("path", err.getPath()) + ) + ); + return Optional.empty(); + }, + Optional::of ); - return Optional.empty(); - } - return Optional.of(templateFile); } public static List getHelpers(TagToken tagToken) { 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..85040036d 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/IfTag.java @@ -19,6 +19,7 @@ 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; @@ -37,7 +38,7 @@ value = "condition", type = "conditional expression", desc = "An expression that evaluates to either true or false" - ) + ), }, snippets = { @JinjavaSnippet( @@ -55,11 +56,13 @@ "{% else %}\n" + "Variable named number is greater than 6.\n" + "{% endif %}" - ) + ), } ) +@JinjavaTextMateSnippet(code = "{% if '${1:condition}' %}\n\n{% endif %}") @JinjavaHasCodeBody public class IfTag implements Tag { + public static final String TAG_NAME = "if"; private static final long serialVersionUID = -3784039314941268904L; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/IfchangedTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/IfchangedTag.java index 8025e0a77..b2e1e996f 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/IfchangedTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/IfchangedTag.java @@ -32,11 +32,12 @@ code = "{% ifchanged var %}\n" + "Variable to test if changed\n" + "{% endifchanged %}" - ) + ), } ) @JinjavaHasCodeBody public class IfchangedTag implements Tag { + public static final String TAG_NAME = "ifchanged"; private static final long serialVersionUID = 3567908136629704724L; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/ImportTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/ImportTag.java index 2ffdcedaa..8fbc7a83a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/ImportTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/ImportTag.java @@ -1,16 +1,19 @@ package com.hubspot.jinjava.lib.tag; import com.google.common.collect.ImmutableMap; +import com.hubspot.algebra.Result; 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.AutoCloseableSupplier; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.DeferredValueException; -import com.hubspot.jinjava.interpret.ImportTagCycleException; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TagCycleException; import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; @@ -43,7 +46,7 @@ @JinjavaParam( value = "import_name", desc = "Give a name to the imported file to access macros from" - ) + ), }, snippets = { @JinjavaSnippet( @@ -60,11 +63,12 @@ code = "{% import 'custom/page/web_page_basic/my_macros.html' as header_footer %}\n" + "{{ header_footer.header('h1', 'My page title') }}\n" + "{{ header_footer.footer('h3', 'Company footer info') }}" - ) + ), } ) @JinjavaTextMateSnippet(code = "{% import '${1:path}' ${2: as ${3:import_name}} %}") public class ImportTag implements Tag { + public static final String TAG_NAME = "import"; private static final long serialVersionUID = 8433638845398005260L; @@ -80,64 +84,79 @@ public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { String contextVar = getContextVar(helper); - Optional maybeTemplateFile = getTemplateFile( - helper, - (TagToken) tagNode.getMaster(), - interpreter - ); - if (!maybeTemplateFile.isPresent()) { - return ""; - } - String templateFile = maybeTemplateFile.get(); - try { - Node node = parseTemplateAsNode(interpreter, templateFile); - - JinjavaInterpreter child = interpreter - .getConfig() - .getInterpreterFactory() - .newInstance(interpreter); - child.getContext().put(Context.IMPORT_RESOURCE_PATH_KEY, templateFile); + try ( + AutoCloseableImpl> templateFileResult = + getTemplateFileWithWrapper(helper, (TagToken) tagNode.getMaster(), interpreter) + .get() + ) { + return templateFileResult + .value() + .match( + err -> { + String path = StringUtils.trimToEmpty(helper.get(0)); + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.EXCEPTION, + ErrorItem.TAG, + "Import cycle detected for path: '" + path + "'", + null, + ((TagToken) tagNode.getMaster()).getLineNumber(), + ((TagToken) tagNode.getMaster()).getStartPosition(), + err, + BasicTemplateErrorCategory.IMPORT_CYCLE_DETECTED, + ImmutableMap.of("path", path) + ) + ); + return ""; + }, + templateFile -> { + try ( + AutoCloseableImpl node = parseTemplateAsNode( + interpreter, + templateFile + ) + .get() + ) { + JinjavaInterpreter child = interpreter + .getConfig() + .getInterpreterFactory() + .newInstance(interpreter); + child.getContext().put(Context.IMPORT_RESOURCE_PATH_KEY, templateFile); + child.render(node.value()); - JinjavaInterpreter.pushCurrent(child); + interpreter.addAllChildErrors(templateFile, child.getErrorsCopy()); - try { - child.render(node); - } finally { - JinjavaInterpreter.popCurrent(); - } + Map childBindings = child.getContext().getSessionBindings(); - interpreter.addAllChildErrors(templateFile, child.getErrorsCopy()); + // 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()) { + handleDeferredNodesDuringImport( + node.value(), + contextVar, + childBindings, + child, + interpreter + ); + throw new DeferredValueException( + templateFile, + tagNode.getLineNumber(), + tagNode.getStartPosition() + ); + } - Map childBindings = child.getContext().getSessionBindings(); - - // 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()) { - handleDeferredNodesDuringImport( - node, - contextVar, - childBindings, - child, - interpreter - ); - throw new DeferredValueException( - templateFile, - tagNode.getLineNumber(), - tagNode.getStartPosition() + integrateChild(contextVar, childBindings, child, interpreter); + return ""; + } catch (IOException e) { + throw new InterpretException( + e.getMessage(), + e, + tagNode.getLineNumber(), + tagNode.getStartPosition() + ); + } + } ); - } - - integrateChild(contextVar, childBindings, child, interpreter); - return ""; - } catch (IOException e) { - throw new InterpretException( - e.getMessage(), - e, - tagNode.getLineNumber(), - tagNode.getStartPosition() - ); - } finally { - interpreter.getContext().getCurrentPathStack().pop(); - interpreter.getContext().getImportPathStack().pop(); } } @@ -184,8 +203,7 @@ public static void handleDeferredNodesDuringImport( ) { node .getChildren() - .forEach( - deferredChild -> interpreter.getContext().handleDeferredNode(deferredChild) + .forEach(deferredChild -> interpreter.getContext().handleDeferredNode(deferredChild) ); if (StringUtils.isBlank(contextVar)) { for (MacroFunction macro : child.getContext().getGlobalMacros().values()) { @@ -210,21 +228,19 @@ public static void handleDeferredNodesDuringImport( } } - public static Node parseTemplateAsNode( + public static AutoCloseableSupplier parseTemplateAsNode( JinjavaInterpreter interpreter, String templateFile - ) - throws IOException { - interpreter + ) throws IOException { + String template = interpreter.getResource(templateFile); + return interpreter .getContext() .getCurrentPathStack() - .push(templateFile, interpreter.getLineNumber(), interpreter.getPosition()); - - String template = interpreter.getResource(templateFile); - return interpreter.parse(template); + .closeablePush(templateFile, interpreter.getLineNumber(), interpreter.getPosition()) + .map(currentPath -> interpreter.parse(template)); } - public static Optional getTemplateFile( + public static AutoCloseableSupplier> getTemplateFileWithWrapper( List helper, TagToken tagToken, JinjavaInterpreter interpreter @@ -237,29 +253,41 @@ public static Optional getTemplateFile( ); templateFile = interpreter.resolveResourceLocation(templateFile); interpreter.getContext().addDependency("coded_files", templateFile); - try { - interpreter - .getContext() - .getImportPathStack() - .push(path, tagToken.getLineNumber(), tagToken.getStartPosition()); - } catch (ImportTagCycleException e) { - interpreter.addError( - new TemplateError( - ErrorType.WARNING, - ErrorReason.EXCEPTION, - ErrorItem.TAG, - "Import cycle detected for path: '" + path + "'", - null, - tagToken.getLineNumber(), - tagToken.getStartPosition(), - e, - BasicTemplateErrorCategory.IMPORT_CYCLE_DETECTED, - ImmutableMap.of("path", path) - ) + return interpreter + .getContext() + .getImportPathStack() + .closeablePush(templateFile, tagToken.getLineNumber(), tagToken.getStartPosition()); + } + + @Deprecated + public static Optional getTemplateFile( + List helper, + TagToken tagToken, + JinjavaInterpreter interpreter + ) { + return getTemplateFileWithWrapper(helper, tagToken, interpreter) + .dangerouslyGetWithoutClosing() + .match( + err -> { + String path = StringUtils.trimToEmpty(helper.get(0)); + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.EXCEPTION, + ErrorItem.TAG, + "Import cycle detected for path: '" + path + "'", + null, + tagToken.getLineNumber(), + tagToken.getStartPosition(), + err, + BasicTemplateErrorCategory.IMPORT_CYCLE_DETECTED, + ImmutableMap.of("path", path) + ) + ); + return Optional.empty(); + }, + ok -> Optional.of(ok) ); - return Optional.empty(); - } - return Optional.of(templateFile); } public static String getContextVar(List helper) { 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..cdebc0041 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/IncludeTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/IncludeTag.java @@ -16,12 +16,15 @@ package com.hubspot.jinjava.lib.tag; import com.google.common.collect.ImmutableMap; +import com.hubspot.algebra.Result; 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.IncludeTagCycleException; +import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TagCycleException; import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; @@ -40,21 +43,114 @@ @JinjavaParam( value = "path", desc = "Design Manager path to the file that you would like to include" - ) + ), }, snippets = { @JinjavaSnippet(code = "{% include \"custom/page/web_page_basic/my_footer.html\" %}"), @JinjavaSnippet(code = "{% include \"generated_global_groups/2781996615.html\" %}"), - @JinjavaSnippet(code = "{% include \"hubspot/styles/patches/recommended.css\" %}") + @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"; private static final long serialVersionUID = -8391753639874726854L; @Override public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { + final String finalTemplateFile = resolveTemplateFile(tagNode, interpreter); + final TagNode finalTagNode = tagNode; + try ( + AutoCloseableImpl> includeStackWrapper = + interpreter + .getContext() + .getIncludePathStack() + .closeablePush( + finalTemplateFile, + tagNode.getLineNumber(), + tagNode.getStartPosition() + ) + .get(); + AutoCloseableImpl> currentPathWrapper = + interpreter + .getContext() + .getCurrentPathStack() + .closeablePush( + finalTemplateFile, + interpreter.getLineNumber(), + interpreter.getPosition() + ) + .get() + ) { + return includeStackWrapper + .value() + .match( + err -> { + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.EXCEPTION, + ErrorItem.TAG, + "Include cycle detected for path: '" + finalTemplateFile + "'", + null, + finalTagNode.getLineNumber(), + finalTagNode.getStartPosition(), + err, + BasicTemplateErrorCategory.INCLUDE_CYCLE_DETECTED, + ImmutableMap.of("path", finalTemplateFile) + ) + ); + return ""; + }, + includeStackPath -> + currentPathWrapper + .value() + .match( + err -> { + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.EXCEPTION, + ErrorItem.TAG, + "Include cycle detected for path: '" + finalTemplateFile + "'", + null, + finalTagNode.getLineNumber(), + finalTagNode.getStartPosition(), + err, + BasicTemplateErrorCategory.INCLUDE_CYCLE_DETECTED, + ImmutableMap.of("path", finalTemplateFile) + ) + ); + return ""; + }, + currentPath -> { + try { + String template = interpreter.getResource(finalTemplateFile); + Node node = interpreter.parse(template); + interpreter + .getContext() + .addDependency("coded_files", finalTemplateFile); + return interpreter.render(node, false); + } catch (IOException e) { + throw new InterpretException( + e.getMessage(), + e, + finalTagNode.getLineNumber(), + finalTagNode.getStartPosition() + ); + } + } + ) + ); + } + } + + public static String resolveTemplateFile( + TagNode tagNode, + JinjavaInterpreter interpreter + ) { HelperStringTokenizer helper = new HelperStringTokenizer(tagNode.getHelpers()); if (!helper.hasNext()) { throw new TemplateSyntaxException( @@ -72,52 +168,7 @@ public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { tagNode.getStartPosition() ); templateFile = interpreter.resolveResourceLocation(templateFile); - - try { - interpreter - .getContext() - .getIncludePathStack() - .push(templateFile, tagNode.getLineNumber(), tagNode.getStartPosition()); - } catch (IncludeTagCycleException e) { - interpreter.addError( - new TemplateError( - ErrorType.WARNING, - ErrorReason.EXCEPTION, - ErrorItem.TAG, - "Include cycle detected for path: '" + templateFile + "'", - null, - tagNode.getLineNumber(), - tagNode.getStartPosition(), - e, - BasicTemplateErrorCategory.INCLUDE_CYCLE_DETECTED, - ImmutableMap.of("path", templateFile) - ) - ); - return ""; - } - - try { - String template = interpreter.getResource(templateFile); - Node node = interpreter.parse(template); - - interpreter.getContext().addDependency("coded_files", templateFile); - interpreter - .getContext() - .getCurrentPathStack() - .push(templateFile, interpreter.getLineNumber(), interpreter.getPosition()); - - return interpreter.render(node, false); - } catch (IOException e) { - throw new InterpretException( - e.getMessage(), - e, - tagNode.getLineNumber(), - tagNode.getStartPosition() - ); - } finally { - interpreter.getContext().getIncludePathStack().pop(); - interpreter.getContext().getCurrentPathStack().pop(); - } + return templateFile; } @Override 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..fd52f6c5a 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/MacroTag.java @@ -7,6 +7,7 @@ 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; @@ -29,7 +30,7 @@ @JinjavaParam( value = "argument_names", desc = "Named arguments that are dynamically, when the macro is run" - ) + ), }, snippets = { @JinjavaSnippet( @@ -53,11 +54,13 @@ @JinjavaSnippet( desc = "The macro can then be called like a function. The macro is printed for anchor tags in CSS.", code = "a { {{ trans(\"all .2s ease-in-out\") }} }" - ) + ), } ) @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"; private static final long serialVersionUID = 8397609322126956077L; @@ -135,16 +138,7 @@ public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { .getContext() .put(Context.IMPORT_RESOURCE_PATH_KEY, contextImportResourcePath); } - macro = - new MacroFunction( - tagNode.getChildren(), - name, - argNamesWithDefaults, - false, - interpreter.getContext(), - interpreter.getLineNumber(), - interpreter.getPosition() - ); + macro = constructMacroFunction(tagNode, interpreter, name, argNamesWithDefaults); } finally { if (scopeEntered) { interpreter.leaveScope(); @@ -200,6 +194,23 @@ public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { return ""; } + protected MacroFunction constructMacroFunction( + TagNode tagNode, + JinjavaInterpreter interpreter, + String name, + LinkedHashMap argNamesWithDefaults + ) { + return new MacroFunction( + tagNode.getChildren(), + name, + argNamesWithDefaults, + false, + interpreter.getContext(), + interpreter.getLineNumber(), + interpreter.getPosition() + ); + } + public static boolean populateArgNames( int lineNumber, JinjavaInterpreter interpreter, diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/PrintTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/PrintTag.java index 0beb1b73f..8d1e7ef55 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/PrintTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/PrintTag.java @@ -17,10 +17,11 @@ snippets = { @JinjavaSnippet( code = "{% set string_to_echo = \"Print me\" %}\n" + "{% print string_to_echo %}" - ) + ), } ) public class PrintTag implements Tag { + public static final String TAG_NAME = "print"; private static final long serialVersionUID = -8613906103187594569L; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/RawTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/RawTag.java index 53e943dde..7b06e3e8b 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/RawTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/RawTag.java @@ -15,10 +15,11 @@ code = "{% raw %}\n" + " The personalization token for a contact's first name is {{ contact.firstname }}\n" + "{% endraw %}" - ) + ), } ) public class RawTag implements Tag { + public static final String TAG_NAME = "raw"; private static final long serialVersionUID = -6963360187396753883L; 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..e686b4537 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/SetTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/SetTag.java @@ -22,6 +22,7 @@ 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.TemplateSyntaxException; import com.hubspot.jinjava.objects.Namespace; import com.hubspot.jinjava.tree.Node; @@ -52,7 +53,7 @@ value = "expr", type = "expression", desc = "The value stored in the variable (string, number, boolean, or sequence" - ) + ), }, snippets = { @JinjavaSnippet( @@ -70,12 +71,13 @@ code = "{% set name = 'Jack' %}\n" + "{% set message %}\n" + "My name is {{ name }}\n" + - "{% end_set %}" - ) + "{% endset %}" + ), } ) @JinjavaTextMateSnippet(code = "{% set ${1:var} = ${2:expr} %}") public class SetTag implements Tag, FlexibleTag { + public static final String TAG_NAME = "set"; public static final String IGNORED_VARIABLE_NAME = "__ignored__"; @@ -136,12 +138,10 @@ private String interpretBlockSet(TagNode tagNode, JinjavaInterpreter interpreter if (filterPos >= 0) { var = tagNode.getHelpers().substring(0, filterPos).trim(); } - StringBuilder sb = new StringBuilder(); - for (Node child : tagNode.getChildren()) { - sb.append(child.render(interpreter)); - } + String result; + result = renderChildren(tagNode, interpreter, var); try { - executeSetBlock(tagNode, var, sb.toString(), filterPos >= 0, interpreter); + executeSetBlock(tagNode, var, result, filterPos >= 0, interpreter); } catch (DeferredValueException e) { DeferredValueUtils.deferVariables(new String[] { var }, interpreter.getContext()); throw e; @@ -149,6 +149,32 @@ private String interpretBlockSet(TagNode tagNode, JinjavaInterpreter interpreter return ""; } + public static String renderChildren( + TagNode tagNode, + JinjavaInterpreter interpreter, + String var + ) { + String result; + if (IGNORED_VARIABLE_NAME.equals(var)) { + result = renderChildren(tagNode, interpreter); + } else { + try (InterpreterScopeClosable c = interpreter.enterScope()) { + result = renderChildren(tagNode, interpreter); + } + } + return result; + } + + private static String renderChildren(TagNode tagNode, JinjavaInterpreter interpreter) { + String result; + StringBuilder sb = new StringBuilder(); + for (Node child : tagNode.getChildren()) { + sb.append(child.render(interpreter)); + } + result = sb.toString(); + return result; + } + private void executeSetBlock( TagNode tagNode, String var, @@ -224,7 +250,7 @@ public void executeSet( setVariable( interpreter, varTokens[0], - resolvedList != null ? resolvedList.get(0) : null + resolvedList != null && resolvedList.size() > 0 ? resolvedList.get(0) : null ); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/TagLibrary.java b/src/main/java/com/hubspot/jinjava/lib/tag/TagLibrary.java index 15312cd79..1a2155001 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/TagLibrary.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/TagLibrary.java @@ -29,7 +29,9 @@ protected void registerDefaults() { registerClasses( AutoEscapeTag.class, BlockTag.class, + BreakTag.class, CallTag.class, + ContinueTag.class, CycleTag.class, ElseTag.class, ElseIfTag.class, 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..90afb734c 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/UnlessTag.java @@ -4,6 +4,7 @@ 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; @@ -29,7 +30,9 @@ ) ) @JinjavaHasCodeBody +@JinjavaTextMateSnippet(code = "{% unless ${1:condition} %}\n\t$0\n{% endunless %}") public class UnlessTag extends IfTag { + public static final String TAG_NAME = "unless"; private static final long serialVersionUID = 1562284758153763419L; diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/DeferredToken.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/DeferredToken.java index e85890e8c..f8c5df393 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/DeferredToken.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/DeferredToken.java @@ -1,19 +1,103 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.google.common.annotations.Beta; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; import com.hubspot.jinjava.interpret.CallStack; import com.hubspot.jinjava.interpret.Context; +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.DeferredValueShadow; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.MetaContextVariables; import com.hubspot.jinjava.tree.parse.Token; +import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; +import com.hubspot.jinjava.util.EagerExpressionResolver; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map.Entry; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +@Beta public class DeferredToken { + + public static class DeferredTokenBuilder { + + private final Token token; + private ImmutableSet.Builder usedDeferredWords; + private ImmutableSet.Builder setDeferredWords; + + private DeferredTokenBuilder(Token token) { + this.token = token; + } + + public DeferredToken build() { + return new DeferredToken( + token, + usedDeferredWords != null ? usedDeferredWords.build() : Collections.emptySet(), + setDeferredWords != null ? setDeferredWords.build() : Collections.emptySet(), + acquireImportResourcePath(), + acquireMacroStack() + ); + } + + public DeferredTokenBuilder addUsedDeferredWords( + Collection usedDeferredWordsToAdd + ) { + if (usedDeferredWords == null) { + usedDeferredWords = ImmutableSet.builder(); + } + usedDeferredWords.addAll(usedDeferredWordsToAdd); + return this; + } + + public DeferredTokenBuilder addUsedDeferredWords( + Stream usedDeferredWordsToAdd + ) { + if (usedDeferredWords == null) { + usedDeferredWords = ImmutableSet.builder(); + } + usedDeferredWordsToAdd.forEach(usedDeferredWords::add); + return this; + } + + public DeferredTokenBuilder addSetDeferredWords( + Collection setDeferredWordsToAdd + ) { + if (setDeferredWords == null) { + setDeferredWords = ImmutableSet.builder(); + } + setDeferredWords.addAll(setDeferredWordsToAdd); + return this; + } + + public DeferredTokenBuilder addSetDeferredWords( + Stream setDeferredWordsToAdd + ) { + if (setDeferredWords == null) { + setDeferredWords = ImmutableSet.builder(); + } + setDeferredWordsToAdd.forEach(setDeferredWords::add); + return this; + } + } + private final Token token; // These words aren't yet DeferredValues, but are unresolved // so they should be replaced with DeferredValueImpls if they exist in the context private final Set usedDeferredWords; + private final Set usedDeferredBases; // These words are those which will be set to a value which has been deferred. private final Set setDeferredWords; + private final Set setDeferredBases; // Used to determine the combine scope private final CallStack macroStack; @@ -21,24 +105,145 @@ public class DeferredToken { // Used to determine if in separate file private final String importResourcePath; + /** + * Create a {@link DeferredTokenBuilder} with the provided {@link Token} {@code token} + * @param token A {@link Token} with a deferred image + * @return DeferredTokenBuilder + */ + public static DeferredTokenBuilder builderFromToken(Token token) { + return new DeferredTokenBuilder(token); + } + + /** + * Create a {@link DeferredTokenBuilder} with a {@link Token} constructed using the constructor of {@code tokenClass} using + * the provided {@code image} and line number, position, and symbols taken from the {@code interpreter}. + * @param image The deferred token image + * @param tokenClass Class of {@link Token} to create + * @param interpreter The {@link JinjavaInterpreter} + * @return DeferredTokenBuilder + * @param generic type of the {@tokenClass}, which extends {@link Token} + */ + public static DeferredTokenBuilder builderFromImage( + String image, + Class tokenClass, + JinjavaInterpreter interpreter + ) { + return builderFromToken( + constructToken( + tokenClass, + image, + interpreter.getLineNumber(), + interpreter.getPosition(), + interpreter.getConfig().getTokenScannerSymbols() + ) + ); + } + + /** + * Create a {@link DeferredTokenBuilder} with a {@link Token} constructed using the provided {@code image} + * and line number, position, and symbols taken from the {@code originalToken}. + * @param image The deferred token image + * @param originalToken Original {@link Token} to reference for attributes + * @return DeferredTokenBuilder + */ + public static DeferredTokenBuilder builderFromImage(String image, Token originalToken) { + return builderFromToken( + constructToken( + originalToken.getClass(), + image, + originalToken.getLineNumber(), + originalToken.getStartPosition(), + originalToken.getSymbols() + ) + ); + } + + private static T constructToken( + Class tokenClass, + String image, + int lineNumber, + int startPosition, + TokenScannerSymbols symbols + ) { + try { + return tokenClass + .getDeclaredConstructor( + String.class, + int.class, + int.class, + TokenScannerSymbols.class + ) + .newInstance(image, lineNumber, startPosition, symbols); + } catch ( + InstantiationException + | IllegalAccessException + | InvocationTargetException + | NoSuchMethodException e + ) { + throw new RuntimeException(e); + } + } + + /** + * @deprecated Use {@link #builderFromToken(Token)} + */ + @Deprecated public DeferredToken(Token token, Set usedDeferredWords) { - this.token = token; - this.usedDeferredWords = usedDeferredWords; - this.setDeferredWords = Collections.emptySet(); - importResourcePath = acquireImportResourcePath(); - macroStack = acquireMacroStack(); + this(token, usedDeferredWords, Collections.emptySet()); } + /** + * @deprecated Use {@link #builderFromToken(Token)} + */ + @Deprecated public DeferredToken( Token token, Set usedDeferredWords, Set setDeferredWords ) { + this( + token, + usedDeferredWords, + setDeferredWords, + acquireImportResourcePath(), + acquireMacroStack() + ); + } + + private DeferredToken( + Token token, + Set usedDeferredWords, + Set setDeferredWords, + String importResourcePath, + CallStack macroStack + ) { + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); this.token = token; + this.usedDeferredBases = + usedDeferredWords.isEmpty() + ? Collections.emptySet() + : usedDeferredWords + .stream() + .map(DeferredToken::splitToken) + .map(DeferredToken::getFirstNonEmptyToken) + .distinct() + .filter(word -> + interpreter == null || + !(interpreter.getContext().get(word) instanceof DeferredMacroValueImpl) + ) + .collect(Collectors.toSet()); this.usedDeferredWords = usedDeferredWords; + this.setDeferredBases = + setDeferredWords.isEmpty() + ? Collections.emptySet() + : setDeferredWords + .stream() + .map(DeferredToken::splitToken) + .map(DeferredToken::getFirstNonEmptyToken) + .collect(Collectors.toSet()); this.setDeferredWords = setDeferredWords; - importResourcePath = acquireImportResourcePath(); - macroStack = acquireMacroStack(); + this.importResourcePath = importResourcePath; + this.macroStack = macroStack; } public Token getToken() { @@ -49,10 +254,18 @@ public Set getUsedDeferredWords() { return usedDeferredWords; } + public Set getUsedDeferredBases() { + return usedDeferredBases; + } + public Set getSetDeferredWords() { return setDeferredWords; } + public Set getSetDeferredBases() { + return setDeferredBases; + } + public String getImportResourcePath() { return importResourcePath; } @@ -61,6 +274,156 @@ public CallStack getMacroStack() { return macroStack; } + public void addTo(Context context) { + addTo( + context, + usedDeferredBases + .stream() + .filter(word -> { + Object value = context.get(word); + return value != null && !(value instanceof DeferredValue); + }) + .collect(Collectors.toCollection(HashSet::new)) + ); + } + + private void addTo(Context context, Set wordsWithoutDeferredSource) { + context.getDeferredTokens().add(this); + deferPropertiesOnContext(context, wordsWithoutDeferredSource); + if (context.getParent() != null) { + Context parent = context.getParent(); + //Ignore global context + if (parent.getParent() != null) { + addTo(parent, wordsWithoutDeferredSource); + } else { + context.checkNumberOfDeferredTokens(); + } + } + } + + private void deferPropertiesOnContext( + Context context, + Set wordsWithoutDeferredSource + ) { + if (isInSameScope(context)) { + // set props are only deferred when within the scope which the variable is set in + markDeferredWordsAndFindSources(context, getSetDeferredBases(), true); + } + wordsWithoutDeferredSource.forEach(word -> deferDuplicatePointers(context, word)); + wordsWithoutDeferredSource.removeAll( + markDeferredWordsAndFindSources(context, wordsWithoutDeferredSource, false) + ); + } + + private boolean isInSameScope(Context context) { + return (getMacroStack() == null || getMacroStack() == context.getMacroStack()); + } + + // If 'list_a' and 'list_b' reference the same object, and 'list_a' is getting deferred, also defer 'list_b' + private static void deferDuplicatePointers(Context context, String 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.getValue() == wordValue || + (entry.getValue() instanceof DeferredValue && + ((DeferredValue) entry.getValue()).getOriginalValue() == wordValue) + ) + .forEach(entry -> { + matchingEntries.add(entry); + deferredLazyReference.getOriginalValue().setReferenceKey(entry.getKey()); + }); + temp = temp.getParent(); + } + if (matchingEntries.size() > 1) { // at least one duplicate + matchingEntries.forEach(entry -> { + if ( + deferredLazyReference + .getOriginalValue() + .getReferenceKey() + .equals(entry.getKey()) + ) { + convertToDeferredLazyReferenceSource(context, entry); + } else { + entry.setValue(deferredLazyReference.clone()); + } + }); + } + } + } + + private static void convertToDeferredLazyReferenceSource( + Context context, + Entry entry + ) { + Object val = entry.getValue(); + if (val instanceof DeferredLazyReferenceSource) { + return; + } + DeferredLazyReferenceSource deferredLazyReferenceSource = + DeferredLazyReferenceSource.instance( + val instanceof DeferredValue ? ((DeferredValue) val).getOriginalValue() : val + ); + + context.replace(entry.getKey(), deferredLazyReferenceSource); + entry.setValue(deferredLazyReferenceSource); + } + + private static Collection markDeferredWordsAndFindSources( + Context context, + Set wordsToDefer, + boolean replacing + ) { + return wordsToDefer + .stream() + .filter(prop -> { + Object val = context.get(prop); + if (replacing) { + return ( + !(val instanceof DeferredValue) || context.getScope().containsKey(prop) + ); + } + return !(val instanceof DeferredValue); + }) + .filter(prop -> !MetaContextVariables.isMetaContextVariable(prop, context)) + .filter(prop -> { + DeferredValue deferredValue = convertToDeferredValue(context, prop); + context.put(prop, deferredValue); + return !(deferredValue instanceof DeferredValueShadow); + }) + .collect(Collectors.toList()); + } + + private static DeferredValue convertToDeferredValue(Context context, String prop) { + Object valueInScope = context.getScope().get(prop); + Object value = context.get(prop); + if (value instanceof DeferredValue) { + value = ((DeferredValue) value).getOriginalValue(); + } + if (value != null) { + if (valueInScope == null) { + return DeferredValue.shadowInstance(value); + } else { + return DeferredValue.instance(value); + } + } + return DeferredValue.instance(); + } + private static String acquireImportResourcePath() { return (String) JinjavaInterpreter .getCurrentMaybe() @@ -75,4 +438,12 @@ private static CallStack acquireMacroStack() { .map(interpreter -> interpreter.getContext().getMacroStack()) .orElse(null); } + + private static String getFirstNonEmptyToken(List strings) { + return Strings.isNullOrEmpty(strings.get(0)) ? strings.get(1) : strings.get(0); + } + + public static List splitToken(String token) { + return Arrays.asList(token.split("\\.")); + } } 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..66952147e 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,22 +1,26 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.google.common.collect.Sets; +import com.google.common.annotations.Beta; +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.RawTag; import com.hubspot.jinjava.lib.tag.SetTag; -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 com.hubspot.jinjava.util.PrefixToPreserveState; import java.util.Optional; +import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Triple; +@Beta public class EagerBlockSetTagStrategy extends EagerSetTagStrategy { + public static final EagerBlockSetTagStrategy INSTANCE = new EagerBlockSetTagStrategy( new SetTag() ); @@ -28,28 +32,68 @@ protected EagerBlockSetTagStrategy(SetTag setTag) { @Override protected EagerExecutionResult getEagerExecutionResult( TagNode tagNode, + String[] variables, String expression, JinjavaInterpreter interpreter ) { - EagerExecutionResult result = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult eagerExecutionResult = EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromSupplier( - () -> { - StringBuilder sb = new StringBuilder(); - for (Node child : tagNode.getChildren()) { - sb.append(child.render(eagerInterpreter).getValue()); - } - return sb.toString(); - }, + () -> SetTag.renderChildren(tagNode, eagerInterpreter, variables[0]), 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()); + if ( + (!eagerExecutionResult.getResult().isFullyResolved() && + !eagerExecutionResult.getSpeculativeBindings().isEmpty()) || + interpreter.getContext().isDeferredExecutionMode() + ) { + EagerReconstructionUtils.resetAndDeferSpeculativeBindings( + interpreter, + eagerExecutionResult + ); + } + eagerExecutionResult = + unwrapRawTagsIfFullyResolved(interpreter, eagerExecutionResult); + return eagerExecutionResult; + } + + private static EagerExecutionResult unwrapRawTagsIfFullyResolved( + JinjavaInterpreter interpreter, + EagerExecutionResult eagerExecutionResult + ) { + if ( + eagerExecutionResult.getResult().isFullyResolved() && + eagerExecutionResult + .getResult() + .toString(true) + .contains( + interpreter.getConfig().getTokenScannerSymbols().getExpressionStartWithTag() + + " " + + RawTag.TAG_NAME + ) + ) { + try ( + TemporaryValueClosable temporaryValueClosable = interpreter + .getContext() + .withUnwrapRawOverride() + ) { + eagerExecutionResult = + new EagerExecutionResult( + EagerExpressionResult.fromString( + interpreter.renderFlat(eagerExecutionResult.asTemplateString()), + ResolutionState.FULL + ), + eagerExecutionResult.getSpeculativeBindings() + ); + } } - return result; + return eagerExecutionResult; } @Override @@ -69,11 +113,13 @@ protected Optional resolveSet( true ); if (filterPos >= 0) { - EagerExecutionResult filterResult = EagerInlineSetTagStrategy.INSTANCE.getEagerExecutionResult( - tagNode, - tagNode.getHelpers().trim(), - interpreter - ); + EagerExecutionResult filterResult = + EagerInlineSetTagStrategy.INSTANCE.getEagerExecutionResult( + tagNode, + variables, + tagNode.getHelpers().trim(), + interpreter + ); if (filterResult.getResult().isFullyResolved()) { setTag.executeSet( (TagToken) tagNode.getMaster(), @@ -109,27 +155,27 @@ protected Triple getPrefixTokenAndSuffix( .add(tagNode.getTag().getName()) .add(variables[0]) .add(tagNode.getSymbols().getExpressionEndWithTag()); - String prefixToPreserveState = getPrefixToPreserveState( + + PrefixToPreserveState prefixToPreserveState = getPrefixToPreserveState( eagerExecutionResult, + variables, interpreter - ); - - interpreter - .getContext() - .handleDeferredToken( - new DeferredToken( - new TagToken( - joiner.toString(), - tagNode.getLineNumber(), - tagNode.getStartPosition(), - tagNode.getSymbols() - ), - Collections.emptySet(), - Sets.newHashSet(variables) + ) + .withAllInFront( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromImage(joiner.toString(), tagNode.getMaster()) + .addSetDeferredWords(Stream.of(variables)) + .build() ) ); - String suffixToPreserveState = getSuffixToPreserveState(variables[0], interpreter); - return Triple.of(prefixToPreserveState, joiner.toString(), suffixToPreserveState); + String suffixToPreserveState = getSuffixToPreserveState(variables, interpreter); + return Triple.of( + prefixToPreserveState.toString(), + joiner.toString(), + suffixToPreserveState + ); } @Override @@ -162,11 +208,13 @@ protected String buildImage( int filterPos = tagNode.getHelpers().indexOf('|'); String filterSetPostfix = ""; if (filterPos >= 0) { - EagerExecutionResult filterResult = EagerInlineSetTagStrategy.INSTANCE.getEagerExecutionResult( - tagNode, - tagNode.getHelpers().trim(), - interpreter - ); + EagerExecutionResult filterResult = + EagerInlineSetTagStrategy.INSTANCE.getEagerExecutionResult( + tagNode, + variables, + tagNode.getHelpers().trim(), + interpreter + ); if (filterResult.getResult().isFullyResolved()) { setTag.executeSet( (TagToken) tagNode.getMaster(), @@ -196,12 +244,13 @@ private String runInlineStrategy( EagerExecutionResult eagerExecutionResult, JinjavaInterpreter interpreter ) { - Triple triple = EagerInlineSetTagStrategy.INSTANCE.getPrefixTokenAndSuffix( - tagNode, - variables, - eagerExecutionResult, - interpreter - ); + Triple triple = + EagerInlineSetTagStrategy.INSTANCE.getPrefixTokenAndSuffix( + tagNode, + variables, + eagerExecutionResult, + interpreter + ); if ( eagerExecutionResult.getResult().isFullyResolved() && interpreter.getContext().isDeferredExecutionMode() 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..128289fd5 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,25 +1,28 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; +import com.google.common.annotations.Beta; +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; import com.hubspot.jinjava.lib.expression.EagerExpressionStrategy; import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.lib.fn.eager.EagerMacroFunction; import com.hubspot.jinjava.lib.tag.CallTag; import com.hubspot.jinjava.lib.tag.FlexibleTag; 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 com.hubspot.jinjava.util.PrefixToPreserveState; import java.util.LinkedHashMap; -import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +@Beta public class EagerCallTag extends EagerStateChangingTag { public EagerCallTag() { @@ -37,38 +40,49 @@ public String eagerInterpret( InterpretException e ) { interpreter.getContext().checkNumberOfDeferredTokens(); - try (InterpreterScopeClosable c = interpreter.enterScope()) { - MacroFunction caller = new MacroFunction( - tagNode.getChildren(), - "caller", - new LinkedHashMap<>(), - true, - interpreter.getContext(), - interpreter.getLineNumber(), - interpreter.getPosition() - ); + MacroFunction caller; + EagerExecutionResult eagerExecutionResult; + PrefixToPreserveState prefixToPreserveState; + LengthLimitingStringJoiner joiner; + try (InterpreterScopeClosable c = interpreter.enterNonStackingScope()) { + caller = + new EagerMacroFunction( + tagNode.getChildren(), + "caller", + new LinkedHashMap<>(), + true, + interpreter.getContext(), + interpreter.getLineNumber(), + interpreter.getPosition() + ); interpreter.getContext().addGlobalMacro(caller); - EagerExecutionResult eagerExecutionResult = EagerReconstructionUtils.executeInChildContext( - eagerInterpreter -> - EagerExpressionResolver.resolveExpression( - tagNode.getHelpers().trim(), - interpreter - ), - interpreter, - EagerChildContextConfig - .newBuilder() - .withTakeNewValue(true) - .withPartialMacroEvaluation( - interpreter.getConfig().isNestedInterpretationEnabled() - ) - .withCheckForContextChanges(interpreter.getContext().isDeferredExecutionMode()) - .build() - ); - StringBuilder prefixToPreserveState = new StringBuilder(); - if (interpreter.getContext().isDeferredExecutionMode()) { - prefixToPreserveState.append(eagerExecutionResult.getPrefixToPreserveState()); + eagerExecutionResult = + EagerContextWatcher.executeInChildContext( + eagerInterpreter -> + EagerExpressionResolver.resolveExpression( + tagNode.getHelpers().trim(), + interpreter + ), + interpreter, + EagerContextWatcher.EagerChildContextConfig + .newBuilder() + .withTakeNewValue(true) + .withPartialMacroEvaluation( + interpreter.getConfig().isNestedInterpretationEnabled() + ) + .build() + ); + prefixToPreserveState = new PrefixToPreserveState(); + if ( + !eagerExecutionResult.getResult().isFullyResolved() || + interpreter.getContext().isDeferredExecutionMode() + ) { + prefixToPreserveState.putAll(eagerExecutionResult.getPrefixToPreserveState()); } else { - interpreter.getContext().putAll(eagerExecutionResult.getSpeculativeBindings()); + EagerReconstructionUtils.commitSpeculativeBindings( + interpreter, + eagerExecutionResult + ); } if (eagerExecutionResult.getResult().isFullyResolved()) { // Possible macro/set tag in front of this one. @@ -86,78 +100,68 @@ public String eagerInterpret( ) ); } + caller.setDeferred(true); - prefixToPreserveState.append( - EagerReconstructionUtils.reconstructFromContextBeforeDeferring( - eagerExecutionResult.getResult().getDeferredWords(), - interpreter - ) + // caller() needs to exist here so that the macro function can be reconstructed + EagerReconstructionUtils.hydrateReconstructionFromContextBeforeDeferring( + prefixToPreserveState, + eagerExecutionResult.getResult().getDeferredWords(), + interpreter ); + } - LengthLimitingStringJoiner joiner = new LengthLimitingStringJoiner( - interpreter.getConfig().getMaxOutputSize(), - " " - ); - joiner - .add(tagNode.getSymbols().getExpressionStartWithTag()) - .add(tagNode.getTag().getName()) - .add(eagerExecutionResult.getResult().toString().trim()) - .add(tagNode.getSymbols().getExpressionEndWithTag()); - interpreter - .getContext() - .handleDeferredToken( - new DeferredToken( - new TagToken( - joiner.toString(), - tagNode.getLineNumber(), - tagNode.getStartPosition(), - tagNode.getSymbols() - ), - eagerExecutionResult - .getResult() - .getDeferredWords() - .stream() - .filter( - word -> - !(interpreter.getContext().get(word) instanceof DeferredMacroValueImpl) - ) - .collect(Collectors.toSet()) + // Now preserve those variables from the scope the call tag was called in + prefixToPreserveState.withAllInFront( + new EagerExecutionResult( + eagerExecutionResult.getResult(), + eagerExecutionResult.getSpeculativeBindings() + ) + .getPrefixToPreserveState() + ); + joiner = + new LengthLimitingStringJoiner(interpreter.getConfig().getMaxOutputSize(), " "); + joiner + .add(tagNode.getSymbols().getExpressionStartWithTag()) + .add(tagNode.getTag().getName()) + .add(eagerExecutionResult.getResult().toString().trim()) + .add(tagNode.getSymbols().getExpressionEndWithTag()); + prefixToPreserveState.withAllInFront( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromImage(joiner.toString(), tagNode.getMaster()) + .addUsedDeferredWords(eagerExecutionResult.getResult().getDeferredWords()) + .build() + ) + ); + + StringBuilder result = new StringBuilder(prefixToPreserveState + joiner.toString()); + interpreter.getContext().setDynamicVariableResolver(s -> DeferredValue.instance()); + if (!tagNode.getChildren().isEmpty()) { + result.append( + EagerContextWatcher + .executeInChildContext( + eagerInterpreter -> + EagerExpressionResult.fromString(renderChildren(tagNode, eagerInterpreter)), + interpreter, + EagerContextWatcher.EagerChildContextConfig + .newBuilder() + .withForceDeferredExecutionMode(true) + .build() ) - ); - StringBuilder result = new StringBuilder( - prefixToPreserveState.toString() + joiner.toString() - ); - if (!tagNode.getChildren().isEmpty()) { - result.append( - EagerReconstructionUtils - .executeInChildContext( - eagerInterpreter -> - EagerExpressionResult.fromString( - renderChildren(tagNode, eagerInterpreter) - ), - interpreter, - EagerChildContextConfig - .newBuilder() - .withCheckForContextChanges(true) - .withForceDeferredExecutionMode(true) - .build() - ) - .asTemplateString() - ); - } - if ( - StringUtils.isNotBlank(tagNode.getEndName()) && - ( - !(getTag() instanceof FlexibleTag) || - ((FlexibleTag) getTag()).hasEndTag((TagToken) tagNode.getMaster()) - ) - ) { - result.append(EagerReconstructionUtils.reconstructEnd(tagNode)); - } // Possible set tag in front of this one. - return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( - result.toString(), - interpreter + .asTemplateString() ); } + if ( + StringUtils.isNotBlank(tagNode.getEndName()) && + (!(getTag() instanceof FlexibleTag) || + ((FlexibleTag) getTag()).hasEndTag((TagToken) tagNode.getMaster())) + ) { + result.append(EagerReconstructionUtils.reconstructEnd(tagNode)); + } // Possible set tag in front of this one. + return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( + result.toString(), + interpreter + ); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerContinueTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerContinueTag.java new file mode 100644 index 000000000..1aaef5e43 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerContinueTag.java @@ -0,0 +1,33 @@ +package com.hubspot.jinjava.lib.tag.eager; + +import com.google.common.annotations.Beta; +import com.hubspot.jinjava.interpret.DeferredValue; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.tag.ContinueTag; +import com.hubspot.jinjava.lib.tag.ForTag; +import com.hubspot.jinjava.tree.parse.TagToken; + +/** + * Eager decorator for the continue tag that handles reconstruction when the continue + * is inside a deferred context (e.g., when in deferred execution mode such as + * inside a deferred if condition within a for loop). + */ +@Beta +public class EagerContinueTag extends EagerTagDecorator { + + public EagerContinueTag() { + super(new ContinueTag()); + } + + public EagerContinueTag(ContinueTag continueTag) { + super(continueTag); + } + + @Override + public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter) { + if (!(interpreter.getContext().get(ForTag.LOOP) instanceof DeferredValue)) { + interpreter.getContext().replace(ForTag.LOOP, DeferredValue.instance()); + } + return super.getEagerTagImage(tagToken, interpreter); + } +} 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..4dc0917bf 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTag.java @@ -1,20 +1,23 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.google.common.annotations.Beta; import com.google.common.collect.ImmutableMap; -import com.hubspot.jinjava.el.ext.ExtendedParser; import com.hubspot.jinjava.interpret.JinjavaInterpreter; 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.EagerExpressionResolver.EagerExpressionResult; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig; import com.hubspot.jinjava.util.HelperStringTokenizer; +import com.hubspot.jinjava.util.PrefixToPreserveState; import com.hubspot.jinjava.util.WhitespaceUtils; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +@Beta public class EagerCycleTag extends EagerStateChangingTag { public EagerCycleTag() { @@ -43,22 +46,27 @@ 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 + EagerContextWatcher.EagerChildContextConfig .newBuilder() .withTakeNewValue(true) - .withCheckForContextChanges(interpreter.getContext().isDeferredExecutionMode()) .build() ); - StringBuilder prefixToPreserveState = new StringBuilder(); - if (interpreter.getContext().isDeferredExecutionMode()) { - prefixToPreserveState.append(eagerExecutionResult.getPrefixToPreserveState()); + PrefixToPreserveState prefixToPreserveState = new PrefixToPreserveState(); + if ( + !eagerExecutionResult.getResult().isFullyResolved() || + interpreter.getContext().isDeferredExecutionMode() + ) { + prefixToPreserveState.putAll(eagerExecutionResult.getPrefixToPreserveState()); } else { - interpreter.getContext().putAll(eagerExecutionResult.getSpeculativeBindings()); + EagerReconstructionUtils.commitSpeculativeBindings( + interpreter, + eagerExecutionResult + ); } String resolvedExpression; List resolvedValues; // can only be retrieved if the EagerExpressionResult are fully resolved. @@ -77,11 +85,10 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter if (!eagerExecutionResult.getResult().isFullyResolved()) { resolvedValues = new HelperStringTokenizer(resolvedExpression).splitComma(true).allTokens(); - prefixToPreserveState.append( - EagerReconstructionUtils.reconstructFromContextBeforeDeferring( - eagerExecutionResult.getResult().getDeferredWords(), - interpreter - ) + EagerReconstructionUtils.hydrateReconstructionFromContextBeforeDeferring( + prefixToPreserveState, + eagerExecutionResult.getResult().getDeferredWords(), + interpreter ); } else { List objects = eagerExecutionResult.getResult().toList(); @@ -112,7 +119,7 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter interpreter, resolvedValues, resolvedExpression, - eagerExecutionResult.getResult().isFullyResolved() + eagerExecutionResult.getResult() ) ); } else if (helper.size() == 3) { @@ -174,10 +181,22 @@ private String interpretPrintingCycle( JinjavaInterpreter interpreter, List values, String resolvedExpression, - boolean fullyResolved + EagerExpressionResult eagerExpressionResult ) { if (interpreter.getContext().isDeferredExecutionMode()) { - return reconstructCycleTag(resolvedExpression, tagToken); + String reconstructedTag = reconstructCycleTag(resolvedExpression, tagToken); + return ( + reconstructedTag + + new PrefixToPreserveState( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromImage(reconstructedTag, tagToken) + .addUsedDeferredWords(eagerExpressionResult.getDeferredWords()) + .build() + ) + ) + ); } Integer forindex = (Integer) interpreter.retraceVariable( CycleTag.LOOP_INDEX, @@ -189,14 +208,17 @@ private String interpretPrintingCycle( } if (values.size() == 1) { String var = values.get(0); - if (!fullyResolved) { + if (!eagerExpressionResult.isFullyResolved()) { return getIsIterable(var, forindex, tagToken); } else { return var; } } String item = values.get(forindex % values.size()); - if (!fullyResolved && EagerExpressionResolver.shouldBeEvaluated(item, interpreter)) { + if ( + !eagerExpressionResult.isFullyResolved() && + EagerExpressionResolver.shouldBeEvaluated(item, interpreter) + ) { return String.format("{{ %s }}", values.get(forindex % values.size())); } return item; @@ -216,19 +238,17 @@ private static String getIsIterable(String var, int forIndex, TagToken tagToken) String tokenEnd = tagToken.getSymbols().getExpressionEndWithTag(); return ( String.format( - "%s if exptest:iterable.evaluate(%s, %s) %s", + "%s if exptest:iterable.evaluate(%s, null) %s", tokenStart, var, - ExtendedParser.INTERPRETER, tokenEnd ) + // modulo indexing String.format( - "{{ %s[%d %% filter:length.filter(%s, %s)] }}", + "{{ %s[%d %% filter:length.filter(%s, ____int3rpr3t3r____)] }}", var, forIndex, - var, - ExtendedParser.INTERPRETER + var ) + String.format("%s else %s", tokenStart, tokenEnd) + String.format("{{ %s }}", var) + 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..87eb19292 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,19 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.google.common.annotations.Beta; +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; +import com.hubspot.jinjava.util.PrefixToPreserveState; -public class EagerDoTag extends EagerStateChangingTag { +@Beta +public class EagerDoTag extends EagerStateChangingTag implements FlexibleTag { public EagerDoTag() { super(new DoTag()); @@ -17,15 +24,54 @@ 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, + EagerContextWatcher.EagerChildContextConfig + .newBuilder() + .withTakeNewValue(true) + .build() + ); + PrefixToPreserveState prefixToPreserveState = new PrefixToPreserveState(); + if (interpreter.getContext().isDeferredExecutionMode()) { + prefixToPreserveState.withAll(eagerExecutionResult.getPrefixToPreserveState()); + } else { + EagerReconstructionUtils.commitSpeculativeBindings( + interpreter, + eagerExecutionResult + ); + } + if (eagerExecutionResult.getResult().isFullyResolved()) { + return (prefixToPreserveState.toString()); + } + return EagerReconstructionUtils.wrapInTag( + eagerExecutionResult.asTemplateString(), + getName(), interpreter, - tagToken.getImage(), - "Tag 'do' expects expression" + 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/EagerExecutionResult.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerExecutionResult.java index 1da827d0d..1d9c1cf4e 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 @@ -2,25 +2,33 @@ import static com.hubspot.jinjava.util.EagerReconstructionUtils.buildSetTag; +import com.google.common.annotations.Beta; +import com.hubspot.jinjava.interpret.DeferredLazyReference; +import com.hubspot.jinjava.interpret.DeferredLazyReferenceSource; +import com.hubspot.jinjava.interpret.DeferredValueShadow; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.LazyReference; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; +import com.hubspot.jinjava.util.EagerReconstructionUtils; +import com.hubspot.jinjava.util.PrefixToPreserveState; +import java.util.AbstractMap; +import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Map.Entry; import java.util.stream.Collectors; -import org.apache.commons.lang3.tuple.Pair; /** * This represents the result of speculatively executing an expression, where if something * got deferred, then the prefixToPreserveState can be added to the output * that would preserve the state for a second pass. */ +@Beta public class EagerExecutionResult { + private final EagerExpressionResult result; private final Map speculativeBindings; - private String prefixToPreserveState; + private PrefixToPreserveState prefixToPreserveState; public EagerExecutionResult( EagerExpressionResult result, @@ -38,56 +46,62 @@ public Map getSpeculativeBindings() { return speculativeBindings; } - public String getPrefixToPreserveState() { + public PrefixToPreserveState getPrefixToPreserveState() { if (prefixToPreserveState != null) { return prefixToPreserveState; } - prefixToPreserveState = - buildSetTag( - speculativeBindings - .entrySet() - .stream() - .filter(entry -> !(entry.getValue() instanceof LazyReference)) - .collect( - Collectors.toMap( - Entry::getKey, - entry -> PyishObjectMapper.getAsPyishString(entry.getValue()) - ) - ), - JinjavaInterpreter.getCurrent(), - !JinjavaInterpreter - .getCurrentMaybe() - .map(interpreter -> interpreter.getContext().isDeferredExecutionMode()) - .orElse(false) - ) + - speculativeBindings - .entrySet() - .stream() - .filter(entry -> (entry.getValue() instanceof LazyReference)) - .map( - entry -> - Pair.of(entry.getKey(), PyishObjectMapper.getAsPyishString(entry.getValue())) + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); + prefixToPreserveState = new PrefixToPreserveState(); + Collection> filteredEntries = speculativeBindings + .entrySet() + .stream() + .filter(entry -> { + Object contextValue = interpreter.getContext().get(entry.getKey()); + if (contextValue instanceof DeferredLazyReferenceSource) { + ((DeferredLazyReferenceSource) contextValue).setReconstructed(true); + } + return !(contextValue instanceof DeferredValueShadow); + }) + .collect(Collectors.toList()); + filteredEntries + .stream() + .filter(entry -> !(entry.getValue() instanceof DeferredLazyReference)) + .forEach(entry -> + EagerReconstructionUtils.hydrateBlockOrInlineSetTagRecursively( + prefixToPreserveState, + entry.getKey(), + entry.getValue(), + interpreter ) - .sorted( - (a, b) -> - a.getValue().equals(b.getKey()) ? 1 : b.getValue().equals(a.getKey()) ? -1 : 0 + ); + filteredEntries + .stream() + .filter(entry -> (entry.getValue() instanceof DeferredLazyReference)) + .map(entry -> + new AbstractMap.SimpleImmutableEntry<>( + entry.getKey(), + PyishObjectMapper.getAsPyishString( + ((DeferredLazyReference) entry.getValue()).getOriginalValue() + ) ) - .map( - pair -> - buildSetTag( - Collections.singletonMap(pair.getKey(), pair.getValue()), - JinjavaInterpreter.getCurrent(), - !JinjavaInterpreter - .getCurrentMaybe() - .map(interpreter -> interpreter.getContext().isDeferredExecutionMode()) - .orElse(false) - ) + ) + .sorted((a, b) -> + a.getValue().equals(b.getKey()) ? 1 : b.getValue().equals(a.getKey()) ? -1 : 0 + ) + .forEach(entry -> + prefixToPreserveState.put( + entry.getKey(), + buildSetTag( + Collections.singletonMap(entry.getKey(), entry.getValue()), + interpreter, + false + ) ) - .collect(Collectors.joining()); + ); return prefixToPreserveState; } public String asTemplateString() { - return getPrefixToPreserveState() + result; + return getPrefixToPreserveState().toString() + result.toString(true); } } 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..f87be2e68 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,33 +1,30 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.google.common.collect.Sets; +import com.google.common.annotations.Beta; +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; 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.objects.serialization.PyishObjectMapper; 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 com.hubspot.jinjava.util.PrefixToPreserveState; +import java.util.Collections; 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; +@Beta public class EagerForTag extends EagerTagDecorator { public EagerForTag() { @@ -39,57 +36,76 @@ public EagerForTag(ForTag forTag) { } @Override - public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { - Set addedTokens = new HashSet<>(); - EagerExecutionResult result = EagerReconstructionUtils.executeInChildContext( - eagerInterpreter -> { - EagerExpressionResult expressionResult = EagerExpressionResult.fromSupplier( - () -> getTag().interpretUnchecked(tagNode, eagerInterpreter), - eagerInterpreter - ); - addedTokens.addAll(eagerInterpreter.getContext().getDeferredTokens()); - return expressionResult; - }, + public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { + Pair, String> loopVarsAndExpression = getTag() + .getLoopVarsAndExpression((TagToken) tagNode.getMaster()); + EagerExecutionResult collectionResult = EagerContextWatcher.executeInChildContext( + eagerInterpreter -> + EagerExpressionResolver.resolveExpression( + '[' + loopVarsAndExpression.getRight() + ']', + interpreter + ), interpreter, - EagerChildContextConfig.newBuilder().withCheckForContextChanges(true).build() + EagerContextWatcher.EagerChildContextConfig + .newBuilder() + .withCheckForContextChanges(!interpreter.getContext().isDeferredExecutionMode()) + .build() ); - try { - if ( - result.getResult().getResolutionState() == ResolutionState.NONE || - ( - !result.getResult().isFullyResolved() && - !result.getSpeculativeBindings().isEmpty() - ) - ) { - EagerIfTag.resetBindingsForNextBranch(interpreter, result); + if (collectionResult.getResult().isFullyResolved()) { + Set addedTokens = new HashSet<>(); + EagerExecutionResult result = EagerContextWatcher.executeInChildContext( + eagerInterpreter -> { + EagerExpressionResult expressionResult = EagerExpressionResult.fromSupplier( + () -> { + try { + interpreter + .getContext() + .addNonMetaContextVariables(loopVarsAndExpression.getLeft()); + return getTag() + .renderForCollection( + tagNode, + eagerInterpreter, + loopVarsAndExpression.getLeft(), + !collectionResult.getResult().toList().isEmpty() + ? collectionResult.getResult().toList().get(0) + : Collections.emptyList() + ); + } finally { + interpreter + .getContext() + .removeNonMetaContextVariables(loopVarsAndExpression.getLeft()); + } + }, + eagerInterpreter + ); + addedTokens.addAll(eagerInterpreter.getContext().getDeferredTokens()); + return expressionResult; + }, + interpreter, + EagerContextWatcher.EagerChildContextConfig.newBuilder().build() + ); + if (result.getResult().getResolutionState() == ResolutionState.NONE) { + EagerReconstructionUtils.resetSpeculativeBindings(interpreter, collectionResult); + EagerReconstructionUtils.resetSpeculativeBindings(interpreter, result); interpreter.getContext().removeDeferredTokens(addedTokens); - throw new DeferredValueException( - result.getResult().getResolutionState() == ResolutionState.NONE - ? result.getResult().toString() - : "Modification inside partially evaluated for loop" - ); + throw new DeferredValueException(result.getResult().toString(true)); } 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()) + return ( + result + .getPrefixToPreserveState() + .withAllInFront(collectionResult.getPrefixToPreserveState()) + + EagerReconstructionUtils.wrapInChildScope( + result.getResult().toString(true), + interpreter + ) ); } } + EagerReconstructionUtils.resetSpeculativeBindings(interpreter, collectionResult); + throw new DeferredValueException(collectionResult.getResult().toString(true)); } @Override @@ -98,10 +114,12 @@ public String eagerInterpret( JinjavaInterpreter interpreter, InterpretException e ) { + if (e instanceof CannotReconstructValueException) { + throw e; + } LengthLimitingStringBuilder result = new LengthLimitingStringBuilder( interpreter.getConfig().getMaxOutputSize() ); - String prefix = ""; try ( TemporaryValueClosable c = interpreter @@ -114,7 +132,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( @@ -129,63 +147,75 @@ public String eagerInterpret( ) ), interpreter, - EagerChildContextConfig.newBuilder().build() + EagerContextWatcher.EagerChildContextConfig.newBuilder().build() ) .asTemplateString() ); } - 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()) - ) - ), + EagerExecutionResult firstRunResult = runLoopOnce(tagNode, interpreter, true); + PrefixToPreserveState prefixToPreserveState = firstRunResult + .getPrefixToPreserveState() + .withAllInFront( + EagerReconstructionUtils.resetAndDeferSpeculativeBindings( interpreter, - true - ); - } + firstRunResult + ) + ); // 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, false); + 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; + return prefixToPreserveState.toString() + result; } private EagerExecutionResult runLoopOnce( TagNode tagNode, - JinjavaInterpreter interpreter + JinjavaInterpreter interpreter, + boolean clearDeferredWords ) { - return EagerReconstructionUtils.executeInChildContext( + return EagerContextWatcher.executeInChildContext( eagerInterpreter -> { - if (!(eagerInterpreter.getContext().get("loop") instanceof DeferredValue)) { - eagerInterpreter.getContext().put("loop", DeferredValue.instance()); + if (!(eagerInterpreter.getContext().get(ForTag.LOOP) instanceof DeferredValue)) { + eagerInterpreter.getContext().put(ForTag.LOOP, DeferredValue.instance()); } - return EagerExpressionResult.fromString( - renderChildren(tagNode, eagerInterpreter) + List loopVars = getTag() + .getLoopVarsAndExpression((TagToken) tagNode.getMaster()) + .getLeft(); + interpreter.getContext().addNonMetaContextVariables(loopVars); + loopVars.forEach(var -> + interpreter.getContext().put(var, DeferredValue.instance()) ); + try { + return EagerExpressionResult.fromString( + renderChildren(tagNode, eagerInterpreter) + ); + } finally { + interpreter.getContext().removeNonMetaContextVariables(loopVars); + if (clearDeferredWords) { + interpreter + .getContext() + .removeDeferredTokens(interpreter.getContext().getDeferredTokens()); + } + } }, interpreter, - EagerChildContextConfig + EagerContextWatcher.EagerChildContextConfig .newBuilder() .withForceDeferredExecutionMode(true) - .withCheckForContextChanges(true) .build() ); } @@ -197,10 +227,8 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter List loopVars = loopVarsAndExpression.getLeft(); String loopExpression = loopVarsAndExpression.getRight(); - EagerExpressionResult eagerExpressionResult = EagerExpressionResolver.resolveExpression( - loopExpression, - interpreter - ); + EagerExpressionResult eagerExpressionResult = + EagerExpressionResolver.resolveExpression(loopExpression, interpreter); LengthLimitingStringJoiner joiner = new LengthLimitingStringJoiner( interpreter.getConfig().getMaxOutputSize(), @@ -214,38 +242,21 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter .add("in") .add(eagerExpressionResult.toString()) .add(tagToken.getSymbols().getExpressionEndWithTag()); - String newlyDeferredFunctionImages = EagerReconstructionUtils.reconstructFromContextBeforeDeferring( - eagerExpressionResult.getDeferredWords(), - interpreter - ); - Set metaLoopVars = Sets - .intersection( - interpreter.getContext().getMetaContextVariables(), - Sets.newHashSet(loopVars) - ) - .immutableCopy(); - interpreter.getContext().getMetaContextVariables().removeAll(metaLoopVars); - interpreter - .getContext() - .handleDeferredToken( - new DeferredToken( - new TagToken( - joiner.toString(), - tagToken.getLineNumber(), - tagToken.getStartPosition(), - tagToken.getSymbols() - ), - eagerExpressionResult - .getDeferredWords() - .stream() - .filter( - word -> - !(interpreter.getContext().get(word) instanceof DeferredMacroValueImpl) - ) - .collect(Collectors.toSet()), - new HashSet<>(loopVars) - ) + PrefixToPreserveState prefixToPreserveState = + EagerReconstructionUtils.hydrateReconstructionFromContextBeforeDeferring( + new PrefixToPreserveState(), + eagerExpressionResult.getDeferredWords(), + interpreter ); - return (newlyDeferredFunctionImages + joiner.toString()); + prefixToPreserveState.withAllInFront( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromImage(joiner.toString(), tagToken) + .addUsedDeferredWords(eagerExpressionResult.getDeferredWords()) + .build() + ) + ); + return (prefixToPreserveState + joiner.toString()); } } 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..7a0ed06c6 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,27 +1,37 @@ package com.hubspot.jinjava.lib.tag.eager; -import static com.hubspot.jinjava.lib.tag.SetTag.IGNORED_VARIABLE_NAME; - +import com.google.common.annotations.Beta; import com.google.common.collect.ImmutableMap; +import com.hubspot.algebra.Result; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl; 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.interpret.TagCycleException; +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 com.hubspot.jinjava.interpret.errorcategory.BasicTemplateErrorCategory; import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.lib.fn.eager.EagerMacroFunction; +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; +import com.hubspot.jinjava.lib.tag.eager.importing.EagerImportingStrategyFactory; import com.hubspot.jinjava.tree.Node; import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.util.EagerReconstructionUtils; +import com.hubspot.jinjava.util.PrefixToPreserveState; import java.io.IOException; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.Map.Entry; import java.util.stream.Collectors; +import java.util.stream.Stream; +@Beta public class EagerFromTag extends EagerStateChangingTag { public EagerFromTag() { @@ -34,137 +44,144 @@ public EagerFromTag(FromTag fromTag) { @Override public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter) { + String initialPathSetter = EagerImportingStrategyFactory.getSetTagForCurrentPath( + interpreter + ); List helper = FromTag.getHelpers(tagToken); Map imports = FromTag.getImportMap(helper); - Optional maybeTemplateFile; + AutoCloseableSupplier> maybeTemplateFileSupplier; try { - maybeTemplateFile = FromTag.getTemplateFile(helper, tagToken, interpreter); + maybeTemplateFileSupplier = + FromTag.getTemplateFileWithWrapper(helper, tagToken, interpreter); } catch (DeferredValueException e) { imports .values() - .forEach( - value -> { - MacroFunction deferredMacro = new MacroFunction( - null, - value, - null, - false, - null, - tagToken.getLineNumber(), - tagToken.getStartPosition() - ); - deferredMacro.setDeferred(true); - interpreter.getContext().addGlobalMacro(deferredMacro); - } - ); + .forEach(value -> { + MacroFunction deferredMacro = new EagerMacroFunction( + null, + value, + null, + false, + null, + tagToken.getLineNumber(), + tagToken.getStartPosition() + ); + deferredMacro.setDeferred(true); + interpreter.getContext().addGlobalMacro(deferredMacro); + }); return ( - EagerReconstructionUtils.buildSetTag( - ImmutableMap.of( - RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, - PyishObjectMapper.getAsPyishString( - interpreter.getContext().get(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY) - ) - ), - interpreter, - false + initialPathSetter + + new PrefixToPreserveState( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromToken(tagToken) + .addUsedDeferredWords(Stream.of(helper.get(0))) + .addUsedDeferredWords(imports.keySet()) + .addSetDeferredWords(imports.values()) + .build() + ) ) + tagToken.getImage() ); } - if (!maybeTemplateFile.isPresent()) { - return ""; - } - String templateFile = maybeTemplateFile.get(); - try { - try { - String template = interpreter.getResource(templateFile); - Node node = interpreter.parse(template); + try ( + AutoCloseableImpl> maybeTemplateFile = + maybeTemplateFileSupplier.get() + ) { + return maybeTemplateFile + .value() + .match( + err -> { + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.EXCEPTION, + ErrorItem.TAG, + "From cycle detected for path: '" + err.getPath() + "'", + null, + tagToken.getLineNumber(), + tagToken.getStartPosition(), + err, + BasicTemplateErrorCategory.FROM_CYCLE_DETECTED, + ImmutableMap.of("path", err.getPath()) + ) + ); + return ""; + }, + templateFile -> { + try { + String template = interpreter.getResource(templateFile); + Node node = interpreter.parse(template); - JinjavaInterpreter child = interpreter - .getConfig() - .getInterpreterFactory() - .newInstance(interpreter); - child.getContext().put(Context.IMPORT_RESOURCE_PATH_KEY, templateFile); - JinjavaInterpreter.pushCurrent(child); - String output; - try { - output = child.render(node); - } finally { - JinjavaInterpreter.popCurrent(); - } + JinjavaInterpreter child = interpreter + .getConfig() + .getInterpreterFactory() + .newInstance(interpreter); + child.getContext().put(Context.IMPORT_RESOURCE_PATH_KEY, templateFile); + String output; + output = child.render(node); - interpreter.addAllChildErrors(templateFile, child.getErrorsCopy()); + interpreter.addAllChildErrors(templateFile, child.getErrorsCopy()); - if (!child.getContext().getDeferredNodes().isEmpty()) { - FromTag.handleDeferredNodesDuringImport( - tagToken, - templateFile, - imports, - child, - interpreter - ); - } + if (!child.getContext().getDeferredNodes().isEmpty()) { + FromTag.handleDeferredNodesDuringImport( + tagToken, + templateFile, + imports, + child, + interpreter + ); + } - FromTag.integrateChild(imports, child, interpreter); - Map newToOldImportNames = renameMacros(imports, interpreter) - .entrySet() - .stream() - .filter(e -> !e.getKey().equals(e.getValue())) - .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); - if (child.getContext().getDeferredTokens().isEmpty() || output == null) { - return ""; - } else if (newToOldImportNames.size() > 0) { - // Set after output - output = - output + - EagerReconstructionUtils.buildSetTag(newToOldImportNames, interpreter, true); - } - return EagerReconstructionUtils.buildBlockSetTag( - IGNORED_VARIABLE_NAME, - output, - interpreter, - true - ); - } catch (IOException e) { - throw new InterpretException( - e.getMessage(), - e, - tagToken.getLineNumber(), - tagToken.getStartPosition() + FromTag.integrateChild(imports, child, interpreter); + Map newToOldImportNames = getNewToOldWithoutMacros( + imports, + interpreter + ); + if (child.getContext().getDeferredTokens().isEmpty() || output == null) { + return ""; + } else if (newToOldImportNames.size() > 0) { + // Set after output + output = + output + + EagerReconstructionUtils.buildSetTag( + newToOldImportNames, + interpreter, + true + ); + } + return EagerReconstructionUtils.wrapInTag( + output, + DoTag.TAG_NAME, + interpreter, + true + ); + } catch (IOException e) { + throw new InterpretException( + e.getMessage(), + e, + tagToken.getLineNumber(), + tagToken.getStartPosition() + ); + } + } ); - } - } finally { - interpreter.getContext().popFromStack(); } } - private static Map renameMacros( + private static Map getNewToOldWithoutMacros( Map oldToNewImportNames, JinjavaInterpreter interpreter ) { - Set toRemove = new HashSet<>(); - Map macroFunctions = oldToNewImportNames + return oldToNewImportNames .entrySet() .stream() - .filter( - e -> - !e.getKey().equals(e.getValue()) && - !interpreter.getContext().containsKey(e.getKey()) && - interpreter.getContext().isGlobalMacro(e.getKey()) + .filter(e -> !e.getKey().equals(e.getValue())) + .filter(e -> + interpreter.getContext().containsKey(e.getValue()) || + !interpreter.getContext().isGlobalMacro(e.getValue()) ) - .peek(entry -> toRemove.add(entry.getKey())) - .collect( - Collectors.toMap( - Map.Entry::getValue, - e -> interpreter.getContext().getGlobalMacro(e.getKey()) - ) - ); - - macroFunctions.forEach( - (key, value) -> - interpreter.getContext().addGlobalMacro(new MacroFunction(value, key)) - ); - toRemove.forEach(oldToNewImportNames::remove); - return oldToNewImportNames; + .collect(Collectors.toMap(Entry::getValue, Entry::getKey)); // flip order } } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerGenericTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerGenericTag.java index 4bd9b628e..f3917141e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerGenericTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerGenericTag.java @@ -1,7 +1,9 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.google.common.annotations.Beta; import com.hubspot.jinjava.lib.tag.Tag; +@Beta public class EagerGenericTag extends EagerTagDecorator implements Tag { public EagerGenericTag(T tag) { 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..dddc449c4 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 @@ -1,27 +1,25 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.hubspot.jinjava.interpret.DeferredValue; +import com.google.common.annotations.Beta; 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; import com.hubspot.jinjava.lib.tag.IfTag; 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 com.hubspot.jinjava.util.PrefixToPreserveState; import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +@Beta public class EagerIfTag extends EagerTagDecorator { public EagerIfTag() { @@ -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 @@ -70,17 +54,16 @@ public String eagerInterpret( ); result.append( - EagerReconstructionUtils + EagerContextWatcher .executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromString( eagerRenderBranches(tagNode, eagerInterpreter, e) ), interpreter, - EagerChildContextConfig + EagerContextWatcher.EagerChildContextConfig .newBuilder() .withForceDeferredExecutionMode(true) - .withCheckForContextChanges(true) .build() ) .asTemplateString() @@ -125,20 +108,21 @@ 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 + EagerContextWatcher.EagerChildContextConfig .newBuilder() .withForceDeferredExecutionMode(true) - .withCheckForContextChanges(true) .build() ); sb.append(result.getResult()); - bindingsToDefer.addAll(resetBindingsForNextBranch(interpreter, result)); + bindingsToDefer.addAll( + EagerReconstructionUtils.resetSpeculativeBindings(interpreter, result) + ); } if (branchEnd >= childrenSize || definitelyExecuted) { break; @@ -170,40 +154,12 @@ public String eagerRenderBranches( } branchStart = branchEnd + 1; } - if (!bindingsToDefer.isEmpty()) { - bindingsToDefer = + PrefixToPreserveState prefixToPreserveState = + EagerReconstructionUtils.deferWordsAndReconstructReferences( + interpreter, bindingsToDefer - .stream() - .filter(key -> !(interpreter.getContext().get(key) instanceof DeferredValue)) - .collect(Collectors.toSet()); - if (!bindingsToDefer.isEmpty()) { - interpreter - .getContext() - .handleDeferredToken( - new DeferredToken( - new NoteToken( - "", - interpreter.getLineNumber(), - interpreter.getPosition(), - interpreter.getConfig().getTokenScannerSymbols() - ), - bindingsToDefer - ) - ); - } - return sb.toString(); - } - 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(); + ); + return prefixToPreserveState + sb.toString(); } private String evaluateBranch( @@ -225,8 +181,8 @@ private int findNextElseToken(TagNode tagNode, int startIdx) { for (i = startIdx; i < tagNode.getChildren().size(); i++) { Node childNode = tagNode.getChildren().get(i); if ( - (TagNode.class.isAssignableFrom(childNode.getClass())) && - childNode.getName().equals(ElseIfTag.TAG_NAME) || + ((TagNode.class.isAssignableFrom(childNode.getClass())) && + childNode.getName().equals(ElseIfTag.TAG_NAME)) || childNode.getName().equals(ElseTag.TAG_NAME) ) { return i; 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..70117f3c6 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 @@ -1,34 +1,32 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; +import com.google.common.annotations.Beta; import com.google.common.collect.ImmutableMap; +import com.hubspot.algebra.Result; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl; import com.hubspot.jinjava.interpret.Context; -import com.hubspot.jinjava.interpret.DeferredValue; 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.interpret.TagCycleException; +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 com.hubspot.jinjava.interpret.errorcategory.BasicTemplateErrorCategory; +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; +import com.hubspot.jinjava.lib.tag.eager.importing.EagerImportingStrategy; +import com.hubspot.jinjava.lib.tag.eager.importing.EagerImportingStrategyFactory; +import com.hubspot.jinjava.lib.tag.eager.importing.ImportingData; import com.hubspot.jinjava.tree.Node; import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.util.EagerReconstructionUtils; import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.StringJoiner; -import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +@Beta public class EagerImportTag extends EagerStateChangingTag { public EagerImportTag() { @@ -41,344 +39,104 @@ public EagerImportTag(ImportTag importTag) { @Override public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter) { - List helper = ImportTag.getHelpers(tagToken); - - String currentImportAlias = ImportTag.getContextVar(helper); - - final String initialPathSetter = getSetTagForCurrentPath(interpreter); - final String newPathSetter; - - Optional maybeTemplateFile; - try { - maybeTemplateFile = ImportTag.getTemplateFile(helper, tagToken, interpreter); - } catch (DeferredValueException e) { - if (currentImportAlias.isEmpty()) { - throw e; - } + ImportingData importingData = EagerImportingStrategyFactory.getImportingData( + tagToken, interpreter - .getContext() - .handleDeferredToken( - new DeferredToken( - tagToken, - Collections.singleton(helper.get(0)), - Collections.singleton(currentImportAlias) - ) - ); - return (initialPathSetter + tagToken.getImage()); - } - if (!maybeTemplateFile.isPresent()) { - return ""; - } - String templateFile = maybeTemplateFile.get(); - try { - Node node = ImportTag.parseTemplateAsNode(interpreter, templateFile); - newPathSetter = getSetTagForCurrentPath(interpreter); - - JinjavaInterpreter child = interpreter - .getConfig() - .getInterpreterFactory() - .newInstance(interpreter); - child.getContext().put(Context.IMPORT_RESOURCE_PATH_KEY, templateFile); - JinjavaInterpreter.pushCurrent(child); - String output; - try { - setupImportAlias(currentImportAlias, child, interpreter); - output = child.render(node); - } finally { - JinjavaInterpreter.popCurrent(); - } - interpreter.addAllChildErrors(templateFile, child.getErrorsCopy()); - Map childBindings = child.getContext().getSessionBindings(); - - // 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()) { - ImportTag.handleDeferredNodesDuringImport( - node, - currentImportAlias, - childBindings, - child, - interpreter - ); - throw new DeferredValueException( - templateFile, - tagToken.getLineNumber(), - tagToken.getStartPosition() - ); - } - integrateChild(currentImportAlias, childBindings, child, interpreter); - String finalOutput; - if (child.getContext().getDeferredTokens().isEmpty() || output == null) { - return ""; - } 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 - ); - } else { - finalOutput = - newPathSetter + - getSetTagForDeferredChildBindings( - interpreter, - currentImportAlias, - childBindings - ) + - output + - initialPathSetter; - } - return EagerReconstructionUtils.buildBlockSetTag( - SetTag.IGNORED_VARIABLE_NAME, - finalOutput, - interpreter, - true - ); - } catch (IOException e) { - throw new InterpretException( - e.getMessage(), - e, - tagToken.getLineNumber(), - tagToken.getStartPosition() - ); - } finally { - interpreter.getContext().getCurrentPathStack().pop(); - interpreter.getContext().getImportPathStack().pop(); - } - } - - private String getSetTagForDeferredChildBindings( - JinjavaInterpreter interpreter, - String currentImportAlias, - Map childBindings - ) { - return EagerReconstructionUtils.buildSetTag( - childBindings - .entrySet() - .stream() - .filter(entry -> !interpreter.getContext().containsKey(entry.getKey())) - .filter(entry -> !entry.getKey().equals(currentImportAlias)) - .filter(entry -> entry.getValue() instanceof DeferredValue) - .collect( - Collectors.toMap( - Entry::getKey, - entry -> - PyishObjectMapper.getAsPyishString( - ((DeferredValue) entry.getValue()).getOriginalValue() - ) - ) - ), - interpreter, - false // false so that we don't defer them on higher context scopes; they only exist in the child scope ); - } - - public static String getSetTagForCurrentPath(JinjavaInterpreter interpreter) { - return EagerReconstructionUtils.buildSetTag( - ImmutableMap.of( - RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, - PyishObjectMapper.getAsPyishString( - interpreter - .getContext() - .getCurrentPathStack() - .peek() - .orElseGet( - () -> - (String) interpreter - .getContext() - .getOrDefault(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, "") - ) - ) - ), - interpreter, - false + EagerImportingStrategy eagerImportingStrategy = EagerImportingStrategyFactory.create( + importingData ); - } - @SuppressWarnings("unchecked") - private static String getDoTagToPreserve( - JinjavaInterpreter interpreter, - String currentImportAlias - ) { - StringJoiner keyValueJoiner = new StringJoiner(","); - Object currentAliasMap = interpreter - .getContext() - .getSessionBindings() - .get(currentImportAlias); - for (Map.Entry entry : ( - (Map) ((DeferredValue) currentAliasMap).getOriginalValue() - ).entrySet()) { - if (entry.getKey().equals(currentImportAlias)) { - continue; - } - if (entry.getValue() instanceof DeferredValue) { - keyValueJoiner.add(String.format("'%s': %s", entry.getKey(), entry.getKey())); - } else if (!(entry.getValue() instanceof MacroFunction)) { - keyValueJoiner.add( - String.format( - "'%s': %s", - entry.getKey(), - PyishObjectMapper.getAsPyishString(entry.getValue()) - ) + try ( + AutoCloseableImpl> templateFileResult = ImportTag + .getTemplateFileWithWrapper(importingData.getHelpers(), tagToken, interpreter) + .get() + ) { + return templateFileResult + .value() + .match( + err -> { + String path = StringUtils.trimToEmpty(importingData.getHelpers().get(0)); + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.EXCEPTION, + ErrorItem.TAG, + "Import cycle detected for path: '" + path + "'", + null, + tagToken.getLineNumber(), + tagToken.getStartPosition(), + err, + BasicTemplateErrorCategory.IMPORT_CYCLE_DETECTED, + ImmutableMap.of("path", path) + ) + ); + return ""; + }, + templateFile -> { + try ( + AutoCloseableImpl node = ImportTag + .parseTemplateAsNode(interpreter, templateFile) + .get() + ) { + JinjavaInterpreter child = interpreter + .getConfig() + .getInterpreterFactory() + .newInstance(interpreter); + child.getContext().put(Context.IMPORT_RESOURCE_PATH_KEY, templateFile); + String output; + eagerImportingStrategy.setup(child); + output = child.render(node.value()); + + interpreter.addAllChildErrors(templateFile, child.getErrorsCopy()); + Map childBindings = child.getContext().getSessionBindings(); + + // 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() || + (interpreter.getContext().isDeferredExecutionMode() && + !child.getContext().getGlobalMacros().isEmpty()) + ) { + ImportTag.handleDeferredNodesDuringImport( + node.value(), + ImportTag.getContextVar(importingData.getHelpers()), + childBindings, + child, + interpreter + ); + throw new DeferredValueException( + templateFile, + tagToken.getLineNumber(), + tagToken.getStartPosition() + ); + } + eagerImportingStrategy.integrateChild(child); + if (child.getContext().getDeferredTokens().isEmpty() || output == null) { + return ""; + } + return EagerReconstructionUtils.wrapInTag( + EagerReconstructionUtils.wrapPathAroundText( + eagerImportingStrategy.getFinalOutput(output, child), + templateFile, + interpreter + ), + DoTag.TAG_NAME, + interpreter, + true + ); + } catch (IOException e) { + throw new InterpretException( + e.getMessage(), + e, + tagToken.getLineNumber(), + tagToken.getStartPosition() + ); + } + } ); - } - } - if (keyValueJoiner.length() > 0) { - return EagerReconstructionUtils.buildDoUpdateTag( - currentImportAlias, - "{" + keyValueJoiner.toString() + "}", - interpreter - ); - } - return ""; - } - - @VisibleForTesting - public static void setupImportAlias( - String currentImportAlias, - JinjavaInterpreter child, - JinjavaInterpreter parent - ) { - if (!Strings.isNullOrEmpty(currentImportAlias)) { - Optional maybeParentImportAlias = parent - .getContext() - .getImportResourceAlias(); - if (maybeParentImportAlias.isPresent()) { - child - .getContext() - .getScope() - .put( - Context.IMPORT_RESOURCE_ALIAS_KEY, - String.format("%s.%s", maybeParentImportAlias.get(), currentImportAlias) - ); - } else { - child - .getContext() - .getScope() - .put(Context.IMPORT_RESOURCE_ALIAS_KEY, currentImportAlias); - } - constructFullAliasPathMap(currentImportAlias, child); - getMapForCurrentContextAlias(currentImportAlias, child); - } - } - - @SuppressWarnings("unchecked") - private static void constructFullAliasPathMap( - String currentImportAlias, - JinjavaInterpreter child - ) { - String fullImportAlias = child - .getContext() - .getImportResourceAlias() - .orElse(currentImportAlias); - String[] allAliases = fullImportAlias.split("\\."); - Map currentMap = child.getContext().getParent(); - for (int i = 0; i < allAliases.length - 1; i++) { - Object maybeNextMap = currentMap.get(allAliases[i]); - if (maybeNextMap instanceof Map) { - currentMap = (Map) maybeNextMap; - } else if ( - maybeNextMap instanceof DeferredValue && - ((DeferredValue) maybeNextMap).getOriginalValue() instanceof Map - ) { - currentMap = - (Map) ((DeferredValue) maybeNextMap).getOriginalValue(); - } else { - throw new InterpretException("Encountered a problem with import alias maps"); - } - } - currentMap.put(allAliases[allAliases.length - 1], new PyMap(new HashMap<>())); - } - - @SuppressWarnings("unchecked") - private static Map getMapForCurrentContextAlias( - String currentImportAlias, - JinjavaInterpreter child - ) { - Object parentValueForChild = child - .getContext() - .getParent() - .getSessionBindings() - .get(currentImportAlias); - if (parentValueForChild instanceof Map) { - return (Map) parentValueForChild; - } else if (parentValueForChild instanceof DeferredValue) { - if (((DeferredValue) parentValueForChild).getOriginalValue() instanceof Map) { - return (Map) ( - (DeferredValue) parentValueForChild - ).getOriginalValue(); - } - Map newMap = new PyMap(new HashMap<>()); - child - .getContext() - .getParent() - .put(currentImportAlias, DeferredValue.instance(newMap)); - return newMap; - } else { - Map newMap = new PyMap(new HashMap<>()); - child.getContext().getParent().put(currentImportAlias, newMap); - return newMap; - } - } - - @VisibleForTesting - public static void integrateChild( - String currentImportAlias, - Map childBindings, - JinjavaInterpreter child, - JinjavaInterpreter parent - ) { - childBindings.remove(SetTag.IGNORED_VARIABLE_NAME); - 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)); - } else { - childBindings.putAll(child.getContext().getGlobalMacros()); - Map mapForCurrentContextAlias = getMapForCurrentContextAlias( - currentImportAlias, - child - ); - // Remove layers from self down to original import alias to prevent reference loops - Arrays - .stream( - child - .getContext() - .getImportResourceAlias() - .orElse(currentImportAlias) - .split("\\.") - ) - .filter( - key -> - mapForCurrentContextAlias == - ( - childBindings.get(key) instanceof DeferredValue - ? ((DeferredValue) childBindings.get(key)).getOriginalValue() - : childBindings.get(key) - ) - ) - .forEach(childBindings::remove); - // Remove meta keys - childBindings.remove(Context.GLOBAL_MACROS_SCOPE_KEY); - childBindings.remove(Context.IMPORT_RESOURCE_ALIAS_KEY); - mapForCurrentContextAlias.putAll(childBindings); + } catch (DeferredValueException e) { + return eagerImportingStrategy.handleDeferredTemplateFile(e); } } } 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..91450dadf 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 @@ -1,15 +1,12 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.google.common.collect.ImmutableMap; +import com.google.common.annotations.Beta; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.IncludeTag; -import com.hubspot.jinjava.loader.RelativePathResolver; -import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.HelperStringTokenizer; -import org.apache.commons.lang3.StringUtils; +@Beta public class EagerIncludeTag extends EagerTagDecorator { public EagerIncludeTag(IncludeTag tag) { @@ -17,30 +14,16 @@ public EagerIncludeTag(IncludeTag tag) { } @Override - public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { + public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { + String templateFile = IncludeTag.resolveTemplateFile(tagNode, 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()); - String templateFile = interpreter.resolveString( - path, - tagNode.getLineNumber(), - tagNode.getStartPosition() - ); - templateFile = interpreter.resolveResourceLocation(templateFile); - final String initialPathSetter = EagerImportTag.getSetTagForCurrentPath( + return EagerReconstructionUtils.wrapPathAroundText( + output, + templateFile, interpreter ); - final String newPathSetter = EagerReconstructionUtils.buildSetTag( - ImmutableMap.of( - RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, - PyishObjectMapper.getAsPyishString(templateFile) - ), - interpreter, - false - ); - return newPathSetter + output + initialPathSetter; } return output; } 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..2c05052e8 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,22 +1,24 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; +import com.google.common.annotations.Beta; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; 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.PrefixToPreserveState; import com.hubspot.jinjava.util.WhitespaceUtils; import java.util.Arrays; import java.util.Optional; -import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.Triple; +@Beta public class EagerInlineSetTagStrategy extends EagerSetTagStrategy { + public static final EagerInlineSetTagStrategy INSTANCE = new EagerInlineSetTagStrategy( new SetTag() ); @@ -28,17 +30,17 @@ protected EagerInlineSetTagStrategy(SetTag setTag) { @Override public EagerExecutionResult getEagerExecutionResult( TagNode tagNode, + String[] variables, String expression, JinjavaInterpreter interpreter ) { - return EagerReconstructionUtils.executeInChildContext( + return EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResolver.resolveExpression('[' + expression + ']', interpreter), interpreter, - EagerChildContextConfig + EagerContextWatcher.EagerChildContextConfig .newBuilder() .withTakeNewValue(true) - .withCheckForContextChanges(interpreter.getContext().isDeferredExecutionMode()) .build() ); } @@ -84,38 +86,27 @@ public Triple getPrefixTokenAndSuffix( .add("=") .add(deferredResult) .add(tagNode.getSymbols().getExpressionEndWithTag()); - String prefixToPreserveState = getPrefixToPreserveState( + PrefixToPreserveState prefixToPreserveState = getPrefixToPreserveState( eagerExecutionResult, + variables, interpreter ); - - interpreter - .getContext() - .handleDeferredToken( - new DeferredToken( - new TagToken( - joiner.toString(), - tagNode.getLineNumber(), - tagNode.getStartPosition(), - tagNode.getSymbols() - ), - eagerExecutionResult - .getResult() - .getDeferredWords() - .stream() - .filter( - word -> - !(interpreter.getContext().get(word) instanceof DeferredMacroValueImpl) - ) - .collect(Collectors.toSet()), - Arrays.stream(variables).map(String::trim).collect(Collectors.toSet()) - ) - ); - String suffixToPreserveState = getSuffixToPreserveState( - String.join(",", Arrays.asList(variables)), - interpreter + prefixToPreserveState.withAllInFront( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromImage(joiner.toString(), tagNode.getMaster()) + .addUsedDeferredWords(eagerExecutionResult.getResult().getDeferredWords()) + .addSetDeferredWords(Arrays.stream(variables).map(String::trim)) + .build() + ) + ); + String suffixToPreserveState = getSuffixToPreserveState(variables, interpreter); + return Triple.of( + prefixToPreserveState.toString(), + joiner.toString(), + suffixToPreserveState ); - return Triple.of(prefixToPreserveState, joiner.toString(), suffixToPreserveState); } @Override diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerMacroTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerMacroTag.java new file mode 100644 index 000000000..5308219a5 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerMacroTag.java @@ -0,0 +1,31 @@ +package com.hubspot.jinjava.lib.tag.eager; + +import com.google.common.annotations.Beta; +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.MacroTag; +import com.hubspot.jinjava.tree.TagNode; +import java.util.LinkedHashMap; + +@Beta +public class EagerMacroTag extends MacroTag { + + @Override + protected MacroFunction constructMacroFunction( + TagNode tagNode, + JinjavaInterpreter interpreter, + String name, + LinkedHashMap argNamesWithDefaults + ) { + return new EagerMacroFunction( + tagNode.getChildren(), + name, + argNamesWithDefaults, + false, + interpreter.getContext(), + interpreter.getLineNumber(), + interpreter.getPosition() + ); + } +} 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..e0697dc0a 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,17 +1,18 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.hubspot.jinjava.interpret.DeferredMacroValueImpl; +import com.google.common.annotations.Beta; import com.hubspot.jinjava.interpret.JinjavaInterpreter; 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 com.hubspot.jinjava.util.PrefixToPreserveState; import org.apache.commons.lang3.StringUtils; +@Beta public class EagerPrintTag extends EagerStateChangingTag { public EagerPrintTag() { @@ -53,40 +54,42 @@ public static String interpretExpression( JinjavaInterpreter interpreter, boolean includeExpressionResult ) { - EagerExecutionResult eagerExecutionResult = EagerReconstructionUtils.executeInChildContext( + EagerExecutionResult eagerExecutionResult = EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResolver.resolveExpression(expr, interpreter), interpreter, - EagerChildContextConfig + EagerContextWatcher.EagerChildContextConfig .newBuilder() .withTakeNewValue(true) - .withCheckForContextChanges(interpreter.getContext().isDeferredExecutionMode()) .build() ); - StringBuilder prefixToPreserveState = new StringBuilder(); - if (interpreter.getContext().isDeferredExecutionMode()) { - prefixToPreserveState.append(eagerExecutionResult.getPrefixToPreserveState()); + PrefixToPreserveState prefixToPreserveState = new PrefixToPreserveState(); + if ( + !eagerExecutionResult.getResult().isFullyResolved() || + interpreter.getContext().isDeferredExecutionMode() + ) { + prefixToPreserveState.putAll(eagerExecutionResult.getPrefixToPreserveState()); } else { - interpreter.getContext().putAll(eagerExecutionResult.getSpeculativeBindings()); + EagerReconstructionUtils.commitSpeculativeBindings( + interpreter, + eagerExecutionResult + ); } if (eagerExecutionResult.getResult().isFullyResolved()) { // Possible macro/set tag in front of this one. return ( prefixToPreserveState.toString() + - ( - includeExpressionResult + (includeExpressionResult ? EagerReconstructionUtils.wrapInRawIfNeeded( eagerExecutionResult.getResult().toString(true), interpreter ) - : "" - ) + : "") ); } - prefixToPreserveState.append( - EagerReconstructionUtils.reconstructFromContextBeforeDeferring( - eagerExecutionResult.getResult().getDeferredWords(), - interpreter - ) + EagerReconstructionUtils.hydrateReconstructionFromContextBeforeDeferring( + prefixToPreserveState, + eagerExecutionResult.getResult().getDeferredWords(), + interpreter ); LengthLimitingStringJoiner joiner = new LengthLimitingStringJoiner( @@ -98,27 +101,15 @@ public static String interpretExpression( .add(tagToken.getTagName()) .add(eagerExecutionResult.getResult().toString().trim()) .add(tagToken.getSymbols().getExpressionEndWithTag()); - interpreter - .getContext() - .handleDeferredToken( - new DeferredToken( - new TagToken( - joiner.toString(), - tagToken.getLineNumber(), - tagToken.getStartPosition(), - tagToken.getSymbols() - ), - eagerExecutionResult - .getResult() - .getDeferredWords() - .stream() - .filter( - word -> - !(interpreter.getContext().get(word) instanceof DeferredMacroValueImpl) - ) - .collect(Collectors.toSet()) - ) - ); + prefixToPreserveState.withAllInFront( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromImage(joiner.toString(), tagToken) + .addUsedDeferredWords(eagerExecutionResult.getResult().getDeferredWords()) + .build() + ) + ); // Possible set tag in front of this one. return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( prefixToPreserveState.toString() + joiner.toString(), diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTag.java index 1a9c22ca6..bf37796c4 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTag.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.google.common.annotations.Beta; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.FlexibleTag; @@ -7,6 +8,7 @@ import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; +@Beta public class EagerSetTag extends EagerStateChangingTag implements FlexibleTag { public EagerSetTag() { 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..9af7f2263 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,19 +1,25 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.google.common.collect.Sets; +import com.google.common.annotations.Beta; +import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.MetaContextVariables; import com.hubspot.jinjava.lib.tag.SetTag; +import com.hubspot.jinjava.loader.RelativePathResolver; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.util.EagerReconstructionUtils; +import com.hubspot.jinjava.util.PrefixToPreserveState; 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 java.util.stream.Stream; import org.apache.commons.lang3.tuple.Triple; +@Beta public abstract class EagerSetTagStrategy { + protected final SetTag setTag; protected EagerSetTagStrategy(SetTag setTag) { @@ -36,31 +42,38 @@ public String run(TagNode tagNode, JinjavaInterpreter interpreter) { variables = new String[] { var }; 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); + interpreter + .getContext() + .addNonMetaContextVariables( + Arrays.stream(variables).map(String::trim).collect(Collectors.toList()) + ); EagerExecutionResult eagerExecutionResult = getEagerExecutionResult( tagNode, + variables, expression, interpreter ); + boolean triedResolve = false; if ( eagerExecutionResult.getResult().isFullyResolved() && - !interpreter.getContext().isDeferredExecutionMode() + !interpreter.getContext().isDeferredExecutionMode() && + (Arrays + .stream(variables) + .noneMatch(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY::equals) || + interpreter.getContext().getPenultimateParent().getDeferredTokens().isEmpty()) // Prevents set tags from disappearing in nested interpretation ) { - interpreter.getContext().putAll(eagerExecutionResult.getSpeculativeBindings()); + EagerReconstructionUtils.commitSpeculativeBindings( + interpreter, + eagerExecutionResult + ); Optional maybeResolved = resolveSet( tagNode, variables, eagerExecutionResult, interpreter ); + triedResolve = true; if (maybeResolved.isPresent()) { return maybeResolved.get(); } @@ -71,10 +84,7 @@ public String run(TagNode tagNode, JinjavaInterpreter interpreter) { eagerExecutionResult, interpreter ); - if ( - eagerExecutionResult.getResult().isFullyResolved() && - interpreter.getContext().isDeferredExecutionMode() - ) { + if (eagerExecutionResult.getResult().isFullyResolved() && !triedResolve) { attemptResolve(tagNode, variables, eagerExecutionResult, interpreter); } return buildImage(tagNode, variables, eagerExecutionResult, triple, interpreter); @@ -82,6 +92,7 @@ public String run(TagNode tagNode, JinjavaInterpreter interpreter) { protected abstract EagerExecutionResult getEagerExecutionResult( TagNode tagNode, + String[] variables, String expression, JinjavaInterpreter interpreter ); @@ -115,62 +126,113 @@ protected abstract String buildImage( JinjavaInterpreter interpreter ); - protected String getPrefixToPreserveState( + protected PrefixToPreserveState getPrefixToPreserveState( EagerExecutionResult eagerExecutionResult, + String[] variables, JinjavaInterpreter interpreter ) { - StringBuilder prefixToPreserveState = new StringBuilder(); - if (interpreter.getContext().isDeferredExecutionMode()) { - prefixToPreserveState.append(eagerExecutionResult.getPrefixToPreserveState()); + PrefixToPreserveState prefixToPreserveState = new PrefixToPreserveState(); + if ( + !eagerExecutionResult.getResult().isFullyResolved() || + interpreter.getContext().isDeferredExecutionMode() + ) { + prefixToPreserveState.putAll(eagerExecutionResult.getPrefixToPreserveState()); } else { - interpreter.getContext().putAll(eagerExecutionResult.getSpeculativeBindings()); + EagerReconstructionUtils.commitSpeculativeBindings( + interpreter, + eagerExecutionResult + ); } - prefixToPreserveState.append( - EagerReconstructionUtils.reconstructFromContextBeforeDeferring( - eagerExecutionResult.getResult().getDeferredWords(), - interpreter - ) + EagerReconstructionUtils.hydrateReconstructionFromContextBeforeDeferring( + prefixToPreserveState, + eagerExecutionResult.getResult().getDeferredWords(), + interpreter + ); + EagerReconstructionUtils.hydrateReconstructionFromContextBeforeDeferring( + prefixToPreserveState, + Arrays + .stream(variables) + .filter(var -> var.contains(".")) + .collect(Collectors.toSet()), + interpreter ); - return prefixToPreserveState.toString(); + return prefixToPreserveState; } - protected String getSuffixToPreserveState( - String variables, + public static String getSuffixToPreserveState( + List varList, + JinjavaInterpreter interpreter + ) { + if (varList.isEmpty()) { + return ""; + } + return getSuffixToPreserveState(varList.stream(), interpreter); + } + + public static String getSuffixToPreserveState( + String[] varList, + JinjavaInterpreter interpreter + ) { + if (varList.length == 0) { + return ""; + } + return getSuffixToPreserveState(Arrays.stream(varList), interpreter); + } + + private static String getSuffixToPreserveState( + Stream varStream, JinjavaInterpreter interpreter ) { StringBuilder suffixToPreserveState = new StringBuilder(); - Optional maybeFullImportAlias = interpreter + Optional maybeTemporaryImportAlias = interpreter .getContext() - .getImportResourceAlias(); - if (maybeFullImportAlias.isPresent()) { - String currentImportAlias = maybeFullImportAlias - .get() - .substring(maybeFullImportAlias.get().lastIndexOf(".") + 1); - String filteredVariables = Arrays - .stream(variables.split(",")) - .filter(var -> !var.equals(currentImportAlias)) - .collect(Collectors.joining(",")); - if (!filteredVariables.isEmpty()) { - String updateString = getUpdateString(filteredVariables); - suffixToPreserveState.append( - interpreter.render( - EagerReconstructionUtils.buildDoUpdateTag( - currentImportAlias, - updateString, - interpreter - ) - ) - ); + .getImportResourceAlias() + .map(MetaContextVariables::getTemporaryImportAlias); + if (maybeTemporaryImportAlias.isPresent()) { + boolean stillInsideImportTag = interpreter + .getContext() + .containsKey(maybeTemporaryImportAlias.get()); + List filteredVars = varStream + .filter(var -> + !MetaContextVariables.isMetaContextVariable(var, interpreter.getContext()) + ) + .peek(var -> { + if (!stillInsideImportTag) { + if ( + interpreter.retraceVariable( + String.format( + "%s.%s", + interpreter.getContext().getImportResourceAlias().get(), + var + ), + -1 + ) != + null + ) { + throw new DeferredValueException( + "Cannot modify temporary import alias outside of import tag" + ); + } + } + }) + .collect(Collectors.toList()); + if (filteredVars.isEmpty()) { + return ""; } + String updateString = getUpdateString(filteredVars); + // Don't need to render because the temporary import alias's value is always deferred, and rendering will do nothing + suffixToPreserveState.append( + EagerReconstructionUtils.buildDoUpdateTag( + maybeTemporaryImportAlias.get(), + updateString, + interpreter + ) + ); } return suffixToPreserveState.toString(); } - private static String getUpdateString(String variables) { - List varList = Arrays - .stream(variables.split(",")) - .map(String::trim) - .collect(Collectors.toList()); + private static String getUpdateString(List varList) { StringJoiner updateString = new StringJoiner(","); // Update the alias map to the value of the set variable. varList.forEach(var -> updateString.add(String.format("'%s': %s", var, var))); 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..5f0b19e81 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 @@ -1,16 +1,18 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.google.common.annotations.Beta; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.FlexibleTag; 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; +@Beta public class EagerStateChangingTag extends EagerTagDecorator { public EagerStateChangingTag(T tag) { @@ -18,7 +20,7 @@ public EagerStateChangingTag(T tag) { } @Override - public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { + public final String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { return eagerInterpret(tagNode, interpreter, null); } @@ -37,26 +39,20 @@ public String eagerInterpret( if (!tagNode.getChildren().isEmpty()) { result.append( - EagerReconstructionUtils + EagerContextWatcher .executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromString(renderChildren(tagNode, eagerInterpreter)), interpreter, - EagerChildContextConfig - .newBuilder() - .withCheckForContextChanges(true) - .withForceDeferredExecutionMode(true) - .build() + EagerContextWatcher.EagerChildContextConfig.newBuilder().build() ) .asTemplateString() ); } if ( StringUtils.isNotBlank(tagNode.getEndName()) && - ( - !(getTag() instanceof FlexibleTag) || - ((FlexibleTag) getTag()).hasEndTag((TagToken) tagNode.getMaster()) - ) + (!(getTag() instanceof FlexibleTag) || + ((FlexibleTag) getTag()).hasEndTag((TagToken) tagNode.getMaster())) ) { result.append(EagerReconstructionUtils.reconstructEnd(tagNode)); } 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..c6b73218f 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,28 +1,29 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.google.common.annotations.Beta; import com.hubspot.jinjava.el.ext.DeferredParsingException; -import com.hubspot.jinjava.interpret.DeferredValue; 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.Tag; import com.hubspot.jinjava.tree.Node; 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; +import com.hubspot.jinjava.util.PrefixToPreserveState; import org.apache.commons.lang3.StringUtils; +@Beta public abstract class EagerTagDecorator implements Tag { + private final T tag; public EagerTagDecorator(T tag) { @@ -34,24 +35,46 @@ 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; 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()) - ); + output = innerInterpret(tagNode, interpreter); + } catch (DeferredValueException | TemplateSyntaxException e) { + return wrapEagerInterpret(tagNode, interpreter, e); + } + if (JinjavaInterpreter.isOutputTooLarge(output)) { + return wrapEagerInterpret(tagNode, interpreter, null); } + return output; + } catch (OutputTooBigException e) { + throw new DeferredValueException( + String.format("Output too big for eager execution: %s", e.getMessage()) + ); } } + private String wrapEagerInterpret( + TagNode tagNode, + JinjavaInterpreter interpreter, + RuntimeException e + ) { + return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded( + eagerInterpret( + tagNode, + interpreter, + e instanceof InterpretException + ? (InterpretException) e + : new InterpretException("Exception with default render", e) + ), + interpreter + ); + } + + protected String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { + return tag.interpret(tagNode, interpreter); + } + @Override public String getName() { return tag.getName(); @@ -85,7 +108,7 @@ public String eagerInterpret( interpreter.getConfig().getMaxOutputSize() ); result.append( - EagerReconstructionUtils + EagerContextWatcher .executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromString( @@ -101,10 +124,9 @@ public String eagerInterpret( renderChildren(tagNode, eagerInterpreter) ), interpreter, - EagerChildContextConfig + EagerContextWatcher.EagerChildContextConfig .newBuilder() .withForceDeferredExecutionMode(true) - .withCheckForContextChanges(true) .build() ) .asTemplateString() @@ -166,6 +188,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); @@ -194,40 +217,33 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter .add(tagToken.getSymbols().getExpressionStartWithTag()) .add(tagToken.getTagName()); - EagerExpressionResult eagerExpressionResult = EagerExpressionResolver.resolveExpression( - tagToken.getHelpers().trim(), - interpreter - ); + EagerExpressionResult eagerExpressionResult = + EagerExpressionResolver.resolveExpression( + tagToken.getHelpers().trim(), + interpreter + ); String resolvedString = eagerExpressionResult.toString(); if (StringUtils.isNotBlank(resolvedString)) { joiner.add(resolvedString); } joiner.add(tagToken.getSymbols().getExpressionEndWithTag()); - String reconstructedFromContext = EagerReconstructionUtils.reconstructFromContextBeforeDeferring( + + PrefixToPreserveState prefixToPreserveState = new PrefixToPreserveState(); + EagerReconstructionUtils.hydrateReconstructionFromContextBeforeDeferring( + prefixToPreserveState, eagerExpressionResult.getDeferredWords(), interpreter ); + prefixToPreserveState.withAllInFront( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromImage(joiner.toString(), tagToken) + .addUsedDeferredWords(eagerExpressionResult.getDeferredWords()) + .build() + ) + ); - interpreter - .getContext() - .handleDeferredToken( - new DeferredToken( - new TagToken( - joiner.toString(), - tagToken.getLineNumber(), - tagToken.getStartPosition(), - tagToken.getSymbols() - ), - eagerExpressionResult - .getDeferredWords() - .stream() - .filter( - word -> !(interpreter.getContext().get(word) instanceof DeferredValue) - ) - .collect(Collectors.toSet()) - ) - ); - - return (reconstructedFromContext + joiner.toString()); + return (prefixToPreserveState + joiner.toString()); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagFactory.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagFactory.java index f3ab85b54..af6b354ae 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagFactory.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerTagFactory.java @@ -1,10 +1,12 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.google.common.annotations.Beta; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.hubspot.jinjava.lib.tag.BlockTag; import com.hubspot.jinjava.lib.tag.CallTag; +import com.hubspot.jinjava.lib.tag.ContinueTag; import com.hubspot.jinjava.lib.tag.CycleTag; import com.hubspot.jinjava.lib.tag.DoTag; import com.hubspot.jinjava.lib.tag.ElseIfTag; @@ -16,6 +18,7 @@ import com.hubspot.jinjava.lib.tag.IfTag; import com.hubspot.jinjava.lib.tag.ImportTag; import com.hubspot.jinjava.lib.tag.IncludeTag; +import com.hubspot.jinjava.lib.tag.MacroTag; import com.hubspot.jinjava.lib.tag.PrintTag; import com.hubspot.jinjava.lib.tag.RawTag; import com.hubspot.jinjava.lib.tag.SetTag; @@ -25,21 +28,25 @@ import java.util.Optional; import java.util.Set; +@Beta public class EagerTagFactory { - public static final Map, Class>> EAGER_TAG_OVERRIDES = ImmutableMap - ., Class>>builder() - .put(SetTag.class, EagerSetTag.class) - .put(DoTag.class, EagerDoTag.class) - .put(PrintTag.class, EagerPrintTag.class) - .put(FromTag.class, EagerFromTag.class) - .put(ImportTag.class, EagerImportTag.class) - .put(IncludeTag.class, EagerIncludeTag.class) - .put(ForTag.class, EagerForTag.class) - .put(CycleTag.class, EagerCycleTag.class) - .put(IfTag.class, EagerIfTag.class) - .put(UnlessTag.class, EagerUnlessTag.class) - .put(CallTag.class, EagerCallTag.class) - .build(); + + public static final Map, Class>> EAGER_TAG_OVERRIDES = + ImmutableMap + ., Class>>builder() + .put(SetTag.class, EagerSetTag.class) + .put(DoTag.class, EagerDoTag.class) + .put(PrintTag.class, EagerPrintTag.class) + .put(FromTag.class, EagerFromTag.class) + .put(ImportTag.class, EagerImportTag.class) + .put(IncludeTag.class, EagerIncludeTag.class) + .put(ForTag.class, EagerForTag.class) + .put(CycleTag.class, EagerCycleTag.class) + .put(IfTag.class, EagerIfTag.class) + .put(UnlessTag.class, EagerUnlessTag.class) + .put(CallTag.class, EagerCallTag.class) + .put(ContinueTag.class, EagerContinueTag.class) + .build(); // These classes don't need an eager decorator. public static final Set> TAG_CLASSES_TO_SKIP = ImmutableSet .>builder() @@ -60,15 +67,19 @@ public static Optional> getEagerTagDecorato if (TAG_CLASSES_TO_SKIP.contains(clazz)) { return Optional.empty(); } - if (EAGER_TAG_OVERRIDES.containsKey(clazz)) { - EagerTagDecorator decorator = EAGER_TAG_OVERRIDES - .get(clazz) + Class> eagerOverrideClass = + EAGER_TAG_OVERRIDES.get(clazz); + if (eagerOverrideClass != null) { + EagerTagDecorator decorator = eagerOverrideClass .getDeclaredConstructor(clazz) .newInstance(tag); if (decorator.getTag().getClass() == clazz) { return Optional.of((EagerTagDecorator) decorator); } } + if (tag instanceof MacroTag) { + return Optional.of(new EagerGenericTag<>((T) new EagerMacroTag())); + } return Optional.of(new EagerGenericTag<>(tag)); } catch (NoSuchMethodException e) { return Optional.empty(); diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerUnlessTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerUnlessTag.java index c1e4e4812..202737688 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerUnlessTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerUnlessTag.java @@ -1,7 +1,9 @@ package com.hubspot.jinjava.lib.tag.eager; +import com.google.common.annotations.Beta; import com.hubspot.jinjava.lib.tag.UnlessTag; +@Beta public class EagerUnlessTag extends EagerIfTag { public EagerUnlessTag() { diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/AliasedEagerImportingStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/AliasedEagerImportingStrategy.java new file mode 100644 index 000000000..fa211714f --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/AliasedEagerImportingStrategy.java @@ -0,0 +1,257 @@ +package com.hubspot.jinjava.lib.tag.eager.importing; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.interpret.Context; +import com.hubspot.jinjava.interpret.DeferredValue; +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.InterpretException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.MetaContextVariables; +import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.lib.tag.eager.DeferredToken; +import com.hubspot.jinjava.objects.collections.PyMap; +import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; +import com.hubspot.jinjava.util.EagerReconstructionUtils; +import com.hubspot.jinjava.util.PrefixToPreserveState; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.stream.Stream; + +public class AliasedEagerImportingStrategy implements EagerImportingStrategy { + + private final ImportingData importingData; + private final String currentImportAlias; + private final String fullImportAlias; + + @VisibleForTesting + public AliasedEagerImportingStrategy( + ImportingData importingData, + String currentImportAlias + ) { + this.importingData = importingData; + this.currentImportAlias = currentImportAlias; + Optional maybeParentImportAlias = importingData + .getOriginalInterpreter() + .getContext() + .getImportResourceAlias(); + if (maybeParentImportAlias.isPresent()) { + fullImportAlias = + String.format("%s.%s", maybeParentImportAlias.get(), currentImportAlias); + } else { + fullImportAlias = currentImportAlias; + } + } + + @Override + public String handleDeferredTemplateFile(DeferredValueException e) { + return ( + importingData.getInitialPathSetter() + + new PrefixToPreserveState( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + importingData.getOriginalInterpreter(), + DeferredToken + .builderFromToken(importingData.getTagToken()) + .addUsedDeferredWords(Stream.of(importingData.getHelpers().get(0))) + .addSetDeferredWords(Stream.of(currentImportAlias)) + .build() + ) + ) + + importingData.getTagToken().getImage() + ); + } + + @Override + public void setup(JinjavaInterpreter child) { + child.getContext().getScope().put(Context.IMPORT_RESOURCE_ALIAS_KEY, fullImportAlias); + child.getContext().put(Context.IMPORT_RESOURCE_ALIAS_KEY, fullImportAlias); + constructFullAliasPathMap(currentImportAlias, child); + getMapForCurrentContextAlias(currentImportAlias, child); + importingData + .getOriginalInterpreter() + .getContext() + .put( + MetaContextVariables.getTemporaryImportAlias(fullImportAlias), + DeferredValue.instance() + ); + } + + @Override + public void integrateChild(JinjavaInterpreter child) { + JinjavaInterpreter parent = importingData.getOriginalInterpreter(); + for (MacroFunction macro : child.getContext().getGlobalMacros().values()) { + if (parent.getContext().isDeferredExecutionMode()) { + macro.setDeferred(true); + } + } + Map childBindings = child.getContext().getSessionBindings(); + childBindings.putAll(child.getContext().getGlobalMacros()); + String temporaryImportAlias = MetaContextVariables.getTemporaryImportAlias( + fullImportAlias + ); + Map mapForCurrentContextAlias = getMapForCurrentContextAlias( + currentImportAlias, + child + ); + childBindings.remove(temporaryImportAlias); + importingData.getOriginalInterpreter().getContext().remove(temporaryImportAlias); + // Remove meta keys + childBindings + .entrySet() + .stream() + .filter(entry -> + !(entry.getKey().equals(Context.GLOBAL_MACROS_SCOPE_KEY) || + entry.getKey().equals(Context.IMPORT_RESOURCE_ALIAS_KEY)) + ) + .forEach(entry -> mapForCurrentContextAlias.put(entry.getKey(), entry.getValue())); + } + + @Override + public String getFinalOutput(String output, JinjavaInterpreter child) { + String temporaryImportAlias = MetaContextVariables.getTemporaryImportAlias( + fullImportAlias + ); + return ( + EagerReconstructionUtils.buildBlockOrInlineSetTag( + temporaryImportAlias, + Collections.emptyMap(), + importingData.getOriginalInterpreter() + ) + + wrapInChildScope( + EagerImportingStrategy.getSetTagForDeferredChildBindings( + child, + currentImportAlias, + child.getContext() + ) + + output, + child + ) + + EagerReconstructionUtils.buildSetTag( + ImmutableMap.of(currentImportAlias, temporaryImportAlias), + importingData.getOriginalInterpreter(), + true + ) + ); + } + + @SuppressWarnings("unchecked") + private static void constructFullAliasPathMap( + String currentImportAlias, + JinjavaInterpreter child + ) { + String fullImportAlias = child + .getContext() + .getImportResourceAlias() + .orElse(currentImportAlias); + String[] allAliases = fullImportAlias.split("\\."); + Map currentMap = child.getContext().getParent(); + for (int i = 0; i < allAliases.length - 1; i++) { + Object maybeNextMap = currentMap.get(allAliases[i]); + if (maybeNextMap instanceof Map) { + currentMap = (Map) maybeNextMap; + } else if ( + maybeNextMap instanceof DeferredValue && + ((DeferredValue) maybeNextMap).getOriginalValue() instanceof Map + ) { + currentMap = + (Map) ((DeferredValue) maybeNextMap).getOriginalValue(); + } else { + throw new InterpretException("Encountered a problem with import alias maps"); + } + } + currentMap.put( + allAliases[allAliases.length - 1], + child.getContext().isDeferredExecutionMode() + ? DeferredValue.instance(new PyMap(new HashMap<>())) + : new PyMap(new HashMap<>()) + ); + } + + @SuppressWarnings("unchecked") + private static Map getMapForCurrentContextAlias( + String currentImportAlias, + JinjavaInterpreter child + ) { + Object parentValueForChild = child + .getContext() + .getParent() + .getSessionBindings() + .get(currentImportAlias); + if (parentValueForChild instanceof Map) { + return (Map) parentValueForChild; + } else if (parentValueForChild instanceof DeferredValue) { + if (((DeferredValue) parentValueForChild).getOriginalValue() instanceof Map) { + return (Map) ((DeferredValue) parentValueForChild).getOriginalValue(); + } + Map newMap = new PyMap(new HashMap<>()); + child + .getContext() + .getParent() + .put(currentImportAlias, DeferredValue.instance(newMap)); + return newMap; + } else { + Map newMap = new PyMap(new HashMap<>()); + child + .getContext() + .getParent() + .put( + currentImportAlias, + child.getContext().isDeferredExecutionMode() + ? DeferredValue.instance(newMap) + : newMap + ); + return newMap; + } + } + + private String wrapInChildScope(String output, JinjavaInterpreter child) { + String combined = + output + getDoTagToPreserve(importingData.getOriginalInterpreter(), child); + // So that any set variables other than the alias won't exist outside the child's scope + return EagerReconstructionUtils.wrapInChildScope( + combined, + importingData.getOriginalInterpreter() + ); + } + + private String getDoTagToPreserve( + JinjavaInterpreter interpreter, + JinjavaInterpreter child + ) { + StringJoiner keyValueJoiner = new StringJoiner(","); + String temporaryImportAlias = MetaContextVariables.getTemporaryImportAlias( + fullImportAlias + ); + Map currentAliasMap = getMapForCurrentContextAlias( + currentImportAlias, + child + ); + for (Map.Entry entry : currentAliasMap.entrySet()) { + if (entry.getKey().equals(temporaryImportAlias)) { + continue; + } + if (entry.getValue() instanceof DeferredValue) { + keyValueJoiner.add(String.format("'%s': %s", entry.getKey(), entry.getKey())); + } else if (!(entry.getValue() instanceof MacroFunction)) { + keyValueJoiner.add( + String.format( + "'%s': %s", + entry.getKey(), + PyishObjectMapper.getAsPyishString(entry.getValue()) + ) + ); + } + } + if (keyValueJoiner.length() > 0) { + return EagerReconstructionUtils.buildDoUpdateTag( + temporaryImportAlias, + "{" + keyValueJoiner.toString() + "}", + interpreter + ); + } + return ""; + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategy.java new file mode 100644 index 000000000..33e6fce8f --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategy.java @@ -0,0 +1,40 @@ +package com.hubspot.jinjava.lib.tag.eager.importing; + +import com.hubspot.jinjava.interpret.DeferredValue; +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.util.EagerReconstructionUtils; +import java.util.Map; +import java.util.stream.Collectors; + +public interface EagerImportingStrategy { + String handleDeferredTemplateFile(DeferredValueException e); + void setup(JinjavaInterpreter child); + + void integrateChild(JinjavaInterpreter child); + String getFinalOutput(String output, JinjavaInterpreter child); + + static String getSetTagForDeferredChildBindings( + JinjavaInterpreter interpreter, + String currentImportAlias, + Map childBindings + ) { + return childBindings + .entrySet() + .stream() + .filter(entry -> + entry.getValue() instanceof DeferredValue && + ((DeferredValue) entry.getValue()).getOriginalValue() != null + ) + .filter(entry -> !interpreter.getContext().containsKey(entry.getKey())) + .filter(entry -> !entry.getKey().equals(currentImportAlias)) + .map(entry -> + EagerReconstructionUtils.buildBlockOrInlineSetTag( // don't register deferred token so that we don't defer them on higher context scopes; they only exist in the child scope + entry.getKey(), + ((DeferredValue) entry.getValue()).getOriginalValue(), + interpreter + ) + ) + .collect(Collectors.joining()); + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategyFactory.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategyFactory.java new file mode 100644 index 000000000..1ab5fefc4 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategyFactory.java @@ -0,0 +1,37 @@ +package com.hubspot.jinjava.lib.tag.eager.importing; + +import com.google.common.base.Strings; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.tag.ImportTag; +import com.hubspot.jinjava.loader.RelativePathResolver; +import com.hubspot.jinjava.tree.parse.TagToken; +import com.hubspot.jinjava.util.EagerReconstructionUtils; +import java.util.List; + +public class EagerImportingStrategyFactory { + + public static ImportingData getImportingData( + TagToken tagToken, + JinjavaInterpreter interpreter + ) { + List helpers = ImportTag.getHelpers(tagToken); + String initialPathSetter = getSetTagForCurrentPath(interpreter); + return new ImportingData(interpreter, tagToken, helpers, initialPathSetter); + } + + public static EagerImportingStrategy create(ImportingData importingData) { + String currentImportAlias = ImportTag.getContextVar(importingData.getHelpers()); + if (Strings.isNullOrEmpty(currentImportAlias)) { + return new FlatEagerImportingStrategy(importingData); + } + return new AliasedEagerImportingStrategy(importingData, currentImportAlias); + } + + public static String getSetTagForCurrentPath(JinjavaInterpreter interpreter) { + return EagerReconstructionUtils.buildBlockOrInlineSetTag( + RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, + RelativePathResolver.getCurrentPathFromStackOrKey(interpreter), + interpreter + ); + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/FlatEagerImportingStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/FlatEagerImportingStrategy.java new file mode 100644 index 000000000..17b4e4d68 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/FlatEagerImportingStrategy.java @@ -0,0 +1,96 @@ +package com.hubspot.jinjava.lib.tag.eager.importing; + +import com.google.common.annotations.VisibleForTesting; +import com.hubspot.jinjava.interpret.Context; +import com.hubspot.jinjava.interpret.DeferredValue; +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.MetaContextVariables; +import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.lib.tag.ImportTag; +import com.hubspot.jinjava.util.EagerReconstructionUtils; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +public class FlatEagerImportingStrategy implements EagerImportingStrategy { + + private final ImportingData importingData; + + @VisibleForTesting + public FlatEagerImportingStrategy(ImportingData importingData) { + this.importingData = importingData; + } + + @Override + public String handleDeferredTemplateFile(DeferredValueException e) { + throw e; + } + + @Override + public void setup(JinjavaInterpreter child) { + // Do nothing + } + + @Override + public void integrateChild(JinjavaInterpreter child) { + JinjavaInterpreter parent = importingData.getOriginalInterpreter(); + for (MacroFunction macro : child.getContext().getGlobalMacros().values()) { + if (parent.getContext().isDeferredExecutionMode()) { + macro.setDeferred(true); + } + } + for (MacroFunction macro : child.getContext().getGlobalMacros().values()) { + parent.getContext().addGlobalMacro(macro); + } + Map childBindings = child.getContext().getSessionBindings(); + + childBindings.remove(Context.GLOBAL_MACROS_SCOPE_KEY); + childBindings.remove(Context.IMPORT_RESOURCE_ALIAS_KEY); + 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); + } + } + + @Override + public String getFinalOutput(String output, JinjavaInterpreter child) { + if (importingData.getOriginalInterpreter().getContext().isDeferredExecutionMode()) { + // defer imported variables + Context context = importingData.getOriginalInterpreter().getContext(); + EagerReconstructionUtils.buildSetTag( + child + .getContext() + .getSessionBindings() + .entrySet() + .stream() + .filter(entry -> + !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null + ) + .filter(entry -> + !MetaContextVariables.isMetaContextVariable(entry.getKey(), context) + ) + .collect(Collectors.toMap(Entry::getKey, entry -> "")), + importingData.getOriginalInterpreter(), + true + ); + } + return ( + EagerImportingStrategy.getSetTagForDeferredChildBindings( + importingData.getOriginalInterpreter(), + null, + child.getContext().getSessionBindings() + ) + + output + ); + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/ImportingData.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/ImportingData.java new file mode 100644 index 000000000..579b3bff6 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/ImportingData.java @@ -0,0 +1,41 @@ +package com.hubspot.jinjava.lib.tag.eager.importing; + +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.tree.parse.TagToken; +import java.util.List; + +public class ImportingData { + + private final JinjavaInterpreter originalInterpreter; + private final TagToken tagToken; + private final List helpers; + private final String initialPathSetter; + + public ImportingData( + JinjavaInterpreter originalInterpreter, + TagToken tagToken, + List helpers, + String initialPathSetter + ) { + this.originalInterpreter = originalInterpreter; + this.tagToken = tagToken; + this.helpers = helpers; + this.initialPathSetter = initialPathSetter; + } + + public JinjavaInterpreter getOriginalInterpreter() { + return originalInterpreter; + } + + public TagToken getTagToken() { + return tagToken; + } + + public List getHelpers() { + return helpers; + } + + public String getInitialPathSetter() { + return initialPathSetter; + } +} diff --git a/src/main/java/com/hubspot/jinjava/loader/CascadingResourceLocator.java b/src/main/java/com/hubspot/jinjava/loader/CascadingResourceLocator.java index 61c5898b9..a32b28950 100644 --- a/src/main/java/com/hubspot/jinjava/loader/CascadingResourceLocator.java +++ b/src/main/java/com/hubspot/jinjava/loader/CascadingResourceLocator.java @@ -6,6 +6,7 @@ import java.util.Arrays; public class CascadingResourceLocator implements ResourceLocator { + private Iterable locators; public CascadingResourceLocator(ResourceLocator... locators) { @@ -17,8 +18,7 @@ public String getString( String fullName, Charset encoding, JinjavaInterpreter interpreter - ) - throws IOException { + ) throws IOException { for (ResourceLocator locator : locators) { try { return locator.getString(fullName, encoding, interpreter); diff --git a/src/main/java/com/hubspot/jinjava/loader/ClasspathResourceLocator.java b/src/main/java/com/hubspot/jinjava/loader/ClasspathResourceLocator.java index 57c77a199..9989c608e 100644 --- a/src/main/java/com/hubspot/jinjava/loader/ClasspathResourceLocator.java +++ b/src/main/java/com/hubspot/jinjava/loader/ClasspathResourceLocator.java @@ -12,8 +12,7 @@ public String getString( String fullName, Charset encoding, JinjavaInterpreter interpreter - ) - throws IOException { + ) throws IOException { try { return Resources.toString(Resources.getResource(fullName), encoding); } catch (IllegalArgumentException e) { diff --git a/src/main/java/com/hubspot/jinjava/loader/FileLocator.java b/src/main/java/com/hubspot/jinjava/loader/FileLocator.java index e1cb39458..de02a7173 100644 --- a/src/main/java/com/hubspot/jinjava/loader/FileLocator.java +++ b/src/main/java/com/hubspot/jinjava/loader/FileLocator.java @@ -23,6 +23,7 @@ import java.nio.charset.Charset; public class FileLocator implements ResourceLocator { + private File baseDir; /** diff --git a/src/main/java/com/hubspot/jinjava/loader/RelativePathResolver.java b/src/main/java/com/hubspot/jinjava/loader/RelativePathResolver.java index 9fb17a98e..77a4493c3 100644 --- a/src/main/java/com/hubspot/jinjava/loader/RelativePathResolver.java +++ b/src/main/java/com/hubspot/jinjava/loader/RelativePathResolver.java @@ -5,19 +5,13 @@ import java.nio.file.Paths; public class RelativePathResolver implements LocationResolver { + public static final String CURRENT_PATH_CONTEXT_KEY = "current_path"; @Override public String resolve(String path, JinjavaInterpreter interpreter) { if (path.startsWith("./") || path.startsWith("../")) { - String parentPath = interpreter - .getContext() - .getCurrentPathStack() - .peek() - .orElseGet( - () -> - (String) interpreter.getContext().getOrDefault(CURRENT_PATH_CONTEXT_KEY, "") - ); + String parentPath = getCurrentPathFromStackOrKey(interpreter); Path templatePath = Paths.get(parentPath); Path folderPath = templatePath.getParent() != null @@ -29,4 +23,14 @@ public String resolve(String path, JinjavaInterpreter interpreter) { } return path; } + + public static String getCurrentPathFromStackOrKey(JinjavaInterpreter interpreter) { + return interpreter + .getContext() + .getCurrentPathStack() + .peek() + .orElseGet(() -> + (String) interpreter.getContext().getOrDefault(CURRENT_PATH_CONTEXT_KEY, "") + ); + } } diff --git a/src/main/java/com/hubspot/jinjava/loader/ResourceNotFoundException.java b/src/main/java/com/hubspot/jinjava/loader/ResourceNotFoundException.java index c8ea6416d..2dbee08b9 100644 --- a/src/main/java/com/hubspot/jinjava/loader/ResourceNotFoundException.java +++ b/src/main/java/com/hubspot/jinjava/loader/ResourceNotFoundException.java @@ -3,6 +3,7 @@ import java.io.IOException; public class ResourceNotFoundException extends IOException { + private static final long serialVersionUID = 1L; public ResourceNotFoundException(String message) { diff --git a/src/main/java/com/hubspot/jinjava/mode/DefaultExecutionMode.java b/src/main/java/com/hubspot/jinjava/mode/DefaultExecutionMode.java index 24b652c8c..99f28b6c5 100644 --- a/src/main/java/com/hubspot/jinjava/mode/DefaultExecutionMode.java +++ b/src/main/java/com/hubspot/jinjava/mode/DefaultExecutionMode.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.mode; public class DefaultExecutionMode implements ExecutionMode { + private static final ExecutionMode INSTANCE = new DefaultExecutionMode(); private DefaultExecutionMode() {} diff --git a/src/main/java/com/hubspot/jinjava/mode/EagerExecutionMode.java b/src/main/java/com/hubspot/jinjava/mode/EagerExecutionMode.java index ab9a7f966..82a82ad90 100644 --- a/src/main/java/com/hubspot/jinjava/mode/EagerExecutionMode.java +++ b/src/main/java/com/hubspot/jinjava/mode/EagerExecutionMode.java @@ -5,11 +5,23 @@ 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 +53,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.addMetaContextVariables(STATIC_META_CONTEXT_VARIABLES); } } diff --git a/src/main/java/com/hubspot/jinjava/mode/NonRevertingEagerExecutionMode.java b/src/main/java/com/hubspot/jinjava/mode/NonRevertingEagerExecutionMode.java index 2e180584f..00ccc7d53 100644 --- a/src/main/java/com/hubspot/jinjava/mode/NonRevertingEagerExecutionMode.java +++ b/src/main/java/com/hubspot/jinjava/mode/NonRevertingEagerExecutionMode.java @@ -1,10 +1,17 @@ package com.hubspot.jinjava.mode; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + public class NonRevertingEagerExecutionMode extends EagerExecutionMode { + private static final ExecutionMode INSTANCE = new NonRevertingEagerExecutionMode(); protected NonRevertingEagerExecutionMode() {} + @SuppressFBWarnings( + value = "HSM_HIDING_METHOD", + justification = "Purposefully overriding to return static instance of this class." + ) public static ExecutionMode instance() { return INSTANCE; } diff --git a/src/main/java/com/hubspot/jinjava/mode/PreserveRawExecutionMode.java b/src/main/java/com/hubspot/jinjava/mode/PreserveRawExecutionMode.java index ca24d5098..152cf67dd 100644 --- a/src/main/java/com/hubspot/jinjava/mode/PreserveRawExecutionMode.java +++ b/src/main/java/com/hubspot/jinjava/mode/PreserveRawExecutionMode.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.mode; public class PreserveRawExecutionMode implements ExecutionMode { + private static final ExecutionMode INSTANCE = new PreserveRawExecutionMode(); private PreserveRawExecutionMode() {} diff --git a/src/main/java/com/hubspot/jinjava/objects/DummyObject.java b/src/main/java/com/hubspot/jinjava/objects/DummyObject.java index 55b7d1d0e..3d082cea7 100644 --- a/src/main/java/com/hubspot/jinjava/objects/DummyObject.java +++ b/src/main/java/com/hubspot/jinjava/objects/DummyObject.java @@ -1,11 +1,12 @@ package com.hubspot.jinjava.objects; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import java.util.Collection; import java.util.Map; import java.util.Set; -public class DummyObject implements Map, PyWrapper { +public class DummyObject implements Map, PyWrapper { @Override public int size() { @@ -33,7 +34,7 @@ public Object get(Object key) { } @Override - public Object put(String key, Object value) { + public Object put(Object key, Object value) { return new DummyObject(); } @@ -43,14 +44,14 @@ public Object remove(Object key) { } @Override - public void putAll(Map m) {} + public void putAll(Map m) {} @Override public void clear() {} @Override - public Set keySet() { - return null; + public Set keySet() { + return ImmutableSet.of(new DummyObject()); } @Override @@ -59,7 +60,7 @@ public Collection values() { } @Override - public Set> entrySet() { - return null; + public Set> entrySet() { + return ImmutableSet.of(Map.entry(new DummyObject(), new DummyObject())); } } diff --git a/src/main/java/com/hubspot/jinjava/objects/Namespace.java b/src/main/java/com/hubspot/jinjava/objects/Namespace.java index da3a42350..4069cf722 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 String toPyishString() { - return String.format("namespace(%s)", PyishSerializable.super.toPyishString()); + @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/SafeString.java b/src/main/java/com/hubspot/jinjava/objects/SafeString.java index bc4bd77a1..2d2d0fc58 100644 --- a/src/main/java/com/hubspot/jinjava/objects/SafeString.java +++ b/src/main/java/com/hubspot/jinjava/objects/SafeString.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonValue; public class SafeString { + private final String value; public SafeString(String value) { diff --git a/src/main/java/com/hubspot/jinjava/objects/collections/ArrayBacked.java b/src/main/java/com/hubspot/jinjava/objects/collections/ArrayBacked.java new file mode 100644 index 000000000..05937c1ed --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/collections/ArrayBacked.java @@ -0,0 +1,5 @@ +package com.hubspot.jinjava.objects.collections; + +public interface ArrayBacked { + Object backingArray(); +} 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..5658f6a0d 100644 --- a/src/main/java/com/hubspot/jinjava/objects/collections/PyList.java +++ b/src/main/java/com/hubspot/jinjava/objects/collections/PyList.java @@ -9,6 +9,8 @@ import java.util.Objects; public class PyList extends ForwardingList implements PyWrapper { + + private boolean computingHashCode = false; private final List list; public PyList(List list) { @@ -25,10 +27,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); } @@ -79,6 +87,14 @@ public int index(Object o) { return indexOf(o); } + @Override + public Object get(int index) { + if (index < 0 || index >= list.size()) { + throw createOutOfRangeException(index); + } + return super.get(index); + } + public int index(Object o, int begin, int end) { for (int i = begin; i < end; i++) { if (Objects.equals(o, get(i))) { @@ -93,4 +109,21 @@ IndexOutOfRangeException createOutOfRangeException(int index) { String.format("Index %d is out of range for list of size %d", index, list.size()) ); } + + /** + * This is not thread-safe + * @return hashCode, preventing recursion + */ + @Override + public int hashCode() { + if (computingHashCode) { + return Objects.hashCode(null); + } + try { + computingHashCode = true; + return super.hashCode(); + } finally { + computingHashCode = false; + } + } } 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..c885dfd70 100644 --- a/src/main/java/com/hubspot/jinjava/objects/collections/PyMap.java +++ b/src/main/java/com/hubspot/jinjava/objects/collections/PyMap.java @@ -3,9 +3,13 @@ import com.google.common.collect.ForwardingMap; import com.hubspot.jinjava.objects.PyWrapper; import java.util.Map; +import java.util.Objects; import java.util.Set; public class PyMap extends ForwardingMap implements PyWrapper { + + private boolean computingHashCode = false; + private final Map map; public PyMap(Map map) { @@ -17,6 +21,10 @@ protected Map delegate() { return map; } + public Object get(String key, Object defaultValue) { + return getOrDefault(key, defaultValue); + } + @Override public Object put(String s, Object o) { if (o == this) { @@ -38,6 +46,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"); @@ -54,4 +66,21 @@ public void putAll(Map m) { } super.putAll(m); } + + /** + * This is not thread-safe + * @return hashCode, preventing recursion + */ + @Override + public int hashCode() { + if (computingHashCode) { + return Objects.hashCode(null); + } + try { + computingHashCode = true; + return super.hashCode(); + } finally { + computingHashCode = false; + } + } } diff --git a/src/main/java/com/hubspot/jinjava/objects/collections/PySet.java b/src/main/java/com/hubspot/jinjava/objects/collections/PySet.java new file mode 100644 index 000000000..6c92560e2 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/collections/PySet.java @@ -0,0 +1,38 @@ +package com.hubspot.jinjava.objects.collections; + +import com.google.common.collect.ForwardingSet; +import com.hubspot.jinjava.objects.PyWrapper; +import java.util.Objects; +import java.util.Set; + +public class PySet extends ForwardingSet implements PyWrapper { + + private boolean computingHashCode = false; + private final Set set; + + public PySet(Set set) { + this.set = set; + } + + @Override + protected Set delegate() { + return set; + } + + /** + * This is not thread-safe + * @return hashCode, preventing recursion + */ + @Override + public int hashCode() { + if (computingHashCode) { + return Objects.hashCode(null); + } + try { + computingHashCode = true; + return super.hashCode(); + } finally { + computingHashCode = false; + } + } +} 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..ef2830631 100644 --- a/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyList.java +++ b/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyList.java @@ -9,8 +9,10 @@ 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; private boolean hasWarned; @@ -20,6 +22,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"); } @@ -43,13 +48,19 @@ public void add(int index, Object 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..e4c7b0b16 100644 --- a/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyMap.java +++ b/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPyMap.java @@ -10,6 +10,7 @@ import java.util.Map; public class SizeLimitingPyMap extends PyMap implements PyWrapper { + private int maxSize; private boolean hasWarned; @@ -19,6 +20,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"); } diff --git a/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPySet.java b/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPySet.java new file mode 100644 index 000000000..b3c25a2fe --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/collections/SizeLimitingPySet.java @@ -0,0 +1,68 @@ +package com.hubspot.jinjava.objects.collections; + +import com.hubspot.jinjava.interpret.CollectionTooBigException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; +import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; +import com.hubspot.jinjava.interpret.TemplateError.ErrorType; +import com.hubspot.jinjava.objects.PyWrapper; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import javax.annotation.Nonnull; + +public class SizeLimitingPySet extends PySet implements PyWrapper { + + private int maxSize; + private boolean hasWarned; + + public SizeLimitingPySet(Set set, int maxSize) { + super(Objects.requireNonNull(set, "set is null")); + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize must be >= 1"); + } + + this.maxSize = maxSize; + if (set.size() > maxSize) { + throw new CollectionTooBigException(set.size(), maxSize); + } + } + + @Override + public boolean add(Object element) { + checkSize(size() + 1); + return super.add(element); + } + + @Override + public boolean addAll(@Nonnull Collection elements) { + if (elements == null || elements.isEmpty()) { + return false; + } + checkSize(size() + elements.size()); + return super.addAll(elements); + } + + private void checkSize(int newSize) { + if (newSize > maxSize) { + throw new CollectionTooBigException(newSize, maxSize); + } else if (!hasWarned && newSize >= maxSize * 0.9) { + hasWarned = true; + JinjavaInterpreter current = JinjavaInterpreter.getCurrent(); + if (current == null) { + return; + } + current.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.COLLECTION_TOO_BIG, + String.format("Set is at 90%% of max size (%d of %d)", newSize, maxSize), + null, + -1, + -1, + new CollectionTooBigException(newSize, maxSize) + ) + ); + } + } +} 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/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..5eae9cabe --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/date/FixedDateTimeProvider.java @@ -0,0 +1,15 @@ +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; + } +} diff --git a/src/main/java/com/hubspot/jinjava/objects/date/FormattedDate.java b/src/main/java/com/hubspot/jinjava/objects/date/FormattedDate.java index 0d964e4aa..3f31b88bc 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/FormattedDate.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/FormattedDate.java @@ -3,6 +3,7 @@ import java.time.ZonedDateTime; public class FormattedDate { + private final String format; private final String language; private final ZonedDateTime date; 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..ac09a4cf2 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/InvalidDateFormatException.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/InvalidDateFormatException.java @@ -1,15 +1,25 @@ package com.hubspot.jinjava.objects.date; public class InvalidDateFormatException extends IllegalArgumentException { + private static final long serialVersionUID = -1577669116818659228L; 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/PyishDate.java b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java index 25bd02d31..f7b194dfa 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java @@ -1,13 +1,30 @@ 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; import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; import java.io.Serializable; +import java.time.DayOfWeek; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoField; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalField; +import java.time.temporal.TemporalQuery; +import java.time.temporal.TemporalUnit; +import java.time.temporal.ValueRange; import java.util.Date; import java.util.Objects; import java.util.Optional; @@ -22,6 +39,7 @@ public final class PyishDate extends Date implements Serializable, PyWrapper, PyishSerializable { + private static final long serialVersionUID = 1L; public static final String PYISH_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; public static final String FULL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; @@ -30,6 +48,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; @@ -47,7 +67,16 @@ 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) + .orElseGet(System::currentTimeMillis) + ) ), ZoneOffset.UTC ) @@ -100,6 +129,250 @@ public int getMicrosecond() { return date.get(ChronoField.MILLI_OF_SECOND); } + // ZonedDateTime delegate methods + + public ZoneId getZone() { + return date.getZone(); + } + + public ZoneOffset getOffset() { + return date.getOffset(); + } + + public ZonedDateTime withZoneSameLocal(ZoneId zone) { + return date.withZoneSameLocal(zone); + } + + public ZonedDateTime withZoneSameInstant(ZoneId zone) { + return date.withZoneSameInstant(zone); + } + + public ZonedDateTime withFixedOffsetZone() { + return date.withFixedOffsetZone(); + } + + public ZonedDateTime withEarlierOffsetAtOverlap() { + return date.withEarlierOffsetAtOverlap(); + } + + public ZonedDateTime withLaterOffsetAtOverlap() { + return date.withLaterOffsetAtOverlap(); + } + + public LocalDateTime toLocalDateTime() { + return date.toLocalDateTime(); + } + + public LocalDate toLocalDate() { + return date.toLocalDate(); + } + + public LocalTime toLocalTime() { + return date.toLocalTime(); + } + + public OffsetDateTime toOffsetDateTime() { + return date.toOffsetDateTime(); + } + + @Override + public Instant toInstant() { + return date.toInstant(); + } + + public boolean isSupported(TemporalField field) { + return date.isSupported(field); + } + + public boolean isSupported(TemporalUnit unit) { + return date.isSupported(unit); + } + + public ValueRange range(TemporalField field) { + return date.range(field); + } + + public int get(TemporalField field) { + return date.get(field); + } + + public long getLong(TemporalField field) { + return date.getLong(field); + } + + public int getMonthValue() { + return date.getMonthValue(); + } + + public int getDayOfMonth() { + return date.getDayOfMonth(); + } + + public int getDayOfYear() { + return date.getDayOfYear(); + } + + public DayOfWeek getDayOfWeek() { + return date.getDayOfWeek(); + } + + public int getNano() { + return date.getNano(); + } + + public ZonedDateTime with(TemporalAdjuster adjuster) { + return date.with(adjuster); + } + + public ZonedDateTime with(TemporalField field, long newValue) { + return date.with(field, newValue); + } + + public ZonedDateTime withYear(int year) { + return date.withYear(year); + } + + public ZonedDateTime withMonth(int month) { + return date.withMonth(month); + } + + public ZonedDateTime withDayOfMonth(int dayOfMonth) { + return date.withDayOfMonth(dayOfMonth); + } + + public ZonedDateTime withDayOfYear(int dayOfYear) { + return date.withDayOfYear(dayOfYear); + } + + public ZonedDateTime withHour(int hour) { + return date.withHour(hour); + } + + public ZonedDateTime withMinute(int minute) { + return date.withMinute(minute); + } + + public ZonedDateTime withSecond(int second) { + return date.withSecond(second); + } + + public ZonedDateTime withNano(int nanoOfSecond) { + return date.withNano(nanoOfSecond); + } + + public ZonedDateTime truncatedTo(TemporalUnit unit) { + return date.truncatedTo(unit); + } + + public ZonedDateTime plus(TemporalAmount amountToAdd) { + return date.plus(amountToAdd); + } + + public ZonedDateTime plus(long amountToAdd, TemporalUnit unit) { + return date.plus(amountToAdd, unit); + } + + public ZonedDateTime plusYears(long years) { + return date.plusYears(years); + } + + public ZonedDateTime plusMonths(long months) { + return date.plusMonths(months); + } + + public ZonedDateTime plusWeeks(long weeks) { + return date.plusWeeks(weeks); + } + + public ZonedDateTime plusDays(long days) { + return date.plusDays(days); + } + + public ZonedDateTime plusHours(long hours) { + return date.plusHours(hours); + } + + public ZonedDateTime plusMinutes(long minutes) { + return date.plusMinutes(minutes); + } + + public ZonedDateTime plusSeconds(long seconds) { + return date.plusSeconds(seconds); + } + + public ZonedDateTime plusNanos(long nanos) { + return date.plusNanos(nanos); + } + + public ZonedDateTime minus(TemporalAmount amountToSubtract) { + return date.minus(amountToSubtract); + } + + public ZonedDateTime minus(long amountToSubtract, TemporalUnit unit) { + return date.minus(amountToSubtract, unit); + } + + public ZonedDateTime minusYears(long years) { + return date.minusYears(years); + } + + public ZonedDateTime minusMonths(long months) { + return date.minusMonths(months); + } + + public ZonedDateTime minusWeeks(long weeks) { + return date.minusWeeks(weeks); + } + + public ZonedDateTime minusDays(long days) { + return date.minusDays(days); + } + + public ZonedDateTime minusHours(long hours) { + return date.minusHours(hours); + } + + public ZonedDateTime minusMinutes(long minutes) { + return date.minusMinutes(minutes); + } + + public ZonedDateTime minusSeconds(long seconds) { + return date.minusSeconds(seconds); + } + + public ZonedDateTime minusNanos(long nanos) { + return date.minusNanos(nanos); + } + + public R query(TemporalQuery query) { + return date.query(query); + } + + public long until(Temporal endExclusive, TemporalUnit unit) { + return date.until(endExclusive, unit); + } + + public String format(DateTimeFormatter formatter) { + return date.format(formatter); + } + + public long toEpochSecond() { + return date.toEpochSecond(); + } + + public String getDateFormat() { + return dateFormat; + } + + 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()); } @@ -126,7 +399,7 @@ public String toString() { ); } - return strftime(PYISH_DATE_FORMAT); + return strftime(dateFormat); } @Override @@ -147,11 +420,16 @@ public boolean equals(Object obj) { } @Override - public String toPyishString() { - return String.format( - "\"%s\"|strtotime(\"%s\")", - strftime(FULL_DATE_FORMAT), - FULL_DATE_FORMAT - ); + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable + .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/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java b/src/main/java/com/hubspot/jinjava/objects/date/StrftimeFormatter.java index c5f070c25..d36229872 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,18 @@ 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; +import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; 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; /** @@ -12,112 +21,105 @@ * @author jstehler */ public class StrftimeFormatter { + public static final String DEFAULT_DATE_FORMAT = "%H:%M / %d-%m-%Y"; /* * 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', 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")) + .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', localized(FormatStyle.SHORT, null)) + .put('X', localized(null, FormatStyle.MEDIUM)) + .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; } - } - return result.toString(); - } + final char finalChar = c; + + Optional + .ofNullable(components.get(finalChar)) + .orElseThrow(() -> + new InvalidDateFormatException( + strftime, + String.format("unknown format code '%s'", finalChar) + ) + ) + .append(builder, stripLeadingZero); + } - public static DateTimeFormatter formatter(String strftime) { - return formatter(strftime, Locale.ENGLISH); + return builder.toFormatter(); } - public static DateTimeFormatter formatter(String strftime, Locale locale) { + private static DateTimeFormatter formatter(String strftime, Locale locale) { DateTimeFormatter fmt; if (strftime == null) { @@ -139,7 +141,7 @@ public 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); @@ -158,10 +160,36 @@ public static String format(ZonedDateTime d, Locale locale) { } public static String format(ZonedDateTime d, String strftime) { - return format(d, strftime, Locale.ENGLISH); + return format( + d, + strftime, + JinjavaInterpreter + .getCurrentMaybe() + .map(JinjavaInterpreter::getConfig) + .map(JinjavaConfig::getLocale) + .orElse(Locale.ENGLISH) + ); } 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 + ); + } + + static ConversionComponent localized(FormatStyle dateStyle, FormatStyle timeStyle) { + return (builder, stripLeadingZero) -> builder.appendLocalized(dateStyle, timeStyle); + } + } } 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..ad7e0b5e8 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java @@ -0,0 +1,46 @@ +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 { + + private final JsonSerializer orignalSerializer; + + private BothCasingBeanSerializer(JsonSerializer jsonSerializer) { + this.orignalSerializer = jsonSerializer; + } + + public static BothCasingBeanSerializer wrapping( + JsonSerializer jsonSerializer + ) { + return new BothCasingBeanSerializer<>(jsonSerializer); + } + + @Override + public void serialize( + T value, + JsonGenerator gen, + SerializerProvider serializerProvider + ) throws IOException { + if ( + Boolean.TRUE.equals( + 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)) + .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/LengthLimitingJsonProcessingException.java b/src/main/java/com/hubspot/jinjava/objects/serialization/LengthLimitingJsonProcessingException.java new file mode 100644 index 000000000..bcd6789e2 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/LengthLimitingJsonProcessingException.java @@ -0,0 +1,31 @@ +package com.hubspot.jinjava.objects.serialization; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.annotations.Beta; + +@Beta +public class LengthLimitingJsonProcessingException extends JsonProcessingException { + + private final int maxSize; + private final int attemptedSize; + + protected LengthLimitingJsonProcessingException(int maxSize, int attemptedSize) { + 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/LengthLimitingWriter.java b/src/main/java/com/hubspot/jinjava/objects/serialization/LengthLimitingWriter.java new file mode 100644 index 000000000..84f6281a4 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/LengthLimitingWriter.java @@ -0,0 +1,72 @@ +package com.hubspot.jinjava.objects.serialization; + +import com.google.common.annotations.Beta; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.concurrent.atomic.AtomicInteger; + +@Beta +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 LengthLimitingWriter( + CharArrayWriter charArrayWriter, + AtomicInteger remainingLength + ) { + this.charArrayWriter = charArrayWriter; + this.remainingLength = remainingLength; + startingLength = remainingLength.get(); + } + + @Override + public void write(int c) throws LengthLimitingJsonProcessingException { + checkMaxSize(1); + charArrayWriter.write(c); + } + + @Override + public void write(char[] c, int off, int len) + throws LengthLimitingJsonProcessingException { + checkMaxSize(len); + charArrayWriter.write(c, off, len); + } + + @Override + public void write(String str, int off, int len) + throws LengthLimitingJsonProcessingException { + checkMaxSize(len); + charArrayWriter.write(str, off, len); + } + + private void checkMaxSize(int extra) throws LengthLimitingJsonProcessingException { + if (remainingLength.addAndGet(extra * -1) < 0) { + throw new LengthLimitingJsonProcessingException( + startingLength, + charArrayWriter.size() + extra + ); + } + } + + 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/objects/serialization/MapEntrySerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java index b82d878bc..69c90aa68 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,53 @@ 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 com.google.common.annotations.Beta; +import java.io.CharArrayWriter; import java.io.IOException; -import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicInteger; + +@Beta +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() + ) throws IOException { + AtomicInteger remainingLength = (AtomicInteger) serializerProvider.getAttribute( + LengthLimitingWriter.REMAINING_LENGTH_ATTRIBUTE ); - String value = PyishObjectMapper.PYISH_OBJECT_WRITER.writeValueAsString( - object.getValue() + 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.withAttribute( + LengthLimitingWriter.REMAINING_LENGTH_ATTRIBUTE, + remainingLength + ); + key = objectWriter.writeValueAsString(entry.getKey()); + LengthLimitingWriter lengthLimitingWriter = new LengthLimitingWriter( + new CharArrayWriter(), + remainingLength + ); + objectWriter.writeValue(lengthLimitingWriter, entry.getValue()); + value = lengthLimitingWriter.toString(); + } else { + 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/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java index a55f0bf36..255c34cdf 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java @@ -3,11 +3,16 @@ 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 com.google.common.annotations.Beta; import java.util.Map; +@Beta public class PyishBeanSerializerModifier extends BeanSerializerModifier { - public static final PyishBeanSerializerModifier INSTANCE = new PyishBeanSerializerModifier(); + + public static final PyishBeanSerializerModifier INSTANCE = + new PyishBeanSerializerModifier(); private PyishBeanSerializerModifier() {} @@ -23,6 +28,9 @@ public JsonSerializer modifySerializer( if (Map.Entry.class.isAssignableFrom(beanDesc.getBeanClass())) { return MapEntrySerializer.INSTANCE; } + if (serializer instanceof BeanSerializer) { + return BothCasingBeanSerializer.wrapping(serializer); + } return serializer; } else { return PyishSerializer.INSTANCE; 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..e7d52e677 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBlockSetSerializable.java @@ -0,0 +1,8 @@ +package com.hubspot.jinjava.objects.serialization; + +import com.google.common.annotations.Beta; + +@Beta +public interface PyishBlockSetSerializable { + String getBlockSetBody(); +} 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..f7207ec1d 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishCharacterEscapes.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishCharacterEscapes.java @@ -2,9 +2,13 @@ import com.fasterxml.jackson.core.SerializableString; import com.fasterxml.jackson.core.io.CharacterEscapes; +import com.fasterxml.jackson.core.io.SerializedString; +import com.google.common.annotations.Beta; import java.util.Arrays; +@Beta public class PyishCharacterEscapes extends CharacterEscapes { + public static final PyishCharacterEscapes INSTANCE = new PyishCharacterEscapes(); private final int[] asciiEscapes; @@ -14,6 +18,7 @@ private PyishCharacterEscapes() { escapes['\t'] = CharacterEscapes.ESCAPE_NONE; escapes['\r'] = CharacterEscapes.ESCAPE_NONE; escapes['\f'] = CharacterEscapes.ESCAPE_NONE; + escapes['\''] = CharacterEscapes.ESCAPE_CUSTOM; asciiEscapes = escapes; } @@ -23,7 +28,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..551ba77d5 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -1,67 +1,149 @@ 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.JsonMappingException; 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.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.google.common.annotations.Beta; +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; +import java.io.Writer; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +@Beta 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 { - ObjectMapper mapper = new ObjectMapper() - .registerModule( + 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() + ) + .registerModule(new Jdk8Module()) + .registerModule( new SimpleModule() .setSerializerModifier(PyishBeanSerializerModifier.INSTANCE) .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) { - if (val != null) { - return WhitespaceUtils.unquoteAndUnescape(getAsPyishString(val)); + if (val == null) { + return ""; } - return ""; + if (val instanceof String || val instanceof Number || val instanceof Boolean) { + return val.toString(); + } + return WhitespaceUtils.unquoteAndUnescape(getAsPyishString(val, true)); } public static String getAsPyishString(Object val) { + return getAsPyishString(val, false); + } + + private static String getAsPyishString(Object val, boolean forOutput) { try { - return getAsPyishStringOrThrow(val); - } catch (JsonProcessingException e) { + return getAsPyishStringOrThrow(val, forOutput); + } catch (IOException e) { + handleLengthLimitingException(e); + handleDeferredValueException(e); return Objects.toString(val, ""); } } - public static String getAsPyishStringOrThrow(Object val) - throws JsonProcessingException { - String string = PYISH_OBJECT_WRITER.writeValueAsString(val); - Optional maxStringLength = JinjavaInterpreter + private static void handleDeferredValueException(IOException e) { + Throwable unwrapped = e; + if (e instanceof JsonMappingException) { + unwrapped = unwrapped.getCause(); + } + if (unwrapped instanceof DeferredValueException) { + throw (DeferredValueException) unwrapped; + } + } + + public static void handleLengthLimitingException(IOException e) { + Throwable unwrapped = e; + if (e instanceof JsonMappingException) { + unwrapped = unwrapped.getCause(); + } + if (unwrapped instanceof LengthLimitingJsonProcessingException) { + throw new OutputTooBigException( + ((LengthLimitingJsonProcessingException) unwrapped).getMaxSize(), + ((LengthLimitingJsonProcessingException) unwrapped).getAttemptedSize() + ); + } else if (unwrapped instanceof OutputTooBigException) { + throw (OutputTooBigException) unwrapped; + } + } + + public static String getAsPyishStringOrThrow(Object val) throws IOException { + return getAsPyishStringOrThrow(val, false); + } + + public static String getAsPyishStringOrThrow(Object val, boolean forOutput) + throws IOException { + 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() - .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()); + if (maxOutputSize.isPresent()) { + AtomicInteger remainingLength = new AtomicInteger( + (int) Math.min(Integer.MAX_VALUE, maxOutputSize.get()) + ); + objectWriter = + objectWriter.withAttribute( + LengthLimitingWriter.REMAINING_LENGTH_ATTRIBUTE, + remainingLength + ); + writer = new LengthLimitingWriter(new CharArrayWriter(), remainingLength); + } else { + writer = new CharArrayWriter(); } - String result = string - .replace("'", "\\'") - // Replace double-quotes with single quote as they are preferred in Jinja - .replaceAll("(? { @@ -71,8 +153,7 @@ public void serialize( Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider - ) - throws IOException { + ) throws IOException { jsonGenerator.writeFieldName(""); } } 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..b74ab5afe 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishPrettyPrinter.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishPrettyPrinter.java @@ -2,9 +2,12 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.google.common.annotations.Beta; import java.io.IOException; +@Beta public class PyishPrettyPrinter extends DefaultPrettyPrinter { + public static final PyishPrettyPrinter INSTANCE = new PyishPrettyPrinter(); @Override @@ -41,6 +44,6 @@ public void writeEndObject(JsonGenerator jg, int nrOfEntries) throws IOException if (!this._objectIndenter.isInline()) { --this._nesting; } - jg.writeRaw('}'); + jg.writeRaw("} "); } } 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..da9dd29c5 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializable.java @@ -1,24 +1,68 @@ 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.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.google.common.annotations.Beta; +import com.hubspot.jinjava.interpret.DeferredValueException; 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; +@Beta public interface PyishSerializable extends PyWrapper { - ObjectWriter SELF_WRITER = new ObjectMapper() + ObjectWriter SELF_WRITER = new ObjectMapper( + new JsonFactoryBuilder().quoteChar('\'').build() + ) + .registerModule(new Jdk8Module()) .writer(PyishPrettyPrinter.INSTANCE) .with(PyishCharacterEscapes.INSTANCE); + + /** + * 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. + *

    + * @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(writeValueAsString(this)); + } + /** - * 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 + * Allows for a class to specify how its pyish string representation will + * be written to the json generator. + *

    + * 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. */ - default String toPyishString() { - return writeValueAsString(this); + default void writePyishSelf( + JsonGenerator jsonGenerator, + SerializerProvider serializerProvider + ) throws IOException { + AtomicInteger remainingLength = (AtomicInteger) serializerProvider.getAttribute( + LengthLimitingWriter.REMAINING_LENGTH_ATTRIBUTE + ); + jsonGenerator.writeRawValue( + appendPyishString( + remainingLength == null + ? new StringBuilder() + : new LengthLimitingStringBuilder(remainingLength.get()) + ) + .toString() + ); } /** @@ -31,7 +75,10 @@ static String writeValueAsString(Object value) { try { return SELF_WRITER.writeValueAsString(value); } catch (JsonProcessingException e) { - return '"' + Objects.toString(value) + '"'; + if (e.getCause() instanceof DeferredValueException) { + throw (DeferredValueException) e.getCause(); + } + return '\'' + Objects.toString(value) + '\''; } } } 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..f4c7a9b6d 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializer.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishSerializer.java @@ -3,22 +3,23 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; +import com.google.common.annotations.Beta; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.io.IOException; import java.util.Objects; +@Beta public class PyishSerializer extends JsonSerializer { + public static final PyishSerializer INSTANCE = new PyishSerializer(); private PyishSerializer() {} - @Override public void serialize( Object object, JsonGenerator jsonGenerator, SerializerProvider serializerProvider - ) - throws IOException { + ) throws IOException { jsonGenerator.setPrettyPrinter(PyishPrettyPrinter.INSTANCE); jsonGenerator.setCharacterEscapes(PyishCharacterEscapes.INSTANCE); String string; @@ -27,7 +28,10 @@ public void serialize( .map(interpreter -> interpreter.wrap(object)) .orElse(object); if (wrappedObject instanceof PyishSerializable) { - jsonGenerator.writeRawValue(((PyishSerializable) wrappedObject).toPyishString()); + ((PyishSerializable) wrappedObject).writePyishSelf( + 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/random/DeferredRandomNumberGenerator.java b/src/main/java/com/hubspot/jinjava/random/DeferredRandomNumberGenerator.java index 4394a88e4..e0ebedda7 100644 --- a/src/main/java/com/hubspot/jinjava/random/DeferredRandomNumberGenerator.java +++ b/src/main/java/com/hubspot/jinjava/random/DeferredRandomNumberGenerator.java @@ -1,6 +1,6 @@ package com.hubspot.jinjava.random; -import com.hubspot.jinjava.el.ext.DeferredParsingException; +import com.hubspot.jinjava.interpret.DeferredValueException; import java.util.Random; import java.util.stream.DoubleStream; import java.util.stream.IntStream; @@ -10,46 +10,47 @@ * A random number generator that throws {@link com.hubspot.jinjava.interpret.DeferredValueException} for all supported methods. */ public class DeferredRandomNumberGenerator extends Random { + private static final String EXCEPTION_MESSAGE = "Generating random number"; @Override protected int next(int bits) { - throw new DeferredParsingException(EXCEPTION_MESSAGE); + throw new DeferredValueException(EXCEPTION_MESSAGE); } @Override public int nextInt() { - throw new DeferredParsingException(EXCEPTION_MESSAGE); + throw new DeferredValueException(EXCEPTION_MESSAGE); } @Override public int nextInt(int bound) { - throw new DeferredParsingException(EXCEPTION_MESSAGE); + throw new DeferredValueException(EXCEPTION_MESSAGE); } @Override public long nextLong() { - throw new DeferredParsingException(EXCEPTION_MESSAGE); + throw new DeferredValueException(EXCEPTION_MESSAGE); } @Override public boolean nextBoolean() { - throw new DeferredParsingException(EXCEPTION_MESSAGE); + throw new DeferredValueException(EXCEPTION_MESSAGE); } @Override public float nextFloat() { - throw new DeferredParsingException(EXCEPTION_MESSAGE); + throw new DeferredValueException(EXCEPTION_MESSAGE); } @Override public double nextDouble() { - throw new DeferredParsingException(EXCEPTION_MESSAGE); + throw new DeferredValueException(EXCEPTION_MESSAGE); } @Override public synchronized double nextGaussian() { - throw new DeferredParsingException(EXCEPTION_MESSAGE); + throw new DeferredValueException(EXCEPTION_MESSAGE); } @Override diff --git a/src/main/java/com/hubspot/jinjava/random/RandomNumberGeneratorStrategy.java b/src/main/java/com/hubspot/jinjava/random/RandomNumberGeneratorStrategy.java index 29912dd09..bcb0ec9fc 100644 --- a/src/main/java/com/hubspot/jinjava/random/RandomNumberGeneratorStrategy.java +++ b/src/main/java/com/hubspot/jinjava/random/RandomNumberGeneratorStrategy.java @@ -3,5 +3,5 @@ public enum RandomNumberGeneratorStrategy { THREAD_LOCAL, CONSTANT_ZERO, - DEFERRED + DEFERRED, } diff --git a/src/main/java/com/hubspot/jinjava/tree/ExpressionNode.java b/src/main/java/com/hubspot/jinjava/tree/ExpressionNode.java index aac897c4c..ecaaef954 100644 --- a/src/main/java/com/hubspot/jinjava/tree/ExpressionNode.java +++ b/src/main/java/com/hubspot/jinjava/tree/ExpressionNode.java @@ -24,6 +24,7 @@ import com.hubspot.jinjava.tree.parse.ExpressionToken; public class ExpressionNode extends Node { + private static final long serialVersionUID = -6063173739682221042L; private final ExpressionStrategy expressionStrategy; @@ -47,9 +48,10 @@ public OutputNode render(JinjavaInterpreter interpreter) { try { return expressionStrategy.interpretOutput(master, interpreter); } catch (DeferredValueException e) { - checkForInterrupt(); interpreter.getContext().handleDeferredNode(this); return new RenderedOutputNode(master.getImage()); + } finally { + postProcess(interpreter); } } diff --git a/src/main/java/com/hubspot/jinjava/tree/Node.java b/src/main/java/com/hubspot/jinjava/tree/Node.java index da39f4852..56a8817f4 100644 --- a/src/main/java/com/hubspot/jinjava/tree/Node.java +++ b/src/main/java/com/hubspot/jinjava/tree/Node.java @@ -15,7 +15,7 @@ **********************************************************************/ package com.hubspot.jinjava.tree; -import com.hubspot.jinjava.interpret.InterpretException; +import com.hubspot.jinjava.el.JinjavaProcessors; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.tree.output.OutputNode; import com.hubspot.jinjava.tree.parse.Token; @@ -25,6 +25,7 @@ import org.apache.commons.lang3.StringUtils; public abstract class Node implements Serializable { + private static final long serialVersionUID = -6194634312533310816L; private final Token master; @@ -93,24 +94,23 @@ public String toTreeString(int level) { } if (getChildren().size() > 0) { - t.append(prefix).append("end :: ").append(toString()).append('\n'); + t.append(prefix).append("end :: ").append(this).append('\n'); } return t.toString(); } public void preProcess(JinjavaInterpreter interpreter) { - interpreter.getContext().setCurrentNode(this); - checkForInterrupt(); + JinjavaProcessors processors = interpreter.getConfig().getProcessors(); + if (processors != null && processors.getNodePreProcessor() != null) { + processors.getNodePreProcessor().accept(this, interpreter); + } } - public final void checkForInterrupt() { - if (Thread.currentThread().isInterrupted()) { - throw new InterpretException( - "Interrupt rendering " + getClass(), - master.getLineNumber(), - master.getStartPosition() - ); + public void postProcess(JinjavaInterpreter interpreter) { + JinjavaProcessors processors = interpreter.getConfig().getProcessors(); + if (processors != null && processors.getNodePostProcessor() != null) { + processors.getNodePostProcessor().accept(this, interpreter); } } } diff --git a/src/main/java/com/hubspot/jinjava/tree/RootNode.java b/src/main/java/com/hubspot/jinjava/tree/RootNode.java index 9db3bd0bb..7cf34432a 100644 --- a/src/main/java/com/hubspot/jinjava/tree/RootNode.java +++ b/src/main/java/com/hubspot/jinjava/tree/RootNode.java @@ -20,6 +20,7 @@ import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; public class RootNode extends Node { + private static final long serialVersionUID = 5904181260202954424L; private final TokenScannerSymbols symbols; diff --git a/src/main/java/com/hubspot/jinjava/tree/TagNode.java b/src/main/java/com/hubspot/jinjava/tree/TagNode.java index 4402bfe38..495064a0b 100644 --- a/src/main/java/com/hubspot/jinjava/tree/TagNode.java +++ b/src/main/java/com/hubspot/jinjava/tree/TagNode.java @@ -29,6 +29,7 @@ import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; public class TagNode extends Node { + private static final long serialVersionUID = -6971280448795354252L; private final Tag tag; @@ -45,18 +46,17 @@ public TagNode(Tag tag, TagToken token, TokenScannerSymbols symbols) { @Override public OutputNode render(JinjavaInterpreter interpreter) { preProcess(interpreter); - if ( - interpreter.getContext().isValidationMode() && !tag.isRenderedInValidationMode() - ) { - return new RenderedOutputNode(""); - } try { + if ( + interpreter.getContext().isValidationMode() && !tag.isRenderedInValidationMode() + ) { + return new RenderedOutputNode(""); + } if (interpreter.getConfig().getExecutionMode().useEagerParser()) { interpreter.getContext().checkNumberOfDeferredTokens(); } return tag.interpretOutput(this, interpreter); } catch (DeferredValueException e) { - checkForInterrupt(); interpreter.getContext().handleDeferredNode(this); return new RenderedOutputNode(reconstructImage()); } catch ( @@ -73,6 +73,8 @@ public OutputNode render(JinjavaInterpreter interpreter) { master.getLineNumber(), master.getStartPosition() ); + } finally { + postProcess(interpreter); } } diff --git a/src/main/java/com/hubspot/jinjava/tree/TextNode.java b/src/main/java/com/hubspot/jinjava/tree/TextNode.java index 4dee39892..64f572e2a 100644 --- a/src/main/java/com/hubspot/jinjava/tree/TextNode.java +++ b/src/main/java/com/hubspot/jinjava/tree/TextNode.java @@ -21,6 +21,7 @@ import com.hubspot.jinjava.tree.parse.TextToken; public class TextNode extends Node { + private static final long serialVersionUID = 127827773323298439L; private final TextToken master; @@ -33,9 +34,13 @@ public TextNode(TextToken token) { @Override public OutputNode render(JinjavaInterpreter interpreter) { preProcess(interpreter); - return new RenderedOutputNode( - interpreter.getContext().isValidationMode() ? "" : master.output() - ); + try { + return new RenderedOutputNode( + interpreter.getContext().isValidationMode() ? "" : master.output() + ); + } finally { + postProcess(interpreter); + } } @Override diff --git a/src/main/java/com/hubspot/jinjava/tree/TreeParser.java b/src/main/java/com/hubspot/jinjava/tree/TreeParser.java index 406e1b76a..56f11003c 100644 --- a/src/main/java/com/hubspot/jinjava/tree/TreeParser.java +++ b/src/main/java/com/hubspot/jinjava/tree/TreeParser.java @@ -17,6 +17,7 @@ import com.google.common.collect.Iterators; import com.google.common.collect.PeekingIterator; +import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.DisabledException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.MissingEndTagException; @@ -36,12 +37,16 @@ import com.hubspot.jinjava.tree.parse.Token; import com.hubspot.jinjava.tree.parse.TokenScanner; import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; +import com.hubspot.jinjava.tree.parse.UnclosedToken; +import com.hubspot.jinjava.tree.parse.WhitespaceControlParser; import org.apache.commons.lang3.StringUtils; public class TreeParser { + private final PeekingIterator scanner; private final JinjavaInterpreter interpreter; private final TokenScannerSymbols symbols; + private final WhitespaceControlParser whitespaceControlParser; private Node parent; @@ -50,6 +55,10 @@ public TreeParser(JinjavaInterpreter interpreter, String input) { Iterators.peekingIterator(new TokenScanner(input, interpreter.getConfig())); this.interpreter = interpreter; this.symbols = interpreter.getConfig().getTokenScannerSymbols(); + this.whitespaceControlParser = + interpreter.getConfig().getLegacyOverrides().isParseWhitespaceControlStrictly() + ? WhitespaceControlParser.STRICT + : WhitespaceControlParser.LENIENT; } public Node buildTree() { @@ -61,9 +70,15 @@ public Node buildTree() { Node node = nextNode(); if (node != null) { - if (node instanceof TextNode && getLastSibling() instanceof TextNode) { + if ( + node instanceof TextNode && + getLastSibling() instanceof TextNode && + !interpreter.getConfig().getLegacyOverrides().isAllowAdjacentTextNodes() + ) { // merge adjacent text nodes so whitespace control properly applies - getLastSibling().getMaster().mergeImageAndContent(node.getMaster()); + ((TextToken) getLastSibling().getMaster()).mergeImageAndContent( + (TextToken) node.getMaster() + ); } else { parent.getChildren().add(node); } @@ -95,8 +110,28 @@ public Node buildTree() { private Node nextNode() { Token token = scanner.next(); + if (token.isLeftTrim() && isTrimmingEnabledForToken(token, interpreter.getConfig())) { + final Node lastSibling = getLastSibling(); + if (lastSibling instanceof TextNode) { + lastSibling.getMaster().setRightTrim(true); + } + } if (token.getType() == symbols.getFixed()) { + if (token instanceof UnclosedToken) { + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.SYNTAX_ERROR, + ErrorItem.TAG, + "Unclosed token", + "token", + token.getLineNumber(), + token.getStartPosition(), + null + ) + ); + } return text((TextToken) token); } else if (token.getType() == symbols.getExprStart()) { return expression((ExpressionToken) token); @@ -141,21 +176,29 @@ private Node getLastSibling() { private Node text(TextToken textToken) { if (interpreter.getConfig().isLstripBlocks()) { - if (scanner.hasNext() && scanner.peek().getType() == symbols.getTag()) { - textToken = - new TextToken( - StringUtils.stripEnd(textToken.getImage(), "\t "), - textToken.getLineNumber(), - textToken.getStartPosition(), - symbols - ); + if (scanner.hasNext()) { + final int nextTokenType = scanner.peek().getType(); + if (nextTokenType == symbols.getTag() || nextTokenType == symbols.getNote()) { + textToken = + new TextToken( + StringUtils.stripEnd(textToken.getImage(), "\t "), + textToken.getLineNumber(), + textToken.getStartPosition(), + symbols, + whitespaceControlParser + ); + } } } final Node lastSibling = getLastSibling(); // if last sibling was a tag and has rightTrimAfterEnd, strip whitespace - if (lastSibling instanceof TagNode && isRightTrim((TagNode) lastSibling)) { + if ( + lastSibling != null && + isRightTrim(lastSibling) && + isTrimmingEnabledForToken(lastSibling.getMaster(), interpreter.getConfig()) + ) { textToken.setLeftTrim(true); } @@ -171,18 +214,19 @@ private Node text(TextToken textToken) { return n; } - private boolean isRightTrim(TagNode lastSibling) { - return ( - lastSibling.getEndName() == null || - ( - lastSibling.getTag() instanceof FlexibleTag && - !((FlexibleTag) lastSibling.getTag()).hasEndTag( - (TagToken) lastSibling.getMaster() - ) + private boolean isRightTrim(Node lastSibling) { + if (lastSibling instanceof TagNode) { + return ( + ((TagNode) lastSibling).getEndName() == null || + (((TagNode) lastSibling).getTag() instanceof FlexibleTag && + !((FlexibleTag) ((TagNode) lastSibling).getTag()).hasEndTag( + (TagToken) lastSibling.getMaster() + )) ) - ) - ? lastSibling.getMaster().isRightTrim() - : lastSibling.getMaster().isRightTrimAfterEnd(); + ? lastSibling.getMaster().isRightTrim() + : lastSibling.getMaster().isRightTrimAfterEnd(); + } + return lastSibling.getMaster().isRightTrim(); } private Node expression(ExpressionToken expressionToken) { @@ -227,14 +271,6 @@ private Node tag(TagToken tagToken) { if (tag instanceof EndTag) { endTag(tag, tagToken); return null; - } else { - // if a tag has left trim, mark the last sibling to trim right whitespace - if (tagToken.isLeftTrim()) { - final Node lastSibling = getLastSibling(); - if (lastSibling instanceof TextNode) { - lastSibling.getMaster().setRightTrim(true); - } - } } TagNode node = new TagNode(tag, tagToken, symbols); @@ -253,16 +289,6 @@ private Node tag(TagToken tagToken) { } private void endTag(Tag tag, TagToken tagToken) { - final Node lastSibling = getLastSibling(); - - if ( - parent instanceof TagNode && - tagToken.isLeftTrim() && - lastSibling instanceof TextNode - ) { - lastSibling.getMaster().setRightTrim(true); - } - if (parent.getMaster() != null) { // root node parent.getMaster().setRightTrimAfterEnd(tagToken.isRightTrim()); } @@ -303,4 +329,11 @@ private void endTag(Tag tag, TagToken tagToken) { ); } } + + private boolean isTrimmingEnabledForToken(Token token, JinjavaConfig jinjavaConfig) { + if (token instanceof TagToken || token instanceof TextToken) { + return true; + } + return jinjavaConfig.getLegacyOverrides().isUseTrimmingForNotesAndExpressions(); + } } diff --git a/src/main/java/com/hubspot/jinjava/tree/output/BlockInfo.java b/src/main/java/com/hubspot/jinjava/tree/output/BlockInfo.java index e82d48ca7..8af0914e7 100644 --- a/src/main/java/com/hubspot/jinjava/tree/output/BlockInfo.java +++ b/src/main/java/com/hubspot/jinjava/tree/output/BlockInfo.java @@ -6,6 +6,7 @@ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") public class BlockInfo { + private final List nodes; private final Optional parentPath; diff --git a/src/main/java/com/hubspot/jinjava/tree/output/BlockPlaceholderOutputNode.java b/src/main/java/com/hubspot/jinjava/tree/output/BlockPlaceholderOutputNode.java index a9cb694d9..c2801ab3a 100644 --- a/src/main/java/com/hubspot/jinjava/tree/output/BlockPlaceholderOutputNode.java +++ b/src/main/java/com/hubspot/jinjava/tree/output/BlockPlaceholderOutputNode.java @@ -4,6 +4,7 @@ import java.nio.charset.Charset; public class BlockPlaceholderOutputNode implements OutputNode { + private final String blockName; private String output; diff --git a/src/main/java/com/hubspot/jinjava/tree/output/DynamicRenderedOutputNode.java b/src/main/java/com/hubspot/jinjava/tree/output/DynamicRenderedOutputNode.java new file mode 100644 index 000000000..9967067b2 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/tree/output/DynamicRenderedOutputNode.java @@ -0,0 +1,33 @@ +package com.hubspot.jinjava.tree.output; + +import com.google.common.base.Charsets; +import java.nio.charset.Charset; + +/** + * An OutputNode that can be modified after already being added to the OutputList + */ +public class DynamicRenderedOutputNode implements OutputNode { + + protected String output = ""; + + public void setValue(String output) { + this.output = output; + } + + @Override + public String getValue() { + return output; + } + + @Override + public long getSize() { + return output == null + ? 0 + : output.getBytes(Charset.forName(Charsets.UTF_8.name())).length; + } + + @Override + public String toString() { + return getValue(); + } +} diff --git a/src/main/java/com/hubspot/jinjava/tree/output/OutputList.java b/src/main/java/com/hubspot/jinjava/tree/output/OutputList.java index 738e9062c..1d3b84710 100644 --- a/src/main/java/com/hubspot/jinjava/tree/output/OutputList.java +++ b/src/main/java/com/hubspot/jinjava/tree/output/OutputList.java @@ -1,13 +1,18 @@ package com.hubspot.jinjava.tree.output; +import com.hubspot.jinjava.features.BuiltInFeatures; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.interpret.TemplateError; +import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; import com.hubspot.jinjava.util.LengthLimitingStringBuilder; import java.util.LinkedList; import java.util.List; public class OutputList { + + public static final String PREVENT_ACCIDENTAL_EXPRESSIONS = + BuiltInFeatures.PREVENT_ACCIDENTAL_EXPRESSIONS; private final List nodes = new LinkedList<>(); private final List blocks = new LinkedList<>(); private final long maxOutputSize; @@ -48,6 +53,71 @@ public List getBlocks() { public String getValue() { LengthLimitingStringBuilder val = new LengthLimitingStringBuilder(maxOutputSize); + return JinjavaInterpreter + .getCurrentMaybe() + .map(JinjavaInterpreter::getConfig) + .filter(config -> + config + .getFeatures() + .getActivationStrategy(BuiltInFeatures.PREVENT_ACCIDENTAL_EXPRESSIONS) + .isActive(null) + ) + .map(config -> + joinNodesWithoutAddingExpressions(val, config.getTokenScannerSymbols()) + ) + .orElseGet(() -> joinNodes(val)); + } + + private String joinNodesWithoutAddingExpressions( + LengthLimitingStringBuilder val, + TokenScannerSymbols tokenScannerSymbols + ) { + String separator = getWhitespaceSeparator(tokenScannerSymbols); + String prev = null; + String cur; + for (OutputNode node : nodes) { + try { + cur = node.getValue(); + if ( + prev != null && + prev.length() > 0 && + prev.charAt(prev.length() - 1) == tokenScannerSymbols.getExprStartChar() + ) { + if ( + cur.length() > 0 && + TokenScannerSymbols.isNoteTagOrExprChar(tokenScannerSymbols, cur.charAt(0)) + ) { + val.append(separator); + } + } + prev = cur; + val.append(node.getValue()); + } catch (OutputTooBigException e) { + JinjavaInterpreter + .getCurrent() + .addError(TemplateError.fromOutputTooBigException(e)); + return val.toString(); + } + } + + return val.toString(); + } + + private static String getWhitespaceSeparator(TokenScannerSymbols tokenScannerSymbols) { + @SuppressWarnings("StringBufferReplaceableByString") + String separator = new StringBuilder() + .append('\n') + .append(tokenScannerSymbols.getPrefixChar()) + .append(tokenScannerSymbols.getNoteChar()) + .append(tokenScannerSymbols.getTrimChar()) + .append(' ') + .append(tokenScannerSymbols.getNoteChar()) + .append(tokenScannerSymbols.getExprEndChar()) + .toString(); + return separator; + } + + private String joinNodes(LengthLimitingStringBuilder val) { for (OutputNode node : nodes) { try { val.append(node.getValue()); diff --git a/src/main/java/com/hubspot/jinjava/tree/output/RenderedOutputNode.java b/src/main/java/com/hubspot/jinjava/tree/output/RenderedOutputNode.java index f85ad4bc6..7c97ddf7d 100644 --- a/src/main/java/com/hubspot/jinjava/tree/output/RenderedOutputNode.java +++ b/src/main/java/com/hubspot/jinjava/tree/output/RenderedOutputNode.java @@ -4,6 +4,7 @@ import java.nio.charset.Charset; public class RenderedOutputNode implements OutputNode { + private final String output; public RenderedOutputNode(String output) { diff --git a/src/main/java/com/hubspot/jinjava/tree/parse/DefaultTokenScannerSymbols.java b/src/main/java/com/hubspot/jinjava/tree/parse/DefaultTokenScannerSymbols.java index 79a79913e..e0ca20e20 100644 --- a/src/main/java/com/hubspot/jinjava/tree/parse/DefaultTokenScannerSymbols.java +++ b/src/main/java/com/hubspot/jinjava/tree/parse/DefaultTokenScannerSymbols.java @@ -1,6 +1,7 @@ package com.hubspot.jinjava.tree.parse; public class DefaultTokenScannerSymbols extends TokenScannerSymbols { + private static final long serialVersionUID = 3825893609777542598L; char TOKEN_PREFIX_CHAR = '{'; diff --git a/src/main/java/com/hubspot/jinjava/tree/parse/ExpressionToken.java b/src/main/java/com/hubspot/jinjava/tree/parse/ExpressionToken.java index 3379e8049..d8d9996d5 100644 --- a/src/main/java/com/hubspot/jinjava/tree/parse/ExpressionToken.java +++ b/src/main/java/com/hubspot/jinjava/tree/parse/ExpressionToken.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils; public class ExpressionToken extends Token { + private static final long serialVersionUID = 6336768632140743908L; private String expr; @@ -28,7 +29,17 @@ public ExpressionToken( int startPosition, TokenScannerSymbols symbols ) { - super(image, lineNumber, startPosition, symbols); + this(image, lineNumber, startPosition, symbols, WhitespaceControlParser.LENIENT); + } + + public ExpressionToken( + String image, + int lineNumber, + int startPosition, + TokenScannerSymbols symbols, + WhitespaceControlParser whitespaceControlParser + ) { + super(image, lineNumber, startPosition, symbols, whitespaceControlParser); } @Override diff --git a/src/main/java/com/hubspot/jinjava/tree/parse/NoteToken.java b/src/main/java/com/hubspot/jinjava/tree/parse/NoteToken.java index fe98c4dda..3f5360e67 100644 --- a/src/main/java/com/hubspot/jinjava/tree/parse/NoteToken.java +++ b/src/main/java/com/hubspot/jinjava/tree/parse/NoteToken.java @@ -16,6 +16,7 @@ package com.hubspot.jinjava.tree.parse; public class NoteToken extends Token { + private static final long serialVersionUID = -3859011447900311329L; public NoteToken( @@ -24,7 +25,17 @@ public NoteToken( int startPosition, TokenScannerSymbols symbols ) { - super(image, lineNumber, startPosition, symbols); + this(image, lineNumber, startPosition, symbols, WhitespaceControlParser.LENIENT); + } + + public NoteToken( + String image, + int lineNumber, + int startPosition, + TokenScannerSymbols symbols, + WhitespaceControlParser whitespaceControlParser + ) { + super(image, lineNumber, startPosition, symbols, whitespaceControlParser); } @Override @@ -37,6 +48,9 @@ public int getType() { */ @Override protected void parse() { + if (image.length() > 4) { // {# #} + handleTrim(image.substring(2, image.length() - 2)); + } content = ""; } diff --git a/src/main/java/com/hubspot/jinjava/tree/parse/TagToken.java b/src/main/java/com/hubspot/jinjava/tree/parse/TagToken.java index d73c33b22..a737dd96c 100644 --- a/src/main/java/com/hubspot/jinjava/tree/parse/TagToken.java +++ b/src/main/java/com/hubspot/jinjava/tree/parse/TagToken.java @@ -18,6 +18,7 @@ import com.hubspot.jinjava.interpret.TemplateSyntaxException; public class TagToken extends Token { + private static final long serialVersionUID = -4927751270481832992L; private String tagName; @@ -30,7 +31,17 @@ public TagToken( int startPosition, TokenScannerSymbols symbols ) { - super(image, lineNumber, startPosition, symbols); + this(image, lineNumber, startPosition, symbols, WhitespaceControlParser.LENIENT); + } + + public TagToken( + String image, + int lineNumber, + int startPosition, + TokenScannerSymbols symbols, + WhitespaceControlParser whitespaceControlParser + ) { + super(image, lineNumber, startPosition, symbols, whitespaceControlParser); } @Override diff --git a/src/main/java/com/hubspot/jinjava/tree/parse/TextToken.java b/src/main/java/com/hubspot/jinjava/tree/parse/TextToken.java index dc3d64b42..23288c5dc 100644 --- a/src/main/java/com/hubspot/jinjava/tree/parse/TextToken.java +++ b/src/main/java/com/hubspot/jinjava/tree/parse/TextToken.java @@ -18,6 +18,7 @@ import org.apache.commons.lang3.StringUtils; public class TextToken extends Token { + private static final long serialVersionUID = -6168990984496468543L; public TextToken( @@ -26,7 +27,24 @@ public TextToken( int startPosition, TokenScannerSymbols symbols ) { - super(image, lineNumber, startPosition, symbols); + this(image, lineNumber, startPosition, symbols, WhitespaceControlParser.LENIENT); + } + + public TextToken( + String image, + int lineNumber, + int startPosition, + TokenScannerSymbols symbols, + WhitespaceControlParser whitespaceControlParser + ) { + super(image, lineNumber, startPosition, symbols, whitespaceControlParser); + } + + public void mergeImageAndContent(TextToken otherToken) { + String thisOutput = output(); + String otherTokenOutput = otherToken.output(); + this.image = thisOutput + otherTokenOutput; + this.content = image; } @Override diff --git a/src/main/java/com/hubspot/jinjava/tree/parse/Token.java b/src/main/java/com/hubspot/jinjava/tree/parse/Token.java index dda5bd63b..ee526f5bb 100644 --- a/src/main/java/com/hubspot/jinjava/tree/parse/Token.java +++ b/src/main/java/com/hubspot/jinjava/tree/parse/Token.java @@ -15,13 +15,11 @@ **********************************************************************/ package com.hubspot.jinjava.tree.parse; -import com.hubspot.jinjava.JinjavaConfig; -import com.hubspot.jinjava.LegacyOverrides; -import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.UnexpectedTokenException; import java.io.Serializable; public abstract class Token implements Serializable { + private static final long serialVersionUID = 3359084948763661809L; protected String image; @@ -31,6 +29,7 @@ public abstract class Token implements Serializable { protected final int lineNumber; protected final int startPosition; private final TokenScannerSymbols symbols; + private final WhitespaceControlParser whitespaceControlParser; private boolean leftTrim; private boolean rightTrim; @@ -40,12 +39,14 @@ public Token( String image, int lineNumber, int startPosition, - TokenScannerSymbols symbols + TokenScannerSymbols symbols, + WhitespaceControlParser whitespaceControlParser ) { this.image = image; this.lineNumber = lineNumber; this.startPosition = startPosition; this.symbols = symbols; + this.whitespaceControlParser = whitespaceControlParser; parse(); } @@ -53,11 +54,6 @@ public String getImage() { return image; } - public void mergeImageAndContent(Token otherToken) { - this.image = image + otherToken.image; - this.content = content + otherToken.content; - } - public int getLineNumber() { return lineNumber; } @@ -93,25 +89,14 @@ public void setRightTrimAfterEnd(boolean rightTrimAfterEnd) { * @return the content stripped of any whitespace control characters. */ protected final String handleTrim(String unwrapped) { - boolean parseWhitespaceControlStrictly = JinjavaInterpreter - .getCurrentMaybe() - .map(JinjavaInterpreter::getConfig) - .map(JinjavaConfig::getLegacyOverrides) - .map(LegacyOverrides::isParseWhitespaceControlStrictly) - .orElse(false); - - WhitespaceControlParser parser = parseWhitespaceControlStrictly - ? WhitespaceControlParser.STRICT - : WhitespaceControlParser.LENIENT; - String result = unwrapped; - if (parser.hasLeftTrim(result)) { + if (whitespaceControlParser.hasLeftTrim(result)) { setLeftTrim(true); - result = parser.stripLeft(result); + result = whitespaceControlParser.stripLeft(result); } - if (parser.hasRightTrim(result)) { + if (whitespaceControlParser.hasRightTrim(result)) { setRightTrim(true); - result = parser.stripRight(result); + result = whitespaceControlParser.stripRight(result); } return result; } @@ -136,18 +121,43 @@ public String toString() { static Token newToken( int tokenKind, TokenScannerSymbols symbols, + WhitespaceControlParser whitespaceControlParser, String image, int lineNumber, int startPosition ) { if (tokenKind == symbols.getFixed()) { - return new TextToken(image, lineNumber, startPosition, symbols); + return new TextToken( + image, + lineNumber, + startPosition, + symbols, + whitespaceControlParser + ); } else if (tokenKind == symbols.getNote()) { - return new NoteToken(image, lineNumber, startPosition, symbols); + return new NoteToken( + image, + lineNumber, + startPosition, + symbols, + whitespaceControlParser + ); } else if (tokenKind == symbols.getExprStart()) { - return new ExpressionToken(image, lineNumber, startPosition, symbols); + return new ExpressionToken( + image, + lineNumber, + startPosition, + symbols, + whitespaceControlParser + ); } else if (tokenKind == symbols.getTag()) { - return new TagToken(image, lineNumber, startPosition, symbols); + return new TagToken( + image, + lineNumber, + startPosition, + symbols, + whitespaceControlParser + ); } else { throw new UnexpectedTokenException( String.valueOf((char) tokenKind), diff --git a/src/main/java/com/hubspot/jinjava/tree/parse/TokenScanner.java b/src/main/java/com/hubspot/jinjava/tree/parse/TokenScanner.java index 78d3ee2cc..7e53b295a 100644 --- a/src/main/java/com/hubspot/jinjava/tree/parse/TokenScanner.java +++ b/src/main/java/com/hubspot/jinjava/tree/parse/TokenScanner.java @@ -19,8 +19,10 @@ import com.google.common.collect.AbstractIterator; import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.features.BuiltInFeatures; public class TokenScanner extends AbstractIterator { + private final JinjavaConfig config; private final char[] is; @@ -37,7 +39,8 @@ public class TokenScanner extends AbstractIterator { private char inQuote = 0; private int currLine = 1; private int lastNewlinePos = 0; - private TokenScannerSymbols symbols; + private final TokenScannerSymbols symbols; + private final WhitespaceControlParser whitespaceControlParser; public TokenScanner(String input, JinjavaConfig config) { this.config = config; @@ -57,6 +60,10 @@ public TokenScanner(String input, JinjavaConfig config) { lastNewlinePos = 0; symbols = config.getTokenScannerSymbols(); + whitespaceControlParser = + config.getLegacyOverrides().isParseWhitespaceControlStrictly() + ? WhitespaceControlParser.STRICT + : WhitespaceControlParser.LENIENT; } private Token getNextToken() { @@ -68,17 +75,15 @@ private Token getNextToken() { } if (inBlock > 0) { - if (inQuote != 0) { + if (c == '\\') { + ++currPost; + continue; + } else if (inQuote != 0) { if (inQuote == c) { inQuote = 0; - continue; - } else if (c == '\\') { - ++currPost; - continue; - } else { - continue; } - } else if (inQuote == 0 && (c == '\'' || c == '"')) { + continue; + } else if (c == '\'' || c == '"') { inQuote = c; continue; } @@ -89,7 +94,11 @@ private Token getNextToken() { if (currPost < length) { c = is[currPost]; boolean startTokenFound = true; - if (config.getLegacyOverrides().isWhitespaceRequiredWithinTokens()) { + if ( + config + .getFeatures() + .isActive(BuiltInFeatures.WHITESPACE_REQUIRED_WITHIN_TOKENS) + ) { boolean hasNextChar = (currPost + 1) < length; boolean nextCharIsWhitespace = hasNextChar && (' ' == is[currPost + 1]); startTokenFound = nextCharIsWhitespace; @@ -228,10 +237,19 @@ private Token getEndToken() { int type = symbols.getFixed(); if (inComment > 0) { type = symbols.getNote(); + } else if (inBlock > 0) { + return new UnclosedToken( + String.valueOf(is, tokenStart, tokenLength), + currLine, + tokenStart - lastNewlinePos + 1, + symbols, + whitespaceControlParser + ); } return Token.newToken( type, symbols, + whitespaceControlParser, String.valueOf(is, tokenStart, tokenLength), currLine, tokenStart - lastNewlinePos + 1 @@ -242,18 +260,24 @@ private Token newToken(int kind) { Token t = Token.newToken( kind, symbols, + whitespaceControlParser, String.valueOf(is, lastStart, tokenLength), currLine, lastStart - lastNewlinePos + 1 ); - if (t instanceof TagToken) { - if (config.isTrimBlocks() && currPost < length && is[currPost] == '\n') { - lastNewlinePos = currPost; - ++currPost; - ++tokenStart; - } + if ( + (t instanceof TagToken || t instanceof NoteToken) && + config.isTrimBlocks() && + currPost < length && + is[currPost] == '\n' + ) { + lastNewlinePos = currPost; + ++currPost; + ++tokenStart; + } + if (t instanceof TagToken) { TagToken tt = (TagToken) t; if ("raw".equals(tt.getTagName())) { inRaw = 1; @@ -265,7 +289,14 @@ private Token newToken(int kind) { } if (inRaw > 0 && t.getType() != symbols.getFixed()) { - return Token.newToken(symbols.getFixed(), symbols, t.image, currLine, tokenStart); + return Token.newToken( + symbols.getFixed(), + symbols, + whitespaceControlParser, + t.image, + currLine, + tokenStart + ); } return t; diff --git a/src/main/java/com/hubspot/jinjava/tree/parse/TokenScannerSymbols.java b/src/main/java/com/hubspot/jinjava/tree/parse/TokenScannerSymbols.java index dc046a767..771dbda41 100644 --- a/src/main/java/com/hubspot/jinjava/tree/parse/TokenScannerSymbols.java +++ b/src/main/java/com/hubspot/jinjava/tree/parse/TokenScannerSymbols.java @@ -18,10 +18,12 @@ import java.io.Serializable; public abstract class TokenScannerSymbols implements Serializable { + private static final long serialVersionUID = -4810220023023256534L; private String expressionStart = null; private String expressionStartWithTag = null; + private String openingComment = null; private String closingComment = null; private String expressionEnd = null; private String expressionEndWithTag = null; @@ -108,10 +110,23 @@ public String getExpressionEndWithTag() { return expressionEndWithTag; } + public String getOpeningComment() { + if (openingComment == null) { + openingComment = String.valueOf(getPrefixChar()) + getNoteChar(); + } + return openingComment; + } + public String getClosingComment() { if (closingComment == null) { closingComment = String.valueOf(getNoteChar()) + getPostfixChar(); } return closingComment; } + + public static boolean isNoteTagOrExprChar(TokenScannerSymbols symbols, char c) { + return ( + c == symbols.getNote() || c == symbols.getTag() || c == symbols.getExprStartChar() + ); + } } diff --git a/src/main/java/com/hubspot/jinjava/tree/parse/UnclosedToken.java b/src/main/java/com/hubspot/jinjava/tree/parse/UnclosedToken.java new file mode 100644 index 000000000..76e5ca1b4 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/tree/parse/UnclosedToken.java @@ -0,0 +1,23 @@ +package com.hubspot.jinjava.tree.parse; + +public class UnclosedToken extends TextToken { + + public UnclosedToken( + String image, + int lineNumber, + int startPosition, + TokenScannerSymbols symbols + ) { + this(image, lineNumber, startPosition, symbols, WhitespaceControlParser.LENIENT); + } + + public UnclosedToken( + String image, + int lineNumber, + int startPosition, + TokenScannerSymbols symbols, + WhitespaceControlParser whitespaceControlParser + ) { + super(image, lineNumber, startPosition, symbols, whitespaceControlParser); + } +} diff --git a/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java b/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java index 2fcbf2f7d..a2f11f8f9 100644 --- a/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/DeferredValueUtils.java @@ -3,22 +3,18 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; -import com.hubspot.jinjava.el.ext.AbstractCallableMethod; import com.hubspot.jinjava.interpret.Context; -import com.hubspot.jinjava.interpret.DeferredLazyReference; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.MetaContextVariables; +import com.hubspot.jinjava.interpret.PartiallyDeferredValue; import com.hubspot.jinjava.lib.tag.SetTag; -import com.hubspot.jinjava.lib.tag.eager.DeferredToken; import com.hubspot.jinjava.tree.ExpressionNode; import com.hubspot.jinjava.tree.Node; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.TextNode; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -29,9 +25,14 @@ import java.util.stream.Stream; public class DeferredValueUtils { + private static final String TEMPLATE_TAG_REGEX = "(\\w+(?:\\.\\w+)*)"; private static final Pattern TEMPLATE_TAG_PATTERN = Pattern.compile(TEMPLATE_TAG_REGEX); + public static boolean isFullyDeferred(Object obj) { + return obj instanceof DeferredValue && !(obj instanceof PartiallyDeferredValue); + } + public static HashMap getDeferredContextWithOriginalValues( Map context ) { @@ -46,21 +47,19 @@ public static HashMap getDeferredContextWithOriginalValues( Set keysToKeep ) { HashMap deferredContext = new HashMap<>(context.size()); - context.forEach( - (contextKey, contextItem) -> { - if (keysToKeep.size() > 0 && !keysToKeep.contains(contextKey)) { - return; - } - if (contextItem instanceof DeferredValue) { - if (((DeferredValue) contextItem).getOriginalValue() != null) { - deferredContext.put( - contextKey, - ((DeferredValue) contextItem).getOriginalValue() - ); - } + context.forEach((contextKey, contextItem) -> { + if (keysToKeep.size() > 0 && !keysToKeep.contains(contextKey)) { + return; + } + if (contextItem instanceof DeferredValue) { + if (((DeferredValue) contextItem).getOriginalValue() != null) { + deferredContext.put( + contextKey, + ((DeferredValue) contextItem).getOriginalValue() + ); } } - ); + }); return deferredContext; } @@ -80,93 +79,10 @@ public static void deferVariables(String[] varTokens, Map contex } } - public static Set findAndMarkDeferredProperties(Context context) { - return findAndMarkDeferredProperties(context, null); - } - - public static Set findAndMarkDeferredProperties( - Context context, - DeferredToken deferredToken - ) { - String templateSource = rebuildTemplateForNodes(context.getDeferredNodes()); + public static Set findAndMarkDeferredProperties(Context context, Node newNode) { + String templateSource = rebuildTemplateForNodes(newNode); Set deferredProps = getPropertiesUsedInDeferredNodes(context, templateSource); Set setProps = getPropertiesSetInDeferredNodes(templateSource); - Set referentialDefers = new HashSet<>(); - if (deferredToken != null) { - if ( - deferredToken.getMacroStack() == null || - deferredToken.getMacroStack() == context.getMacroStack() - ) { - deferredProps.addAll( - getPropertiesUsedInDeferredNodes( - context, - rebuildTemplateForEagerTagTokens(deferredToken, true), - false - ) - ); - referentialDefers.addAll( - getPropertiesUsedInDeferredNodes( - context, - rebuildTemplateForEagerTagTokens(deferredToken, false), - true - ) - ); - } else { - List macroArgs = deferredToken - .getMacroStack() - .peek() - .map( - name -> - Optional - .ofNullable(context.getGlobalMacro(name)) - .map(AbstractCallableMethod::getArguments) - .orElseGet( - () -> - context - .getLocalMacro(name) - .map(AbstractCallableMethod::getArguments) - .orElse(Collections.emptyList()) - ) - ) - .orElse(Collections.emptyList()); - // Filter out macro args because we will want them to be deferred on the higher-level contexts later - referentialDefers.addAll( - getPropertiesUsedInDeferredNodes( - context, - rebuildTemplateForEagerTagTokens(deferredToken, false), - true - ) - .stream() - .filter(prop -> !macroArgs.contains(prop)) - .collect(Collectors.toSet()) - ); - } - } - deferredProps.addAll(referentialDefers); - referentialDefers.forEach( - word -> { - Object wordValue = context.get(word); - if ( - !(wordValue instanceof DeferredValue) && - !EagerExpressionResolver.isPrimitive(wordValue) - ) { - Context temp = context; - while (temp.getParent() != null) { - temp - .getScope() - .entrySet() - .stream() - .filter(entry -> !entry.getKey().equals(word)) - .filter(entry -> entry.getValue() == wordValue) - .forEach( - entry -> entry.setValue(DeferredLazyReference.instance(context, word)) - ); - temp = temp.getParent(); - } - } - } - ); - markDeferredProperties(context, Sets.union(deferredProps, setProps)); return deferredProps; } @@ -175,8 +91,8 @@ public static Set getPropertiesSetInDeferredNodes(String templateSource) return findSetProperties(templateSource); } - public static Set getDeferredTags(Set deferredNodes) { - return getDeferredTags(new LinkedList<>(deferredNodes), 0); + public static Set getDeferredTagsRecursively(Node deferredNode) { + return getDeferredTags(deferredNode, 0); } public static Set getPropertiesUsedInDeferredNodes( @@ -204,52 +120,38 @@ private static void markDeferredProperties(Context context, Set props) { props .stream() .filter(prop -> !(context.get(prop) instanceof DeferredValue)) - .filter(prop -> !context.getMetaContextVariables().contains(prop)) - .forEach( - prop -> { - if (context.get(prop) != null) { - context.put(prop, DeferredValue.instance(context.get(prop))); - } else { - //Handle set props - context.put(prop, DeferredValue.instance()); - } + .filter(prop -> !MetaContextVariables.isMetaContextVariable(prop, context)) + .forEach(prop -> { + Object value = context.get(prop); + if (value != null) { + context.put(prop, DeferredValue.instance(value)); + } else { + //Handle set props + context.put(prop, DeferredValue.instance()); } - ); + }); } - private static Set getDeferredTags(List nodes, int depth) { + private static Set getDeferredTags(Node node, int depth) { // precaution - templates are parsed with this render depth so in theory the depth should never be exceeded - Set deferredTags = new HashSet<>(); + Set deferredTags = getDeferredTags(node).orElse(new HashSet<>()); int maxRenderDepth = JinjavaInterpreter.getCurrent() == null ? 3 : JinjavaInterpreter.getCurrent().getConfig().getMaxRenderDepth(); if (depth > maxRenderDepth) { return deferredTags; } - for (Node node : nodes) { - getDeferredTags(node).ifPresent(deferredTags::addAll); - deferredTags.addAll(getDeferredTags(node.getChildren(), depth + 1)); - } + node + .getChildren() + .forEach(child -> deferredTags.addAll(getDeferredTags(child, depth + 1))); return deferredTags; } - private static String rebuildTemplateForNodes(Set nodes) { - StringJoiner joiner = new StringJoiner(" "); - getDeferredTags(nodes).stream().map(DeferredTag::getTag).forEach(joiner::add); - return joiner.toString(); - } - - private static String rebuildTemplateForEagerTagTokens( - DeferredToken deferredToken, - boolean fromSetWords - ) { + private static String rebuildTemplateForNodes(Node node) { StringJoiner joiner = new StringJoiner(" "); - - ( - fromSetWords - ? deferredToken.getSetDeferredWords().stream() - : deferredToken.getUsedDeferredWords().stream() - ).map(h -> h + ".eager.helper") + getDeferredTagsRecursively(node) + .stream() + .map(DeferredTag::getTag) .forEach(joiner::add); return joiner.toString(); } @@ -311,6 +213,7 @@ private static String getNormalizedTag(Node node) { } private static class DeferredTag { + String tag; String normalizedTag; 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..334886152 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java @@ -0,0 +1,502 @@ +package com.hubspot.jinjava.util; + +import com.google.common.annotations.Beta; +import com.hubspot.jinjava.interpret.CannotReconstructValueException; +import com.hubspot.jinjava.interpret.Context; +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.MetaContextVariables; +import com.hubspot.jinjava.interpret.OneTimeReconstructible; +import com.hubspot.jinjava.interpret.RevertibleObject; +import com.hubspot.jinjava.lib.tag.ForTag; +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.Collections; +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; + +@Beta +public class EagerContextWatcher { + + /** + * 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 EagerExecutionResult initialResult; + final Map speculativeBindings; + if (eagerChildContextConfig.checkForContextChanges) { + final Set> entrySet = interpreter.getContext().entrySet(); + final Map initiallyResolvedHashes = getInitiallyResolvedHashes( + entrySet, + interpreter.getContext() + ); + final Map initiallyResolvedAsStrings = + getInitiallyResolvedAsStrings(interpreter, entrySet, initiallyResolvedHashes); + initialResult = applyFunction(function, interpreter, eagerChildContextConfig); + speculativeBindings = + getAllSpeculativeBindings( + interpreter, + eagerChildContextConfig, + initiallyResolvedHashes, + initiallyResolvedAsStrings, + initialResult + ); + } else { + Set ignoredKeys = getAdditionalKeysToIgnore( + interpreter, + eagerChildContextConfig + ); + initialResult = applyFunction(function, interpreter, eagerChildContextConfig); + speculativeBindings = + getBasicSpeculativeBindings( + interpreter, + eagerChildContextConfig, + ignoredKeys, + initialResult + ); + } + 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 -> isResolvableForContextReverting(entry.getValue()) // TODO make this configurable + ); + entryStream.forEach(entry -> + cacheRevertibleObject( + interpreter, + initiallyResolvedHashes, + initiallyResolvedAsStrings, + entry + ) + ); + return initiallyResolvedAsStrings; + } + + private static Map getInitiallyResolvedHashes( + Set> entrySet, + Context context + ) { + Map mapOfHashes = new HashMap<>(); + entrySet + .stream() + .filter(entry -> + !MetaContextVariables.isMetaContextVariable(entry.getKey(), context) + ) + .filter(entry -> + !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null + ) + .forEach(entry -> + mapOfHashes.put(entry.getKey(), getObjectOrHashCode(entry.getValue())) + ); // Avoid NPE when getObjectOrHashCode(entry.getValue()) is null) + return mapOfHashes; + } + + private static Set getAdditionalKeysToIgnore( + JinjavaInterpreter interpreter, + 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 + ) + ? interpreter + .getContext() + .entrySet() + .stream() + .filter(entry -> entry.getValue() instanceof DeferredValue) + .map(Entry::getKey) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + private static Map getBasicSpeculativeBindings( + JinjavaInterpreter interpreter, + EagerChildContextConfig eagerChildContextConfig, + Set ignoredKeys, + EagerExecutionResult eagerExecutionResult + ) { + if (!eagerChildContextConfig.takeNewValue) { + eagerExecutionResult + .getSpeculativeBindings() + .putAll( + interpreter + .getContext() + .getScope() + .entrySet() + .stream() + .filter(entry -> + entry.getValue() instanceof OneTimeReconstructible && + !(((OneTimeReconstructible) entry.getValue()).isReconstructed()) + ) + .peek(entry -> + ((OneTimeReconstructible) entry.getValue()).setReconstructed(true) + ) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)) + ); + } + return eagerExecutionResult + .getSpeculativeBindings() + .entrySet() + .stream() + .filter(entry -> + !MetaContextVariables.isMetaContextVariable( + entry.getKey(), + interpreter.getContext() + ) + ) + .filter(entry -> !ignoredKeys.contains(entry.getKey())) + .filter(entry -> !ForTag.LOOP.equals(entry.getKey())) + .map(entry -> { + if ( + eagerExecutionResult.getResult().isFullyResolved() || + eagerChildContextConfig.takeNewValue + ) { + 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 CannotReconstructValueException(entry.getKey()); + } + return new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), contextValue); + } + return null; + }) + .filter(Objects::nonNull) + .filter(entry -> entry.getValue() != null) + .filter(entry -> !isDeferredWithOriginalValueNull(entry.getValue())) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + } + + private static Map getAllSpeculativeBindings( + JinjavaInterpreter interpreter, + EagerChildContextConfig eagerChildContextConfig, + Map initiallyResolvedHashes, + Map initiallyResolvedAsStrings, + EagerExecutionResult eagerExecutionResult + ) { + 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() + .stream() + .map(key -> + new AbstractMap.SimpleImmutableEntry<>(key, interpreter.getContext().get(key)) + ) + .filter(entry -> + !Objects.equals( + initiallyResolvedHashes.get(entry.getKey()), + getObjectOrHashCode(entry.getValue()) + ) + ) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> + getOriginalValue( + interpreter, + eagerChildContextConfig, + initiallyResolvedHashes, + initiallyResolvedAsStrings, + entry, + eagerExecutionResult.getResult().isFullyResolved() + ) + ) + ) + ); + + speculativeBindings = + speculativeBindings + .entrySet() + .stream() + .filter(entry -> + !MetaContextVariables.isMetaContextVariable( + entry.getKey(), + interpreter.getContext() + ) + ) + .filter(entry -> entry.getValue() != null) + .filter(entry -> !isDeferredWithOriginalValueNull(entry.getValue())) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + return speculativeBindings; + } + + /** + * This is an optimization used to filter so that we don't reconstruct unnecessary tags like {@code {% set num = null %}} + * because {@code num} is already null when it hasn't been set to anything. + */ + private static boolean isDeferredWithOriginalValueNull(Object value) { + return ( + value instanceof DeferredValue && ((DeferredValue) value).getOriginalValue() == null + ); + } + + 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, + boolean isFullyResolved + ) { + if (eagerChildContextConfig.takeNewValue || isFullyResolved) { + return e.getValue(); + } + + if ( + e.getValue() instanceof DeferredValue && + initiallyResolvedHashes + .get(e.getKey()) + .equals(getObjectOrHashCode(((DeferredValue) e.getValue()).getOriginalValue())) + ) { + return e.getValue(); + } + + // 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 CannotReconstructValueException(e.getKey()); + } + + private static Object getObjectOrHashCode(Object o) { + if (o instanceof LazyExpression) { + o = ((LazyExpression) o).get(); + } + + if (o instanceof PyList && isResolvableForContextReverting(o)) { + return o.hashCode(); + } + if (o instanceof PyMap && isResolvableForContextReverting(o)) { + return o.hashCode() + ((PyMap) o).keySet().hashCode(); + } + return o; + } + + private static boolean isResolvableForContextReverting(Object o) { + return EagerExpressionResolver.isResolvableObject(o, 4, 400); + } + + 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/EagerExpressionResolver.java b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java index 2d601be00..cd3b640d4 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerExpressionResolver.java @@ -1,17 +1,27 @@ package com.hubspot.jinjava.util; -import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.annotations.Beta; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Primitives; import com.hubspot.jinjava.el.ext.DeferredParsingException; import com.hubspot.jinjava.el.ext.ExtendedParser; +import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.ErrorHandlingStrategy; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.OutputTooBigException; +import com.hubspot.jinjava.interpret.PartiallyDeferredValue; import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.interpret.UnknownTokenException; +import com.hubspot.jinjava.objects.collections.ArrayBacked; 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.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -19,14 +29,18 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; 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; +@Beta public class EagerExpressionResolver { + public static final String JINJAVA_NULL = "null"; public static final String JINJAVA_EMPTY_STRING = "''"; @@ -45,20 +59,13 @@ public class EagerExpressionResolver { "false", "__macros__", ExtendedParser.INTERPRETER, - "exptest", - "filter" - ); - - private static final Set> RESOLVABLE_CLASSES = ImmutableSet.of( - String.class, - Boolean.class, - Number.class + "exptest" ); private static final Pattern NAMED_PARAMETER_KEY_PATTERN = Pattern.compile( "[\\w.]+=([^=]|$)" ); - private static final Pattern DICTIONARY_KEY_PATTERN = Pattern.compile("[\\w]+: "); + private static final Pattern DICTIONARY_KEY_PATTERN = Pattern.compile("\\w+: "); /** * Resolve the expression while handling deferred values. @@ -66,7 +73,7 @@ public class EagerExpressionResolver { * partially resolved string as well as a set of any words that couldn't be resolved. * If a DeferredParsingException is thrown, the expression was partially resolved. * If a DeferredValueException is thrown, the expression could not be resolved at all. - * + *

    * E.g with foo=3, bar=2: * "range(0,foo)[-1] + deferred/bar" -> "2 + deferred/2" */ @@ -83,9 +90,7 @@ public static EagerExpressionResult resolveExpression( } catch (DeferredParsingException e) { deferredWords.addAll(findDeferredWords(e.getDeferredEvalResult(), interpreter)); result = e.getDeferredEvalResult().trim(); - } catch (DeferredValueException e) { - deferredWords.addAll(findDeferredWords(expression, interpreter)); - result = expression; + // Throw base-class DeferredValueExceptions because only DeferredParsingExceptions are expected when parsing EL expressions } catch (TemplateSyntaxException e) { result = Collections.singletonList(null); fullyResolved = true; @@ -107,23 +112,38 @@ public static String getValueAsJinjavaStringSafe(Object val) { return pyishString; } } - } catch (JsonProcessingException ignored) {} + } catch (IOException | OutputTooBigException ignored) {} throw new DeferredValueException("Can not convert deferred result to string"); } // Find any unresolved variables, functions, etc in this expression to mark as deferred. - private static Set findDeferredWords( + public static Set findDeferredWords( String partiallyResolved, JinjavaInterpreter interpreter ) { - boolean throwInterpreterErrorsStart = interpreter - .getContext() - .getThrowInterpreterErrors(); - try { - interpreter.getContext().setThrowInterpreterErrors(true); + TokenScannerSymbols scannerSymbols = interpreter.getConfig().getTokenScannerSymbols(); + boolean nestedInterpretationEnabled = interpreter + .getConfig() + .isNestedInterpretationEnabled(); + FoundQuotedExpressionTags foundQuotedExpressionTags = new FoundQuotedExpressionTags(); + try ( + TemporaryValueClosable closable = interpreter + .getContext() + .withErrorHandlingStrategy( + ErrorHandlingStrategy + .builder() + .setFatalErrorStrategy( + ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy.THROW_EXCEPTION + ) + .setNonFatalErrorStrategy( + ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy.IGNORE + ) + .build() + ) + ) { Set words = new HashSet<>(); char[] value = partiallyResolved.toCharArray(); - int prevQuotePos = 0; + int prevQuotePos = -1; int curPos = 0; char c; char prevChar = 0; @@ -133,6 +153,16 @@ private static Set findDeferredWords( c = value[curPos]; if (inQuote) { if (c == quoteChar && prevChar != '\\') { + if (nestedInterpretationEnabled) { + getDeferredWordsInsideNestedExpression( + interpreter, + scannerSymbols, + words, + partiallyResolved.substring(prevQuotePos, curPos + 1), + prevQuotePos, + foundQuotedExpressionTags + ); + } inQuote = false; prevQuotePos = curPos; } @@ -142,24 +172,97 @@ private static Set findDeferredWords( words.addAll( findDeferredWordsInSubstring( partiallyResolved, - prevQuotePos, + prevQuotePos + 1, curPos, interpreter ) ); + prevQuotePos = curPos; + } + if (prevChar == '\\') { + // Double escapes cancel out. + prevChar = 0; + } else { + prevChar = c; } - prevChar = c; curPos++; } words.addAll( - findDeferredWordsInSubstring(partiallyResolved, prevQuotePos, curPos, interpreter) + findDeferredWordsInSubstring( + partiallyResolved, + prevQuotePos + 1, + curPos, + interpreter + ) ); + + if (foundQuotedExpressionTags.fullTagMayExist()) { + throw new DeferredValueException( + "Cannot get words inside nested interpretation tags" + ); + } return words; - } finally { - interpreter.getContext().setThrowInterpreterErrors(throwInterpreterErrorsStart); } } + private static void getDeferredWordsInsideNestedExpression( + JinjavaInterpreter interpreter, + TokenScannerSymbols scannerSymbols, + Set words, + String quoted, + int offset, + FoundQuotedExpressionTags foundQuotedExpressionTags + ) { + 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 ( + quoted.contains(scannerSymbols.getExpressionStart()) && + quoted.contains(scannerSymbols.getExpressionEnd()) + ) { + List expressionNodes = getExpressionNodes( + WhitespaceUtils.unquoteAndUnescape(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( @@ -200,7 +303,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; } @@ -226,39 +329,57 @@ private static boolean isResolvableObjectRec( if (isPrimitive(val)) { return true; } - if (val instanceof Collection || val instanceof Map) { - int size = val instanceof Collection - ? ((Collection) val).size() - : ((Map) val).size(); - if (size == 0) { - return true; - } else if (size > maxSize) { - return false; + if (val instanceof ArrayBacked arrayBacked) { + val = arrayBacked.backingArray(); + } + try { + if (val instanceof Collection || val instanceof Map) { + int size = val instanceof Collection + ? ((Collection) val).size() + : ((Map) val).size(); + if (size == 0) { + return true; + } else if (size > maxSize) { + return false; + } + return ( + val instanceof Collection ? (Collection) val : ((Map) val).values() + ).stream() + .filter(Objects::nonNull) + .allMatch(item -> isResolvableObjectRec(item, depth + 1, maxDepth, maxSize)); + } else if (val.getClass().isArray()) { + if (((Object[]) val).length == 0) { + return true; + } else if (((Object[]) val).length > maxSize) { + return false; + } + return (Arrays.stream((Object[]) val)).filter(Objects::nonNull) + .allMatch(item -> isResolvableObjectRec(item, depth + 1, maxDepth, maxSize)); + } else if (val instanceof Optional) { + return ((Optional) val).map(item -> + isResolvableObjectRec(item, depth + 1, maxDepth, maxSize) + ) + .orElse(true); } - return ( - val instanceof Collection ? (Collection) val : ((Map) val).values() - ).stream() - .filter(Objects::nonNull) - .allMatch(item -> isResolvableObjectRec(item, depth + 1, maxDepth, maxSize)); - } else if (val.getClass().isArray()) { - if (((Object[]) val).length == 0) { - return true; - } else if (((Object[]) val).length > maxSize) { - return false; + } catch (DeferredValueException e) { + if (!(val instanceof PartiallyDeferredValue)) { + throw e; } - return (Arrays.stream((Object[]) val)).filter(Objects::nonNull) - .allMatch(item -> isResolvableObjectRec(item, depth + 1, maxDepth, maxSize)); } return PyishSerializable.class.isAssignableFrom(val.getClass()); } public static boolean isPrimitive(Object val) { return ( - val == null || Primitives.isWrapperType(val.getClass()) || val instanceof String + val == null || + Primitives.isWrapperType(val.getClass()) || + val instanceof String || + val instanceof Number ); } public static class EagerExpressionResult { + private final Object resolvedObject; private final Set deferredWords; private final ResolutionState resolutionState; @@ -304,6 +425,23 @@ public String toString(boolean forOutput) { } else { asString = PyishObjectMapper.getAsPyishString(resolvedObject); } + if ( + !forOutput && + interpreter != null && + interpreter.getConfig().isNestedInterpretationEnabled() && + asString.contains( + interpreter.getConfig().getTokenScannerSymbols().getExpressionStart() + ) + ) { + Set dependentWords = EagerExpressionResolver.findDeferredWords( + asString, + interpreter + ); + if (!dependentWords.isEmpty()) { + deferredWords.addAll(dependentWords); + return asString; + } + } return asString; } @@ -394,11 +532,25 @@ public enum ResolutionState { PARTIAL(false), NONE(false); - boolean fullyResolved; + final boolean fullyResolved; ResolutionState(boolean fullyResolved) { this.fullyResolved = fullyResolved; } } } + + private static class FoundQuotedExpressionTags { + + Integer firstStartTagFoundLocation; + Integer lastEndTagFoundLocation; + + boolean fullTagMayExist() { + return ( + firstStartTagFoundLocation != null && + lastEndTagFoundLocation != null && + firstStartTagFoundLocation < lastEndTagFoundLocation + ); + } + } } diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 86df973ce..cef5d396f 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -1,16 +1,17 @@ package com.hubspot.jinjava.util; +import com.google.common.annotations.Beta; +import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.el.ext.AbstractCallableMethod; 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.DeferredValueShadow; 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.interpret.MetaContextVariables; +import com.hubspot.jinjava.interpret.OneTimeReconstructible; import com.hubspot.jinjava.lib.fn.MacroFunction; import com.hubspot.jinjava.lib.fn.eager.EagerMacroFunction; import com.hubspot.jinjava.lib.tag.AutoEscapeTag; @@ -20,15 +21,26 @@ 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.objects.collections.PyList; -import com.hubspot.jinjava.objects.collections.PyMap; +import com.hubspot.jinjava.lib.tag.eager.EagerSetTagStrategy; +import com.hubspot.jinjava.loader.RelativePathResolver; +import com.hubspot.jinjava.objects.serialization.PyishBlockSetSerializable; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; +import com.hubspot.jinjava.objects.serialization.PyishSerializable; import com.hubspot.jinjava.tree.TagNode; +import com.hubspot.jinjava.tree.output.DynamicRenderedOutputNode; +import com.hubspot.jinjava.tree.output.OutputList; +import com.hubspot.jinjava.tree.output.RenderedOutputNode; +import com.hubspot.jinjava.tree.parse.NoteToken; import com.hubspot.jinjava.tree.parse.TagToken; +import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; +import com.hubspot.jinjava.util.EagerContextWatcher.EagerChildContextConfig; import com.hubspot.jinjava.util.EagerExpressionResolver.EagerExpressionResult; +import java.util.AbstractMap; +import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -38,9 +50,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +@Beta public class EagerReconstructionUtils { /** + * @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 @@ -63,6 +77,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, @@ -76,7 +91,6 @@ public static EagerExecutionResult executeInChildContext( EagerChildContextConfig .newBuilder() .withTakeNewValue(takeNewValue) - .withCheckForContextChanges(checkForContextChanges) .withForceDeferredExecutionMode(checkForContextChanges) .withPartialMacroEvaluation(partialMacroEvaluation) .build() @@ -88,236 +102,166 @@ public static EagerExecutionResult executeInChildContext( JinjavaInterpreter interpreter, EagerChildContextConfig eagerChildContextConfig ) { - EagerExpressionResult result; - Set metaContextVariables = interpreter.getContext().getMetaContextVariables(); - final Map initiallyResolvedHashes; - final Map initiallyResolvedAsStrings; - if (eagerChildContextConfig.checkForContextChanges) { - initiallyResolvedHashes = - interpreter - .getContext() - .entrySet() - .stream() - .filter(e -> !metaContextVariables.contains(e.getKey())) - .filter( - entry -> - !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null - ) - .collect( - Collectors.toMap( - Entry::getKey, - entry -> getObjectOrHashCode(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(), 2, 20) // TODO make this configurable - ); - } else { - entryStream = - interpreter - .getContext() - .entrySet() - .stream() - .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) - .filter( - entry -> EagerExpressionResolver.isResolvableObject(entry.getValue(), 2, 20) // 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)); - } - } - ); - } else { - initiallyResolvedHashes = Collections.emptyMap(); - initiallyResolvedAsStrings = Collections.emptyMap(); - } - - // Don't create new call stacks to prevent hitting max recursion with this silent new scope - Map sessionBindings; - try (InterpreterScopeClosable c = interpreter.enterNonStackingScope()) { - if (eagerChildContextConfig.forceDeferredExecutionMode) { - interpreter.getContext().setDeferredExecutionMode(true); - } - interpreter - .getContext() - .setPartialMacroEvaluation(eagerChildContextConfig.partialMacroEvaluation); - result = function.apply(interpreter); - sessionBindings = interpreter.getContext().getSessionBindings(); - } - sessionBindings = - sessionBindings - .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)); - if (eagerChildContextConfig.checkForContextChanges) { - sessionBindings.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 -> { - 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()); - } - ) - ) - ); - } - sessionBindings = - sessionBindings - .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 EagerContextWatcher.executeInChildContext( + function, + interpreter, + eagerChildContextConfig + ); + } - return new EagerExecutionResult(result, sessionBindings); + /** + * Reconstruct the macro functions and variables from the context before they + * get deferred. + * Those macro functions and variables found within {@code deferredWords} are + * reconstructed with {@link MacroTag}(s) and {@link SetTag}(s), respectively to + * preserve the context within the Jinjava template itself. + * @param deferredWords set of words that will need to be deferred based on the + * previously performed operation. + * @param interpreter the Jinjava interpreter. + * @return a Jinjava-syntax string of 0 or more macro tags and 0 or more set tags. + * @deprecated use {@link #hydrateReconstructionFromContextBeforeDeferring(PrefixToPreserveState, Set, JinjavaInterpreter)} + */ + @Deprecated + public static String reconstructFromContextBeforeDeferring( + Set deferredWords, + JinjavaInterpreter interpreter + ) { + return String.join( + "", + reconstructFromContextBeforeDeferringAsMap(deferredWords, interpreter).values() + ); } - 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. + * Those macro functions and variables found within {@code deferredWords} are + * reconstructed with {@link MacroTag}(s) and {@link SetTag}(s), respectively to + * preserve the context within the Jinjava template itself. + * @param deferredWords set of words that will need to be deferred based on the + * previously performed operation. + * @param interpreter the Jinjava interpreter. + * @return a PrefixToPreserveState map of 0 or more macro tags and 0 or more set tags. + * @deprecated use {@link #hydrateReconstructionFromContextBeforeDeferring(PrefixToPreserveState, Set, JinjavaInterpreter)} + */ + @Deprecated + public static PrefixToPreserveState reconstructFromContextBeforeDeferringAsMap( + Set deferredWords, + JinjavaInterpreter interpreter + ) { + PrefixToPreserveState prefixToPreserveState = new PrefixToPreserveState(); + hydrateReconstructionFromContextBeforeDeferring( + prefixToPreserveState, + deferredWords, + interpreter, + 0 + ); + return prefixToPreserveState; } /** * Reconstruct the macro functions and variables from the context before they * get deferred. * Those macro functions and variables found within {@code deferredWords} are - * reconstructed with {@link MacroTag}(s) and a {@link SetTag}, respectively to + * reconstructed with {@link MacroTag}(s) and {@link SetTag}(s), respectively to * preserve the context within the Jinjava template itself. + * @param prefixToPreserveState This PrefixToPreserveState will be hydrated with the Macro tag images and set tag images * @param deferredWords set of words that will need to be deferred based on the * previously performed operation. * @param interpreter the Jinjava interpreter. - * @return a Jinjava-syntax string of 0 or more macro tags and 0 or 1 set tags. + * @return The PrefixToPreserveState to allow method chaining */ - public static String reconstructFromContextBeforeDeferring( + public static PrefixToPreserveState hydrateReconstructionFromContextBeforeDeferring( + PrefixToPreserveState prefixToPreserveState, Set deferredWords, JinjavaInterpreter interpreter ) { - return ( - reconstructMacroFunctionsBeforeDeferring(deferredWords, interpreter) + - reconstructVariablesBeforeDeferring(deferredWords, interpreter) + return hydrateReconstructionFromContextBeforeDeferring( + prefixToPreserveState, + deferredWords, + interpreter, + 0 ); } + private static PrefixToPreserveState hydrateReconstructionFromContextBeforeDeferring( + PrefixToPreserveState prefixToPreserveState, + Set deferredWords, + JinjavaInterpreter interpreter, + int depth + ) { + if (depth <= interpreter.getConfig().getMaxRenderDepth()) { + hydrateReconstructionOfMacroFunctionsBeforeDeferring( + prefixToPreserveState, + deferredWords, + interpreter + ); + Set deferredWordBases = filterToRelevantBases(deferredWords, interpreter); + if (deferredWordBases.isEmpty()) { + return prefixToPreserveState; + } + + return hydrateReconstructionOfVariablesBeforeDeferring( + prefixToPreserveState, + deferredWordBases, + interpreter, + depth + ); + } + return prefixToPreserveState; + } + + private static Set filterToRelevantBases( + Set deferredWords, + JinjavaInterpreter interpreter + ) { + Map combinedScope = interpreter.getContext().getCombinedScope(); + Set deferredWordBases = deferredWords + .stream() + .map(w -> w.split("\\.", 2)[0]) + .filter(combinedScope::containsKey) + .collect(Collectors.toSet()); + if (interpreter.getContext().isDeferredExecutionMode()) { + Context parent = interpreter.getContext().getParent(); + while (parent.isDeferredExecutionMode()) { + parent = parent.getParent(); + } + final Context finalParent = parent; + deferredWordBases = + deferredWordBases + .stream() + .filter(word -> { + Object parentValue = finalParent.get(word); + return ( + !(parentValue instanceof DeferredValue) && + interpreter.getContext().get(word) != finalParent.get(word) + ); + }) + .collect(Collectors.toSet()); + } + return deferredWordBases; + } + /** * Build macro tag images for any macro functions that are included in deferredWords * and remove those macro functions from the deferredWords set. * These macro functions are either global or local macro functions, with local * meaning they've been imported under an alias such as "simple.multiply()". + * @param prefixToPreserveState This PrefixToPreserveState will be hydrated with the Macro tag images * @param deferredWords Set of words that were encountered and their evaluation has * to be deferred for a later render. * @param interpreter The Jinjava interpreter. - * @return A jinjava-syntax string that is the images of any macro functions that must - * be evaluated at a later time. + * @return The PrefixToPreserveState to allow method chaining */ - private static String reconstructMacroFunctionsBeforeDeferring( + private static PrefixToPreserveState hydrateReconstructionOfMacroFunctionsBeforeDeferring( + PrefixToPreserveState prefixToPreserveState, Set deferredWords, JinjavaInterpreter interpreter ) { Set toRemove = new HashSet<>(); Map macroFunctions = deferredWords .stream() + .filter(w -> !prefixToPreserveState.containsKey(w)) .filter(w -> !interpreter.getContext().containsKey(w)) .map(w -> interpreter.getContext().getGlobalMacro(w)) .filter(Objects::nonNull) @@ -332,86 +276,173 @@ private static String reconstructMacroFunctionsBeforeDeferring( } } - String result = macroFunctions + Map reconstructedMacros = macroFunctions .entrySet() .stream() .peek(entry -> toRemove.add(entry.getKey())) .peek(entry -> entry.getValue().setDeferred(true)) - .map( - entry -> - executeInChildContext( + .map(entry -> + new AbstractMap.SimpleImmutableEntry<>( + entry.getKey(), + EagerContextWatcher.executeInChildContext( eagerInterpreter -> EagerExpressionResult.fromString( - new EagerMacroFunction(entry.getKey(), entry.getValue(), interpreter) - .reconstructImage() + ((EagerMacroFunction) entry.getValue()).reconstructImage(entry.getKey()) ), interpreter, - EagerChildContextConfig + EagerContextWatcher.EagerChildContextConfig .newBuilder() .withForceDeferredExecutionMode(true) - .withCheckForContextChanges(true) .build() ) + ) ) - .map(EagerExecutionResult::asTemplateString) - .collect(Collectors.joining()); + .collect( + Collectors.toMap(Entry::getKey, entry -> entry.getValue().asTemplateString()) + ); + prefixToPreserveState.withAll(reconstructedMacros); // Remove macro functions from the set because they've been fully processed now. deferredWords.removeAll(toRemove); - return result; + return prefixToPreserveState; } - private static String reconstructVariablesBeforeDeferring( + private static PrefixToPreserveState hydrateReconstructionOfVariablesBeforeDeferring( + PrefixToPreserveState prefixToPreserveState, Set deferredWords, - JinjavaInterpreter interpreter + JinjavaInterpreter interpreter, + int depth ) { - 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 deferredMap = new HashMap<>(); deferredWords .stream() - .map(w -> w.split("\\.", 2)[0]) // get base prop - .filter( - w -> - interpreter.getContext().containsKey(w) && - !(interpreter.getContext().get(w) instanceof DeferredValue) + .filter(w -> + !MetaContextVariables.isMetaContextVariable(w, interpreter.getContext()) ) - .filter(w -> !metaContextVariables.contains(w)) - .forEach( - w -> { - Object value = interpreter.getContext().get(w); - deferredMap.put(w, PyishObjectMapper.getAsPyishString(value)); - } + .filter(w -> !prefixToPreserveState.containsKey(w)) + .map(word -> + new AbstractMap.SimpleImmutableEntry<>(word, interpreter.getContext().get(word)) + ) + .filter(entry -> + entry.getValue() != null && !(entry.getValue() instanceof DeferredValue) + ) + .forEach(entry -> + hydrateBlockOrInlineSetTagRecursively( + prefixToPreserveState, + entry.getKey(), + entry.getValue(), + interpreter, + depth + ) ); - deferredWords - .stream() - .map(w -> w.split("\\.", 2)[0]) // get base prop - .filter(w -> (interpreter.getContext().get(w) instanceof DeferredLazyReference)) - .forEach( - w -> { - Object value = interpreter.getContext().get(w); - deferredMap.put( - w, - PyishObjectMapper.getAsPyishString( - ((DeferredLazyReference) value).getOriginalValue() - ) - ); - } + return prefixToPreserveState; + } + + public static String buildBlockOrInlineSetTag( + String name, + Object value, + JinjavaInterpreter interpreter + ) { + return buildBlockOrInlineSetTag(name, value, interpreter, false); + } + + public static String buildBlockOrInlineSetTagAndRegisterDeferredToken( + String name, + Object value, + JinjavaInterpreter interpreter + ) { + return buildBlockOrInlineSetTag(name, value, interpreter, true); + } + + public static PrefixToPreserveState hydrateBlockOrInlineSetTagRecursively( + PrefixToPreserveState prefixToPreserveState, + String name, + Object value, + JinjavaInterpreter interpreter + ) { + return hydrateBlockOrInlineSetTagRecursively( + prefixToPreserveState, + name, + value, + interpreter, + 0 + ); + } + + private static PrefixToPreserveState hydrateBlockOrInlineSetTagRecursively( + PrefixToPreserveState prefixToPreserveState, + String name, + Object value, + JinjavaInterpreter interpreter, + int depth + ) { + if ( + value instanceof DeferredValue && + !(value instanceof PyishBlockSetSerializable || value instanceof PyishSerializable) + ) { + value = ((DeferredValue) value).getOriginalValue(); + } + if (value instanceof PyishBlockSetSerializable) { + prefixToPreserveState.put( + name, + buildBlockSetTag( + name, + ((PyishBlockSetSerializable) value).getBlockSetBody(), + interpreter, + false + ) + ); + return prefixToPreserveState; + } + String pyishStringRepresentation = PyishObjectMapper.getAsPyishString(value); + + if ( + depth < interpreter.getConfig().getMaxRenderDepth() && + interpreter.getConfig().isNestedInterpretationEnabled() + ) { + Set dependentWords = EagerExpressionResolver.findDeferredWords( + pyishStringRepresentation, + interpreter + ); + if (!dependentWords.isEmpty()) { + hydrateReconstructionFromContextBeforeDeferring( + prefixToPreserveState, + dependentWords, + interpreter, + depth + 1 + ); + } + } + prefixToPreserveState.put( + name, + buildSetTag(ImmutableMap.of(name, pyishStringRepresentation), interpreter, false) + ); + return prefixToPreserveState; + } + + public static String buildBlockOrInlineSetTag( + String name, + Object value, + JinjavaInterpreter interpreter, + boolean registerDeferredToken + ) { + if ( + value instanceof DeferredValue && + !(value instanceof PyishBlockSetSerializable || value instanceof PyishSerializable) + ) { + value = ((DeferredValue) value).getOriginalValue(); + } + if (value instanceof PyishBlockSetSerializable) { + return buildBlockSetTag( + name, + ((PyishBlockSetSerializable) value).getBlockSetBody(), + interpreter, + registerDeferredToken ); - return buildSetTag(deferredMap, interpreter, false); + } + return buildSetTag( + ImmutableMap.of(name, PyishObjectMapper.getAsPyishString(value)), + interpreter, + registerDeferredToken + ); } /** @@ -435,23 +466,24 @@ public static String buildSetTag( return ""; } Map> disabled = interpreter.getConfig().getDisabled(); - if ( - disabled != null && - disabled.containsKey(Library.TAG) && - disabled.get(Library.TAG).contains(SetTag.TAG_NAME) - ) { - throw new DisabledException("set tag disabled"); + if (disabled != null) { + Set disabledTags = disabled.get(Library.TAG); + if (disabledTags != null && disabledTags.contains(SetTag.TAG_NAME)) { + throw new DisabledException("set tag disabled"); + } } StringJoiner vars = new StringJoiner(","); StringJoiner values = new StringJoiner(","); - deferredValuesToSet.forEach( - (key, value) -> { - // This ensures they are properly aligned to each other. - vars.add(key); - values.add(value); + List varsRequiringSuffix = new ArrayList<>(); + deferredValuesToSet.forEach((key, value) -> { + // This ensures they are properly aligned to each other. + vars.add(key); + values.add(value); + if (!MetaContextVariables.isTemporaryImportAlias(value)) { + varsRequiringSuffix.add(key); } - ); + }); LengthLimitingStringJoiner result = new LengthLimitingStringJoiner( interpreter.getConfig().getMaxOutputSize(), " " @@ -464,25 +496,27 @@ public static String buildSetTag( .add(values.toString()) .add(interpreter.getConfig().getTokenScannerSymbols().getExpressionEndWithTag()); String image = result.toString(); + String suffix = EagerSetTagStrategy.getSuffixToPreserveState( + varsRequiringSuffix, + interpreter + ); // Don't defer if we're sticking with the new value if (registerDeferredToken) { - interpreter - .getContext() - .handleDeferredToken( - new DeferredToken( - new TagToken( - image, - // TODO this line number won't be accurate, currently doesn't matter. - interpreter.getLineNumber(), - interpreter.getPosition(), - interpreter.getConfig().getTokenScannerSymbols() - ), - Collections.emptySet(), - deferredValuesToSet.keySet() + return ( + new PrefixToPreserveState( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromImage(image, TagToken.class, interpreter) + .addSetDeferredWords(deferredValuesToSet.keySet()) + .build() ) - ); + ) + + image + + suffix + ); } - return image; + return (image + suffix); } /** @@ -491,7 +525,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. @@ -503,12 +537,11 @@ public static String buildBlockSetTag( boolean registerDeferredToken ) { Map> disabled = interpreter.getConfig().getDisabled(); - if ( - disabled != null && - disabled.containsKey(Library.TAG) && - disabled.get(Library.TAG).contains(SetTag.TAG_NAME) - ) { - throw new DisabledException("set tag disabled"); + if (disabled != null) { + Set disabledTags = disabled.get(Library.TAG); + if (disabledTags != null && disabledTags.contains(SetTag.TAG_NAME)) { + throw new DisabledException("set tag disabled"); + } } LengthLimitingStringJoiner blockSetTokenBuilder = new LengthLimitingStringJoiner( @@ -526,23 +559,30 @@ public static String buildBlockSetTag( .add("end" + SetTag.TAG_NAME) .add(interpreter.getConfig().getTokenScannerSymbols().getExpressionEndWithTag()); String image = blockSetTokenBuilder + value + endTokenBuilder; - if (registerDeferredToken) { + String suffix = EagerSetTagStrategy.getSuffixToPreserveState( + new String[] { name }, interpreter - .getContext() - .handleDeferredToken( - new DeferredToken( - new TagToken( - blockSetTokenBuilder.toString(), - interpreter.getLineNumber(), - interpreter.getPosition(), - interpreter.getConfig().getTokenScannerSymbols() - ), - Collections.emptySet(), - Collections.singleton(name) + ); + if (registerDeferredToken) { + return ( + new PrefixToPreserveState( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromImage( + blockSetTokenBuilder.toString(), + TagToken.class, + interpreter + ) + .addSetDeferredWords(Stream.of(name)) + .build() ) - ); + ) + + image + + suffix + ); } - return image; + return image + suffix; } public static String buildDoUpdateTag( @@ -551,12 +591,11 @@ public static String buildDoUpdateTag( JinjavaInterpreter interpreter ) { Map> disabled = interpreter.getConfig().getDisabled(); - if ( - disabled != null && - disabled.containsKey(Library.TAG) && - disabled.get(Library.TAG).contains(DoTag.TAG_NAME) - ) { - throw new DisabledException("do tag disabled"); + if (disabled != null) { + Set disabledTags = disabled.get(Library.TAG); + if (disabledTags != null && disabledTags.contains(DoTag.TAG_NAME)) { + throw new DisabledException("do tag disabled"); + } } return new LengthLimitingStringJoiner(interpreter.getConfig().getMaxOutputSize(), " ") .add(interpreter.getConfig().getTokenScannerSymbols().getExpressionStartWithTag()) @@ -588,7 +627,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; @@ -600,38 +639,101 @@ public static String wrapInAutoEscapeIfNeeded( ) { if ( interpreter.getContext().isAutoEscape() && - ( - interpreter.getContext().getParent() == null || - !interpreter.getContext().getParent().isAutoEscape() - ) + (interpreter.getContext().getParent() == null || + !interpreter.getContext().getParent().isAutoEscape()) ) { - output = wrapInTag(output, AutoEscapeTag.TAG_NAME, interpreter); + output = wrapInTag(output, AutoEscapeTag.TAG_NAME, interpreter, false); } return output; } + /** + * 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 s, + String body, String tagNameToWrap, + JinjavaInterpreter interpreter, + boolean registerDeferredToken + ) { + Map> disabled = interpreter.getConfig().getDisabled(); + if (disabled != null) { + Set disabledTags = disabled.get(Library.TAG); + if (disabledTags != null && disabledTags.contains(tagNameToWrap)) { + throw new DisabledException(String.format("%s tag disabled", tagNameToWrap)); + } + } + StringJoiner startTokenBuilder = new StringJoiner(" "); + StringJoiner endTokenBuilder = new StringJoiner(" "); + startTokenBuilder + .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 = startTokenBuilder + body + endTokenBuilder; + if (registerDeferredToken) { + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromImage(startTokenBuilder.toString(), TagToken.class, interpreter) + .build() + ); + } + return image; + } + + /** + * Surround the {@param body} with notes to provide identifying information on what {@param body} is. + * If {@param noteIdentifier} is {@code foo} and {@param body} is {@code {{ bar }}}, the result will be: + *

    + * {@code {# foo #}{{ bar }}{# endfoo #}} + * @param body The string body to wrap. + * @param noteIdentifier The identifier for the note. + * @param interpreter The Jinjava interpreter. + * @return A block surrounded with labelled notes + */ + public static String labelWithNotes( + String body, + String noteIdentifier, JinjavaInterpreter interpreter ) { 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() - ) + getStartLabel(noteIdentifier, interpreter.getConfig().getTokenScannerSymbols()) + + body + + getEndLabel(noteIdentifier, interpreter.getConfig().getTokenScannerSymbols()) ); } + public static String getStartLabel(String noteIdentifier, TokenScannerSymbols symbols) { + StringJoiner stringJoiner = new StringJoiner(" "); + return stringJoiner + .add(symbols.getOpeningComment()) + .add("Start Label: ") + .add(noteIdentifier) + .add(symbols.getClosingComment()) + .toString(); + } + + public static String getEndLabel(String noteIdentifier, TokenScannerSymbols symbols) { + StringJoiner stringJoiner = new StringJoiner(" "); + return stringJoiner + .add(symbols.getOpeningComment()) + .add("End Label: ") + .add(noteIdentifier) + .add(symbols.getClosingComment()) + .toString(); + } + public static String wrapInChildScope(String toWrap, JinjavaInterpreter interpreter) { return ( String.format( @@ -648,64 +750,227 @@ public static String wrapInChildScope(String toWrap, JinjavaInterpreter interpre ); } - public static class EagerChildContextConfig { - private final boolean takeNewValue; - private final boolean partialMacroEvaluation; - private final boolean checkForContextChanges; - private final boolean forceDeferredExecutionMode; + public static Boolean isDeferredExecutionMode() { + return JinjavaInterpreter + .getCurrentMaybe() + .map(interpreter -> interpreter.getContext().isDeferredExecutionMode()) + .orElse(false); + } - private EagerChildContextConfig( - boolean takeNewValue, - boolean partialMacroEvaluation, - boolean checkForContextChanges, - boolean forceDeferredExecutionMode - ) { - this.takeNewValue = takeNewValue; - this.partialMacroEvaluation = partialMacroEvaluation; - this.checkForContextChanges = checkForContextChanges; - this.forceDeferredExecutionMode = forceDeferredExecutionMode; + public static PrefixToPreserveState deferWordsAndReconstructReferences( + JinjavaInterpreter interpreter, + Set wordsToDefer + ) { + if (!wordsToDefer.isEmpty()) { + wordsToDefer = + wordsToDefer + .stream() + .filter(key -> !(interpreter.getContext().get(key) instanceof DeferredValue)) + .collect(Collectors.toSet()); + PrefixToPreserveState prefixToPreserveState = new PrefixToPreserveState(); + if (!wordsToDefer.isEmpty()) { + prefixToPreserveState.withAllInFront( + handleDeferredTokenAndReconstructReferences( + interpreter, + DeferredToken + .builderFromImage("", NoteToken.class, interpreter) + .addUsedDeferredWords(wordsToDefer) + .build() + ) + ); + } + return prefixToPreserveState; } + return new PrefixToPreserveState(); + } - public static Builder newBuilder() { - return new Builder(); - } + public static Map handleDeferredTokenAndReconstructReferences( + JinjavaInterpreter interpreter, + DeferredToken deferredToken + ) { + deferredToken.addTo(interpreter.getContext()); + return reconstructDeferredReferences( + interpreter, + deferredToken.getUsedDeferredBases() + ); + } - public static class Builder { - private boolean takeNewValue; - private boolean partialMacroEvaluation; - private boolean checkForContextChanges; - private boolean forceDeferredExecutionMode; + public static Map reconstructDeferredReferences( + JinjavaInterpreter interpreter, + Set usedDeferredBases + ) { + return interpreter + .getContext() + .getScope() + .entrySet() + .stream() + .filter(entry -> + entry.getValue() instanceof OneTimeReconstructible && + !((OneTimeReconstructible) entry.getValue()).isReconstructed() + ) + .filter(entry -> + // Always reconstruct the DeferredLazyReferenceSource, but only reconstruct DeferredLazyReferences when they are used + entry.getValue() instanceof DeferredLazyReferenceSource || + usedDeferredBases.contains(entry.getKey()) + ) + .peek(entry -> ((OneTimeReconstructible) entry.getValue()).setReconstructed(true)) + .map(entry -> + new AbstractMap.SimpleImmutableEntry<>( + entry.getKey(), + PyishObjectMapper.getAsPyishString( + ((DeferredValue) entry.getValue()).getOriginalValue() + ) + ) + ) + .sorted((a, b) -> + a.getValue().equals(b.getKey()) ? 1 : b.getValue().equals(a.getKey()) ? -1 : 0 + ) + .collect( + Collectors.toMap( + Entry::getKey, + entry -> + buildSetTag( + Collections.singletonMap(entry.getKey(), entry.getValue()), + interpreter, + false + ), + (a, b) -> b, + LinkedHashMap::new + ) + ); + } - private Builder() {} + /** + * Reset variables to what they were before running the latest execution represented by {@param eagerExecutionResult}. + * Then re-defer those variables and reconstruct deferred lazy references to them. + * This method is needed in 2 circumstances: + *

    + * * When doing some eager execution and then needing to repeat the same execution in deferred execution mode. + *

    + * * When rendering logic which takes place in its own child scope (for tag, macro function, set block) and there + * are speculative bindings. + * These must be deferred and the execution must run again, so they don't get reconstructed + * within the child scope, and can instead be reconstructed in their original scopes. + * @param interpreter The JinjavaInterpreter + * @param eagerExecutionResult The execution result which contains information about which bindings were modified + * during the execution. + * @return + */ + public static PrefixToPreserveState resetAndDeferSpeculativeBindings( + JinjavaInterpreter interpreter, + EagerExecutionResult eagerExecutionResult + ) { + return deferWordsAndReconstructReferences( + interpreter, + resetSpeculativeBindings(interpreter, eagerExecutionResult) + ); + } - public Builder withTakeNewValue(boolean takeNewValue) { - this.takeNewValue = takeNewValue; - return this; - } + public static Set resetSpeculativeBindings( + JinjavaInterpreter interpreter, + EagerExecutionResult result + ) { + result + .getSpeculativeBindings() + .forEach((k, v) -> { + if (v instanceof DeferredValue) { + v = ((DeferredValue) v).getOriginalValue(); + } + replace(interpreter.getContext(), k, v); + }); + return result.getSpeculativeBindings().keySet(); + } - public Builder withPartialMacroEvaluation(boolean partialMacroEvaluation) { - this.partialMacroEvaluation = partialMacroEvaluation; - return this; - } + private static void replace(Context context, String k, Object v) { + if (context == null) { + return; + } + Object replaced = context.getScope().replace(k, v); + if (replaced == null) { + replace(context.getParent(), k, v); + } else if (replaced instanceof DeferredValueShadow) { + context.getScope().remove(k); + replace(context.getParent(), k, v); + } + } - public Builder withCheckForContextChanges(boolean checkForContextChanges) { - this.checkForContextChanges = checkForContextChanges; - return this; - } + public static void commitSpeculativeBindings( + JinjavaInterpreter interpreter, + EagerExecutionResult result + ) { + result + .getSpeculativeBindings() + .entrySet() + .stream() + // Filter DeferredValueShadow because these are just used to mark that a value became deferred within this scope + // The original key will be a DeferredValueImpl already on its original scope + .filter(entry -> !(entry.getValue() instanceof DeferredValueShadow)) + .forEach(entry -> interpreter.getContext().put(entry.getKey(), entry.getValue())); + } - public Builder withForceDeferredExecutionMode(boolean forceDeferredExecutionMode) { - this.forceDeferredExecutionMode = forceDeferredExecutionMode; - return this; - } + public static void reconstructPathAroundBlock( + DynamicRenderedOutputNode prefix, + OutputList blockValueBuilder, + JinjavaInterpreter interpreter + ) { + String blockPath = RelativePathResolver.getCurrentPathFromStackOrKey(interpreter); + String tempVarName = MetaContextVariables.getTemporaryCurrentPathVarName(blockPath); + prefix.setValue( + buildSetTag( + ImmutableMap.of( + tempVarName, + RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, + RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, + PyishObjectMapper.getAsPyishString(blockPath) + ), + interpreter, + false + ) + ); + blockValueBuilder.addNode( + new RenderedOutputNode( + buildSetTag( + ImmutableMap.of( + RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, + tempVarName, + tempVarName, + "null" + ), + interpreter, + false + ) + ) + ); + } - public EagerChildContextConfig build() { - return new EagerChildContextConfig( - takeNewValue, - partialMacroEvaluation, - checkForContextChanges, - forceDeferredExecutionMode - ); - } - } + public static String wrapPathAroundText( + String text, + String newPath, + JinjavaInterpreter interpreter + ) { + String tempVarName = MetaContextVariables.getTemporaryCurrentPathVarName(newPath); + return ( + buildSetTag( + ImmutableMap.of( + tempVarName, + RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, + RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, + PyishObjectMapper.getAsPyishString(newPath) + ), + interpreter, + false + ) + + text + + buildSetTag( + ImmutableMap.of( + RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, + tempVarName, + tempVarName, + "null" + ), + interpreter, + false + ) + ); } } diff --git a/src/main/java/com/hubspot/jinjava/util/ForLoop.java b/src/main/java/com/hubspot/jinjava/util/ForLoop.java index 8d249a6cd..e233e7b95 100644 --- a/src/main/java/com/hubspot/jinjava/util/ForLoop.java +++ b/src/main/java/com/hubspot/jinjava/util/ForLoop.java @@ -18,6 +18,7 @@ import java.util.Iterator; public class ForLoop implements Iterator { + private static final int NULL_VALUE = Integer.MIN_VALUE; private int index = -1; @@ -27,6 +28,8 @@ public class ForLoop implements Iterator { private int length = NULL_VALUE; private boolean first = true; private boolean last; + private boolean continued; + private boolean broken; private int depth; @@ -44,6 +47,8 @@ public ForLoop(Iterator ite, int len) { last = false; } it = ite; + continued = false; + broken = false; } public ForLoop(Iterator ite) { @@ -56,10 +61,16 @@ public ForLoop(Iterator ite) { revcounter = 2; last = true; } + continued = false; + broken = false; } @Override public Object next() { + if (broken) { + return null; + } + continued = false; Object res; if (it.hasNext()) { index++; @@ -128,8 +139,24 @@ public boolean isLast() { return last; } + public boolean isContinued() { + return continued; + } + + public void doContinue() { + continued = true; + } + + public void doBreak() { + continued = true; + broken = true; + } + @Override public boolean hasNext() { + if (broken) { + return false; + } return it.hasNext(); } diff --git a/src/main/java/com/hubspot/jinjava/util/HelperStringTokenizer.java b/src/main/java/com/hubspot/jinjava/util/HelperStringTokenizer.java index 38e8a321f..0fade94c3 100644 --- a/src/main/java/com/hubspot/jinjava/util/HelperStringTokenizer.java +++ b/src/main/java/com/hubspot/jinjava/util/HelperStringTokenizer.java @@ -26,6 +26,7 @@ * */ public class HelperStringTokenizer extends AbstractIterator { + private final char[] value; private final int length; @@ -35,6 +36,7 @@ public class HelperStringTokenizer extends AbstractIterator { private boolean useComma = false; private char quoteChar = 0; private boolean inQuote = false; + private boolean isEscaped = false; public HelperStringTokenizer(String s) { value = s.toCharArray(); @@ -70,7 +72,8 @@ protected String computeNext() { private String makeToken() { char c = value[currPost++]; - if (c == '"' || c == '\'') { + + if ((c == '"' || c == '\'') && !isEscaped) { if (inQuote) { if (quoteChar == c) { inQuote = false; @@ -80,6 +83,9 @@ private String makeToken() { quoteChar = c; } } + + isEscaped = (c == '\\' && !isEscaped); + if ((Character.isWhitespace(c) || (useComma && c == ',')) && !inQuote) { return newToken(); } diff --git a/src/main/java/com/hubspot/jinjava/util/LengthLimitingStringBuilder.java b/src/main/java/com/hubspot/jinjava/util/LengthLimitingStringBuilder.java index 11b3cc3d0..850d7df5f 100644 --- a/src/main/java/com/hubspot/jinjava/util/LengthLimitingStringBuilder.java +++ b/src/main/java/com/hubspot/jinjava/util/LengthLimitingStringBuilder.java @@ -4,7 +4,9 @@ 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 +52,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/main/java/com/hubspot/jinjava/util/LengthLimitingStringJoiner.java b/src/main/java/com/hubspot/jinjava/util/LengthLimitingStringJoiner.java index 10337d49e..c4f278b80 100644 --- a/src/main/java/com/hubspot/jinjava/util/LengthLimitingStringJoiner.java +++ b/src/main/java/com/hubspot/jinjava/util/LengthLimitingStringJoiner.java @@ -4,6 +4,7 @@ import java.util.StringJoiner; public class LengthLimitingStringJoiner { + private final StringJoiner joiner; private final int delimiterLength; private final long maxLength; diff --git a/src/main/java/com/hubspot/jinjava/util/PrefixToPreserveState.java b/src/main/java/com/hubspot/jinjava/util/PrefixToPreserveState.java new file mode 100644 index 000000000..999cfb051 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/util/PrefixToPreserveState.java @@ -0,0 +1,42 @@ +package com.hubspot.jinjava.util; + +import com.google.common.annotations.Beta; +import com.google.common.collect.ForwardingMap; +import java.util.LinkedHashMap; +import java.util.Map; + +@Beta +public class PrefixToPreserveState extends ForwardingMap { + + private Map reconstructedValues; + + public PrefixToPreserveState() { + reconstructedValues = new LinkedHashMap<>(); + } + + public PrefixToPreserveState(Map reconstructedValues) { + this.reconstructedValues = reconstructedValues; + } + + @Override + protected Map delegate() { + return reconstructedValues; + } + + @Override + public String toString() { + return String.join("", reconstructedValues.values()); + } + + public PrefixToPreserveState withAllInFront(Map toInsert) { + Map newMap = new LinkedHashMap<>(toInsert); + reconstructedValues.forEach(newMap::putIfAbsent); + reconstructedValues = newMap; + return this; + } + + public PrefixToPreserveState withAll(Map toPut) { + putAll(toPut); + return this; + } +} diff --git a/src/main/java/com/hubspot/jinjava/util/RenderLimitUtils.java b/src/main/java/com/hubspot/jinjava/util/RenderLimitUtils.java new file mode 100644 index 000000000..61f2de594 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/util/RenderLimitUtils.java @@ -0,0 +1,23 @@ +package com.hubspot.jinjava.util; + +import com.hubspot.jinjava.JinjavaConfig; + +public class RenderLimitUtils { + + public static long clampProvidedRenderLimitToConfig( + long providedLimit, + JinjavaConfig jinjavaConfig + ) { + long configMaxOutput = jinjavaConfig.getMaxOutputSize(); + + if (configMaxOutput <= 0) { + return providedLimit; + } + + if (providedLimit <= 0) { + return configMaxOutput; + } + + return Math.min(providedLimit, configMaxOutput); + } +} diff --git a/src/main/java/com/hubspot/jinjava/util/ScopeMap.java b/src/main/java/com/hubspot/jinjava/util/ScopeMap.java index 28d3662f2..6208db8fc 100644 --- a/src/main/java/com/hubspot/jinjava/util/ScopeMap.java +++ b/src/main/java/com/hubspot/jinjava/util/ScopeMap.java @@ -1,19 +1,16 @@ package com.hubspot.jinjava.util; -import static com.hubspot.jinjava.util.Logging.ENGINE_LOG; - import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import javax.annotation.Nonnull; public class ScopeMap implements Map { + private final Map scope; private final ScopeMap parent; @@ -24,25 +21,6 @@ public ScopeMap() { public ScopeMap(ScopeMap parent) { this.scope = new HashMap<>(); this.parent = parent; - - Set> parents = new HashSet<>(); - if (parent != null) { - ScopeMap p = parent.getParent(); - while (p != null) { - parents.add(p); - if (parents.contains(parent)) { - ENGINE_LOG.error( - "Parent loop detected:\n{}", - Arrays - .stream(Thread.currentThread().getStackTrace()) - .map(StackTraceElement::toString) - .collect(Collectors.joining("\n")) - ); - break; - } - p = p.getParent(); - } - } } public ScopeMap(ScopeMap parent, Map scope) { @@ -214,6 +192,7 @@ public Set> entrySet() { } public static class ScopeMapEntry implements Map.Entry { + private final Map map; private final K key; private V value; diff --git a/src/main/java/com/hubspot/jinjava/util/Variable.java b/src/main/java/com/hubspot/jinjava/util/Variable.java index 31ed6b320..5649ce408 100644 --- a/src/main/java/com/hubspot/jinjava/util/Variable.java +++ b/src/main/java/com/hubspot/jinjava/util/Variable.java @@ -22,6 +22,7 @@ import java.util.List; public class Variable { + private static final Splitter DOT_SPLITTER = Splitter.on('.'); private final JinjavaInterpreter interpreter; diff --git a/src/main/java/com/hubspot/jinjava/util/WhitespaceUtils.java b/src/main/java/com/hubspot/jinjava/util/WhitespaceUtils.java index 43eb5f85c..b3a7c6e17 100644 --- a/src/main/java/com/hubspot/jinjava/util/WhitespaceUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/WhitespaceUtils.java @@ -2,8 +2,10 @@ 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[] { '\'', '"' }; public static boolean startsWith(String s, String prefix) { @@ -102,7 +104,6 @@ public static String unquote(String s) { return s.trim(); } - // TODO see if all usages of unquote can use this method instead public static String unquoteAndUnescape(String s) { if (Strings.isNullOrEmpty(s)) { return ""; @@ -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/BaseInterpretingTest.java b/src/test/java/com/hubspot/jinjava/BaseInterpretingTest.java index d5db9d74d..7fdf9c4c4 100644 --- a/src/test/java/com/hubspot/jinjava/BaseInterpretingTest.java +++ b/src/test/java/com/hubspot/jinjava/BaseInterpretingTest.java @@ -6,6 +6,7 @@ import org.junit.Before; public abstract class BaseInterpretingTest extends BaseJinjavaTest { + public JinjavaInterpreter interpreter; public Context context; diff --git a/src/test/java/com/hubspot/jinjava/BaseJinjavaTest.java b/src/test/java/com/hubspot/jinjava/BaseJinjavaTest.java index 86fd55ef7..f7b3244a4 100644 --- a/src/test/java/com/hubspot/jinjava/BaseJinjavaTest.java +++ b/src/test/java/com/hubspot/jinjava/BaseJinjavaTest.java @@ -1,20 +1,42 @@ package com.hubspot.jinjava; +import com.hubspot.jinjava.el.ext.AllowlistMethodValidator; +import com.hubspot.jinjava.el.ext.AllowlistReturnTypeValidator; +import com.hubspot.jinjava.el.ext.MethodValidatorConfig; +import com.hubspot.jinjava.el.ext.ReturnTypeValidatorConfig; import org.junit.Before; public abstract class BaseJinjavaTest { + + public static final AllowlistMethodValidator METHOD_VALIDATOR = + AllowlistMethodValidator.create( + MethodValidatorConfig + .builder() + .addDefaultAllowlistGroups() + .addAllowedDeclaredMethodsFromCanonicalClassPrefixes( + "com.hubspot.jinjava.testobjects." + ) + .build() + ); + public static final AllowlistReturnTypeValidator RETURN_TYPE_VALIDATOR = + AllowlistReturnTypeValidator.create( + ReturnTypeValidatorConfig + .builder() + .addDefaultAllowlistGroups() + .addAllowedCanonicalClassPrefixes("com.hubspot.jinjava.testobjects.") + .build() + ); public Jinjava jinjava; @Before public void baseSetup() { - jinjava = - new Jinjava( - JinjavaConfig - .newBuilder() - .withLegacyOverrides( - LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() - ) - .build() - ); + jinjava = new Jinjava(BaseJinjavaTest.newConfigBuilder().build()); + } + + public static JinjavaConfig.Builder newConfigBuilder() { + return JinjavaConfig + .builder() + .withMethodValidator(METHOD_VALIDATOR) + .withReturnTypeValidator(RETURN_TYPE_VALIDATOR); } } diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index bbd80ec9d..2cd4590b0 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -22,16 +22,20 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; public class EagerTest { + private JinjavaInterpreter interpreter; private Jinjava jinjava; private ExpectedTemplateInterpreter expectedTemplateInterpreter; @@ -55,10 +59,9 @@ public String getString( String fullName, Charset encoding, JinjavaInterpreter interpreter - ) - throws IOException { + ) throws IOException { return Resources.toString( - Resources.getResource(String.format("tags/macrotag/%s", fullName)), + Resources.getResource(fullName), StandardCharsets.UTF_8 ); } @@ -69,8 +72,8 @@ public Optional getLocationResolver() { } } ); - JinjavaConfig config = JinjavaConfig - .newBuilder() + JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withRandomNumberGeneratorStrategy(RandomNumberGeneratorStrategy.DEFERRED) .withExecutionMode(executionMode) .withNestedInterpretationEnabled(true) @@ -87,7 +90,7 @@ public Optional getLocationResolver() { ); interpreter = new JinjavaInterpreter(parentInterpreter); expectedTemplateInterpreter = - new ExpectedTemplateInterpreter(jinjava, interpreter, "eager"); + ExpectedTemplateInterpreter.withSensibleCurrentPath(jinjava, interpreter, "eager"); localContext = interpreter.getContext(); localContext.put("deferred", DeferredValue.instance()); @@ -116,10 +119,10 @@ public void itReconstructsMapWithNullValues() { @Test public void itDefersNodeWhenModifiedInForLoop() { assertThat( - interpreter.render( - "{% set bar = 'bar' %}{% set foo = 0 %}{% for i in deferred %}{{ bar ~ foo ~ bar }} {% set foo = foo + 1 %}{% endfor %}" - ) + interpreter.render( + "{% set bar = 'bar' %}{% set foo = 0 %}{% for i in deferred %}{{ bar ~ foo ~ bar }} {% set foo = foo + 1 %}{% endfor %}" ) + ) .isEqualTo( "{% set foo = 0 %}{% for i in deferred %}{{ 'bar' ~ foo ~ 'bar' }} {% set foo = foo + 1 %}{% endfor %}" ); @@ -363,9 +366,8 @@ public void itDefersAllVariablesUsedInDeferredNode() { DeferredValue varInScopeDeferred = (DeferredValue) varInScope; assertThat(varInScopeDeferred.getOriginalValue()).isEqualTo("outside if statement"); - HashMap deferredContext = DeferredValueUtils.getDeferredContextWithOriginalValues( - localContext - ); + HashMap deferredContext = + DeferredValueUtils.getDeferredContextWithOriginalValues(localContext); deferredContext.forEach(localContext::put); String secondRender = interpreter.render(output); assertThat(secondRender).isEqualTo("outside if statement entered if statement"); @@ -378,7 +380,7 @@ public void itDefersAllVariablesUsedInDeferredNode() { public void itDefersDependantVariables() { String template = ""; template += - "{% set resolved_variable = 'resolved' %} {% set deferred_variable = deferred + '-' + resolved_variable %}"; + "{% set resolved_variable = 'resolved' %} {% set deferred_variable = deferred + '-' + resolved_variable %}"; template += "{{ deferred_variable }}"; interpreter.render(template); localContext.get("resolved_variable"); @@ -395,9 +397,8 @@ public void itDefersVariablesComparedAgainstDeferredVals() { assertThat(output.trim()) .isEqualTo("{% if deferred == 'testvalue' %} true {% else %} false {% endif %}"); - HashMap deferredContext = DeferredValueUtils.getDeferredContextWithOriginalValues( - localContext - ); + HashMap deferredContext = + DeferredValueUtils.getDeferredContextWithOriginalValues(localContext); deferredContext.forEach(localContext::put); String secondRender = interpreter.render(output); assertThat(secondRender.trim()).isEqualTo("true"); @@ -420,9 +421,8 @@ public void itPutsDeferredVariablesOnParentScopes() { ); localContext.put("deferredValue", DeferredValue.instance("resolved")); String output = interpreter.render(template); - HashMap deferredContext = DeferredValueUtils.getDeferredContextWithOriginalValues( - localContext - ); + HashMap deferredContext = + DeferredValueUtils.getDeferredContextWithOriginalValues(localContext); deferredContext.forEach(localContext::put); String secondRender = interpreter.render(output); assertThat(secondRender.trim()).isEqualTo("inside first scope".trim()); @@ -437,9 +437,8 @@ public void puttingDeferredVariablesOnParentScopesDoesNotBreakSetTag() { localContext.put("deferredValue", DeferredValue.instance("resolved")); String output = interpreter.render(template); - HashMap deferredContext = DeferredValueUtils.getDeferredContextWithOriginalValues( - localContext - ); + HashMap deferredContext = + DeferredValueUtils.getDeferredContextWithOriginalValues(localContext); deferredContext.forEach(localContext::put); String secondRender = interpreter.render(output); assertThat(secondRender.trim()) @@ -481,62 +480,68 @@ public void itMarksVariablesUsedAsMapKeysAsDeferred() { String output = interpreter.render(template); assertThat(localContext).containsKey("deferredValue2"); Object deferredValue2 = localContext.get("deferredValue2"); - DeferredValueUtils.findAndMarkDeferredProperties(localContext); + localContext + .getDeferredNodes() + .forEach(node -> + DeferredValueUtils.findAndMarkDeferredProperties(localContext, node) + ); assertThat(deferredValue2).isInstanceOf(DeferredValue.class); assertThat(output) .contains( - "{% set varSetInside = {'key': 'value'}[deferredValue2.nonexistentprop] %}" + "{% set varSetInside = {'key': 'value'} [deferredValue2.nonexistentprop] %}" ); } @Test public void itEagerlyDefersSet() { localContext.put("bar", true); - expectedTemplateInterpreter.assertExpectedOutput("eagerly-defers-set"); + expectedTemplateInterpreter.assertExpectedOutput("eagerly-defers-set/test"); } @Test public void itEvaluatesNonEagerSet() { - expectedTemplateInterpreter.assertExpectedOutput("evaluates-non-eager-set"); + expectedTemplateInterpreter.assertExpectedOutput("evaluates-non-eager-set/test"); assertThat( - localContext - .getDeferredTokens() - .stream() - .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) - .collect(Collectors.toSet()) - ) - .containsExactlyInAnyOrder("item"); + localContext + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) + .collect(Collectors.toSet()) + ) + .isEmpty(); assertThat( - localContext - .getDeferredTokens() - .stream() - .flatMap(deferredToken -> deferredToken.getUsedDeferredWords().stream()) - .collect(Collectors.toSet()) - ) + localContext + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getUsedDeferredWords().stream()) + .collect(Collectors.toSet()) + ) .contains("deferred"); } @Test public void itDefersOnImmutableMode() { - expectedTemplateInterpreter.assertExpectedOutput("defers-on-immutable-mode"); + expectedTemplateInterpreter.assertExpectedOutput("defers-on-immutable-mode/test"); } @Test public void itDoesntAffectParentFromEagerIf() { expectedTemplateInterpreter.assertExpectedOutput( - "doesnt-affect-parent-from-eager-if" + "doesnt-affect-parent-from-eager-if/test" ); } @Test public void itDefersEagerChildScopedVars() { - expectedTemplateInterpreter.assertExpectedOutput("defers-eager-child-scoped-vars"); + expectedTemplateInterpreter.assertExpectedOutput( + "defers-eager-child-scoped-vars/test" + ); } @Test public void itSetsMultipleVarsDeferredInChild() { expectedTemplateInterpreter.assertExpectedOutput( - "sets-multiple-vars-deferred-in-child" + "sets-multiple-vars-deferred-in-child/test" ); } @@ -544,26 +549,31 @@ public void itSetsMultipleVarsDeferredInChild() { public void itSetsMultipleVarsDeferredInChildSecondPass() { localContext.put("deferred", true); expectedTemplateInterpreter.assertExpectedOutput( - "sets-multiple-vars-deferred-in-child.expected" + "sets-multiple-vars-deferred-in-child/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "sets-multiple-vars-deferred-in-child/test.expected" ); } @Test public void itDoesntDoubleAppendInDeferredIfTag() { expectedTemplateInterpreter.assertExpectedOutput( - "doesnt-double-append-in-deferred-if-tag" + "doesnt-double-append-in-deferred-if-tag/test" ); } @Test public void itPrependsSetIfStateChanges() { - expectedTemplateInterpreter.assertExpectedOutput("prepends-set-if-state-changes"); + expectedTemplateInterpreter.assertExpectedOutput( + "prepends-set-if-state-changes/test" + ); } @Test public void itHandlesLoopVarAgainstDeferredInLoop() { expectedTemplateInterpreter.assertExpectedOutput( - "handles-loop-var-against-deferred-in-loop" + "handles-loop-var-against-deferred-in-loop/test" ); } @@ -571,10 +581,10 @@ public void itHandlesLoopVarAgainstDeferredInLoop() { public void itHandlesLoopVarAgainstDeferredInLoopSecondPass() { localContext.put("deferred", "resolved"); expectedTemplateInterpreter.assertExpectedOutput( - "handles-loop-var-against-deferred-in-loop.expected" + "handles-loop-var-against-deferred-in-loop/test.expected" ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "handles-loop-var-against-deferred-in-loop.expected" + "handles-loop-var-against-deferred-in-loop/test.expected" ); } @@ -584,7 +594,7 @@ public void itDefersMacroForDoAndPrint() { localContext.put("first", 10); localContext.put("deferred2", DeferredValue.instance()); String deferredOutput = expectedTemplateInterpreter.assertExpectedOutput( - "defers-macro-for-do-and-print" + "defers-macro-for-do-and-print/test" ); Object myList = localContext.get("my_list"); assertThat(myList).isInstanceOf(DeferredValue.class); @@ -609,19 +619,19 @@ public void itDefersMacroForDoAndPrint() { @Test public void itDefersMacroInFor() { localContext.put("my_list", new PyList(new ArrayList<>())); - expectedTemplateInterpreter.assertExpectedOutput("defers-macro-in-for"); + expectedTemplateInterpreter.assertExpectedOutput("defers-macro-in-for/test"); } @Test public void itDefersMacroInIf() { localContext.put("my_list", new PyList(new ArrayList<>())); - expectedTemplateInterpreter.assertExpectedOutput("defers-macro-in-if"); + expectedTemplateInterpreter.assertExpectedOutput("defers-macro-in-if/test"); } @Test public void itPutsDeferredImportedMacroInOutput() { expectedTemplateInterpreter.assertExpectedOutput( - "puts-deferred-imported-macro-in-output" + "puts-deferred-imported-macro-in-output/test" ); } @@ -629,17 +639,17 @@ public void itPutsDeferredImportedMacroInOutput() { public void itPutsDeferredImportedMacroInOutputSecondPass() { localContext.put("deferred", 1); expectedTemplateInterpreter.assertExpectedOutput( - "puts-deferred-imported-macro-in-output.expected" + "puts-deferred-imported-macro-in-output/test.expected" ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "puts-deferred-imported-macro-in-output.expected" + "puts-deferred-imported-macro-in-output/test.expected" ); } @Test public void itPutsDeferredFromedMacroInOutput() { expectedTemplateInterpreter.assertExpectedOutput( - "puts-deferred-fromed-macro-in-output" + "puts-deferred-fromed-macro-in-output/test" ); } @@ -647,38 +657,44 @@ public void itPutsDeferredFromedMacroInOutput() { public void itEagerlyDefersMacro() { localContext.put("foo", "I am foo"); localContext.put("bar", "I am bar"); - expectedTemplateInterpreter.assertExpectedOutput("eagerly-defers-macro"); + expectedTemplateInterpreter.assertExpectedOutput("eagerly-defers-macro/test"); } @Test public void itEagerlyDefersMacroSecondPass() { + localContext.put("foo", "I am foo"); + localContext.put("bar", "I am bar"); localContext.put("deferred", true); - expectedTemplateInterpreter.assertExpectedOutput("eagerly-defers-macro.expected"); + expectedTemplateInterpreter.assertExpectedOutput( + "eagerly-defers-macro/test.expected" + ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "eagerly-defers-macro.expected" + "eagerly-defers-macro/test.expected" ); } @Test public void itLoadsImportedMacroSyntax() { - expectedTemplateInterpreter.assertExpectedOutput("loads-imported-macro-syntax"); + expectedTemplateInterpreter.assertExpectedOutput("loads-imported-macro-syntax/test"); } @Test public void itDefersCaller() { - expectedTemplateInterpreter.assertExpectedOutput("defers-caller"); + expectedTemplateInterpreter.assertExpectedOutput("defers-caller/test"); } @Test public void itDefersCallerSecondPass() { localContext.put("deferred", "foo"); - expectedTemplateInterpreter.assertExpectedOutput("defers-caller.expected"); - expectedTemplateInterpreter.assertExpectedNonEagerOutput("defers-caller.expected"); + expectedTemplateInterpreter.assertExpectedOutput("defers-caller/test.expected"); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "defers-caller/test.expected" + ); } @Test public void itDefersMacroInExpression() { - expectedTemplateInterpreter.assertExpectedOutput("defers-macro-in-expression"); + expectedTemplateInterpreter.assertExpectedOutput("defers-macro-in-expression/test"); } @Test @@ -686,79 +702,83 @@ public void itDefersMacroInExpressionSecondPass() { interpreter.resolveELExpression("(range(0,1))", -1); localContext.put("deferred", 5); expectedTemplateInterpreter.assertExpectedOutput( - "defers-macro-in-expression.expected" + "defers-macro-in-expression/test.expected" ); localContext.put("deferred", 5); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "defers-macro-in-expression.expected" + "defers-macro-in-expression/test.expected" ); } @Test public void itHandlesDeferredInIfchanged() { - expectedTemplateInterpreter.assertExpectedOutput("handles-deferred-in-ifchanged"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferred-in-ifchanged/test" + ); } @Test public void itDefersIfchanged() { - expectedTemplateInterpreter.assertExpectedOutput("defers-ifchanged"); + expectedTemplateInterpreter.assertExpectedOutput("defers-ifchanged/test"); } @Test public void itHandlesCycleInDeferredFor() { - expectedTemplateInterpreter.assertExpectedOutput("handles-cycle-in-deferred-for"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-cycle-in-deferred-for/test" + ); } @Test public void itHandlesCycleInDeferredForSecondPass() { localContext.put("deferred", new String[] { "foo", "bar", "foobar", "baz" }); expectedTemplateInterpreter.assertExpectedOutput( - "handles-cycle-in-deferred-for.expected" + "handles-cycle-in-deferred-for/test.expected" ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "handles-cycle-in-deferred-for.expected" + "handles-cycle-in-deferred-for/test.expected" ); } @Test public void itHandlesDeferredInCycle() { - expectedTemplateInterpreter.assertExpectedOutput("handles-deferred-in-cycle"); + expectedTemplateInterpreter.assertExpectedOutput("handles-deferred-in-cycle/test"); } @Test public void itHandlesDeferredCycleAs() { - expectedTemplateInterpreter.assertExpectedOutput("handles-deferred-cycle-as"); + expectedTemplateInterpreter.assertExpectedOutput("handles-deferred-cycle-as/test"); } @Test public void itHandlesDeferredCycleAsSecondPass() { localContext.put("deferred", "hello"); expectedTemplateInterpreter.assertExpectedOutput( - "handles-deferred-cycle-as.expected" + "handles-deferred-cycle-as/test.expected" ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "handles-deferred-cycle-as.expected" + "handles-deferred-cycle-as/test.expected" ); } @Test public void itHandlesNonDeferringCycles() { - expectedTemplateInterpreter.assertExpectedOutput("handles-non-deferring-cycles"); + expectedTemplateInterpreter.assertExpectedOutput("handles-non-deferring-cycles/test"); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "handles-non-deferring-cycles" + "handles-non-deferring-cycles/test" ); } @Test public void itHandlesAutoEscape() { localContext.put("myvar", "foo < bar"); - expectedTemplateInterpreter.assertExpectedOutput("handles-auto-escape"); + expectedTemplateInterpreter.assertExpectedOutput("handles-auto-escape/test"); } @Test public void itWrapsCertainOutputInRaw() { - JinjavaConfig config = JinjavaConfig - .newBuilder() + JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withRandomNumberGeneratorStrategy(RandomNumberGeneratorStrategy.DEFERRED) .withExecutionMode(EagerExecutionMode.instance()) .withNestedInterpretationEnabled(false) @@ -775,8 +795,9 @@ public void itWrapsCertainOutputInRaw() { JinjavaInterpreter.pushCurrent(noNestedInterpreter); try { - new ExpectedTemplateInterpreter(jinjava, noNestedInterpreter, "eager") - .assertExpectedOutput("wraps-certain-output-in-raw"); + ExpectedTemplateInterpreter + .withSensibleCurrentPath(jinjava, noNestedInterpreter, "eager") + .assertExpectedOutput("wraps-certain-output-in-raw/test"); assertThat(noNestedInterpreter.getErrors()).isEmpty(); } finally { JinjavaInterpreter.popCurrent(); @@ -786,7 +807,7 @@ public void itWrapsCertainOutputInRaw() { @Test public void itHandlesDeferredImportVars() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "handles-deferred-import-vars" + "handles-deferred-import-vars/test" ); } @@ -794,39 +815,49 @@ public void itHandlesDeferredImportVars() { public void itHandlesDeferredImportVarsSecondPass() { localContext.put("deferred", 1); expectedTemplateInterpreter.assertExpectedOutput( - "handles-deferred-import-vars.expected" + "handles-deferred-import-vars/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "handles-deferred-import-vars/test.expected" ); } @Test public void itHandlesNonDeferredImportVars() { expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "handles-non-deferred-import-vars" + "handles-non-deferred-import-vars/test" + ); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-non-deferred-import-vars/test" ); - expectedTemplateInterpreter.assertExpectedOutput("handles-non-deferred-import-vars"); } @Test public void itHandlesDeferredFromImportAs() { - expectedTemplateInterpreter.assertExpectedOutput("handles-deferred-from-import-as"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferred-from-import-as/test" + ); } @Test public void itHandlesDeferredFromImportAsSecondPass() { localContext.put("deferred", 1); expectedTemplateInterpreter.assertExpectedOutput( - "handles-deferred-from-import-as.expected" + "handles-deferred-from-import-as/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "handles-deferred-from-import-as/test.expected" ); } @Test public void itPreservesValueSetInIf() { - expectedTemplateInterpreter.assertExpectedOutput("preserves-value-set-in-if"); + expectedTemplateInterpreter.assertExpectedOutput("preserves-value-set-in-if/test"); } @Test public void itHandlesCycleWithQuote() { - expectedTemplateInterpreter.assertExpectedOutput("handles-cycle-with-quote"); + expectedTemplateInterpreter.assertExpectedOutput("handles-cycle-with-quote/test"); } @Test @@ -834,28 +865,33 @@ public void itHandlesUnknownFunctionErrors() { JinjavaInterpreter eagerInterpreter = new JinjavaInterpreter( jinjava, jinjava.getGlobalContextCopy(), - JinjavaConfig.newBuilder().withExecutionMode(EagerExecutionMode.instance()).build() + BaseJinjavaTest + .newConfigBuilder() + .withExecutionMode(EagerExecutionMode.instance()) + .build() ); JinjavaInterpreter defaultInterpreter = new JinjavaInterpreter( jinjava, jinjava.getGlobalContextCopy(), - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(DefaultExecutionMode.instance()) .build() ); try { JinjavaInterpreter.pushCurrent(eagerInterpreter); - new ExpectedTemplateInterpreter(jinjava, eagerInterpreter, "eager") - .assertExpectedOutput("handles-unknown-function-errors"); + ExpectedTemplateInterpreter + .withSensibleCurrentPath(jinjava, eagerInterpreter, "eager") + .assertExpectedOutput("handles-unknown-function-errors/test"); } finally { JinjavaInterpreter.popCurrent(); } try { JinjavaInterpreter.pushCurrent(defaultInterpreter); - new ExpectedTemplateInterpreter(jinjava, defaultInterpreter, "eager") - .assertExpectedOutput("handles-unknown-function-errors"); + ExpectedTemplateInterpreter + .withSensibleCurrentPath(jinjava, defaultInterpreter, "eager") + .assertExpectedOutput("handles-unknown-function-errors/test"); } finally { JinjavaInterpreter.popCurrent(); } @@ -865,80 +901,98 @@ public void itHandlesUnknownFunctionErrors() { @Test public void itKeepsMaxMacroRecursionDepth() { - expectedTemplateInterpreter.assertExpectedOutput("keeps-max-macro-recursion-depth"); + expectedTemplateInterpreter.assertExpectedOutput( + "keeps-max-macro-recursion-depth/test" + ); } @Test public void itHandlesComplexRaw() { - expectedTemplateInterpreter.assertExpectedOutput("handles-complex-raw"); + expectedTemplateInterpreter.assertExpectedOutput("handles-complex-raw/test"); } @Test public void itHandlesDeferredInNamespace() { - expectedTemplateInterpreter.assertExpectedOutput("handles-deferred-in-namespace"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferred-in-namespace/test" + ); } @Test public void itHandlesDeferredInNamespaceSecondPass() { localContext.put("deferred", "resolved"); expectedTemplateInterpreter.assertExpectedOutput( - "handles-deferred-in-namespace.expected" + "handles-deferred-in-namespace/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "handles-deferred-in-namespace/test.expected" ); } @Test public void itHandlesClashingNameInMacro() { - expectedTemplateInterpreter.assertExpectedOutput("handles-clashing-name-in-macro"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-clashing-name-in-macro/test" + ); } @Test public void itHandlesClashingNameInMacroSecondPass() { localContext.put("deferred", 0); expectedTemplateInterpreter.assertExpectedOutput( - "handles-clashing-name-in-macro.expected" + "handles-clashing-name-in-macro/test.expected" ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "handles-clashing-name-in-macro.expected" + "handles-clashing-name-in-macro/test.expected" ); } @Test public void itHandlesBlockSetInDeferredIf() { - expectedTemplateInterpreter.assertExpectedOutput("handles-block-set-in-deferred-if"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-block-set-in-deferred-if/test" + ); } @Test public void itHandlesBlockSetInDeferredIfSecondPass() { localContext.put("deferred", 1); expectedTemplateInterpreter.assertExpectedOutput( - "handles-block-set-in-deferred-if.expected" + "handles-block-set-in-deferred-if/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "handles-block-set-in-deferred-if/test.expected" ); } @Test public void itDoesntOverwriteElif() { - expectedTemplateInterpreter.assertExpectedOutput("doesnt-overwrite-elif"); + expectedTemplateInterpreter.assertExpectedOutput("doesnt-overwrite-elif/test"); } @Test public void itHandlesSetAndModifiedInFor() { - expectedTemplateInterpreter.assertExpectedOutput("handles-set-and-modified-in-for"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-set-and-modified-in-for/test" + ); } @Test public void itHandlesSetInFor() { - expectedTemplateInterpreter.assertExpectedOutput("handles-set-in-for"); + expectedTemplateInterpreter.assertExpectedOutput("handles-set-in-for/test"); } @Test public void itHandlesDeferringLoopVariable() { - expectedTemplateInterpreter.assertExpectedOutput("handles-deferring-loop-variable"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferring-loop-variable/test" + ); } @Test public void itDefersChangesWithinDeferredSetBlock() { - expectedTemplateInterpreter.assertExpectedOutput( - "defers-changes-within-deferred-set-block" + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "defers-changes-within-deferred-set-block/test" ); } @@ -946,225 +1000,822 @@ public void itDefersChangesWithinDeferredSetBlock() { public void itDefersChangesWithinDeferredSetBlockSecondPass() { localContext.put("deferred", 1); expectedTemplateInterpreter.assertExpectedOutput( - "defers-changes-within-deferred-set-block.expected" + "defers-changes-within-deferred-set-block/test.expected" ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "defers-changes-within-deferred-set-block.expected" + "defers-changes-within-deferred-set-block/test.expected" ); } + @Test + public void itHandlesImportWithMacrosInDeferredIf() { + String template = expectedTemplateInterpreter.getFixtureTemplate( + "handles-import-with-macros-in-deferred-if/test" + ); + JinjavaInterpreter.getCurrent().render(template); + assertThat(JinjavaInterpreter.getCurrent().getContext().getDeferredNodes()) + .isNotEmpty(); + } + @Test public void itHandlesImportInDeferredIf() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "handles-import-in-deferred-if" + "handles-import-in-deferred-if/test" ); } @Test public void itAllowsMetaContextVarOverriding() { - interpreter.getContext().getMetaContextVariables().add("meta"); + interpreter.getContext().addMetaContextVariables(Collections.singleton("meta")); interpreter.getContext().put("meta", "META"); - expectedTemplateInterpreter.assertExpectedOutput( - "allows-meta-context-var-overriding" + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "allows-meta-context-var-overriding/test" ); } @Test public void itHandlesValueModifiedInMacro() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "handles-value-modified-in-macro" + "handles-value-modified-in-macro/test" ); } @Test public void itHandlesDoubleImportModification() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "handles-double-import-modification" + "handles-double-import-modification/test" ); } @Test public void itHandlesDoubleImportModificationSecondPass() { interpreter.getContext().put("deferred", false); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-double-import-modification/test.expected" + ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "handles-double-import-modification.expected" + "handles-double-import-modification/test.expected" ); } @Test public void itHandlesSameNameImportVar() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "handles-same-name-import-var" + "handles-same-name-import-var/test" + ); + } + + @Test + public void itHandlesSameNameImportVarSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-same-name-import-var/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "handles-same-name-import-var/test.expected" ); } @Test public void itReconstructsTypesProperly() { - expectedTemplateInterpreter.assertExpectedOutput("reconstructs-types-properly"); + expectedTemplateInterpreter.assertExpectedOutput("reconstructs-types-properly/test"); } @Test public void itRunsForLoopInsideDeferredForLoop() { expectedTemplateInterpreter.assertExpectedOutput( - "runs-for-loop-inside-deferred-for-loop" + "runs-for-loop-inside-deferred-for-loop/test" ); } @Test public void itModifiesVariableInDeferredMacro() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "modifies-variable-in-deferred-macro" + "modifies-variable-in-deferred-macro/test" ); } @Test public void itRevertsSimple() { - expectedTemplateInterpreter.assertExpectedOutput("reverts-simple"); + expectedTemplateInterpreter.assertExpectedOutput("reverts-simple/test"); } @Test public void itScopesResettingBindings() { - expectedTemplateInterpreter.assertExpectedOutput("scopes-resetting-bindings"); + expectedTemplateInterpreter.assertExpectedOutput("scopes-resetting-bindings/test"); } @Test public void itReconstructsWithMultipleLoops() { - expectedTemplateInterpreter.assertExpectedOutput("reconstructs-with-multiple-loops"); + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-with-multiple-loops/test" + ); } @Test public void itFullyDefersFilteredMacro() { - expectedTemplateInterpreter.assertExpectedOutput("fully-defers-filtered-macro"); + // Macro and set tag reconstruction are flipped so not exactly idempotent, but functionally identical + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "fully-defers-filtered-macro/test" + ); } @Test public void itFullyDefersFilteredMacroSecondPass() { interpreter.getContext().put("deferred", "resolved"); expectedTemplateInterpreter.assertExpectedOutput( - "fully-defers-filtered-macro.expected" + "fully-defers-filtered-macro/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "fully-defers-filtered-macro/test.expected" ); } @Test public void itDefersLargeLoop() { - expectedTemplateInterpreter.assertExpectedOutput("defers-large-loop"); + expectedTemplateInterpreter.assertExpectedOutput("defers-large-loop/test"); } @Test public void itHandlesSetInInnerScope() { - expectedTemplateInterpreter.assertExpectedOutput("handles-set-in-inner-scope"); + expectedTemplateInterpreter.assertExpectedOutput("handles-set-in-inner-scope/test"); } @Test public void itCorrectlyDefersWithMultipleLoops() { expectedTemplateInterpreter.assertExpectedOutput( - "correctly-defers-with-multiple-loops" + "correctly-defers-with-multiple-loops/test" ); } @Test public void itRevertsModificationWithDeferredLoop() { expectedTemplateInterpreter.assertExpectedOutput( - "reverts-modification-with-deferred-loop" + "reverts-modification-with-deferred-loop/test" ); } @Test public void itReconstructsMapNode() { - expectedTemplateInterpreter.assertExpectedOutput("reconstructs-map-node"); + expectedTemplateInterpreter.assertExpectedOutput("reconstructs-map-node/test"); } @Test public void itReconstructsMapNodeSecondPass() { interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-map-node/test.expected" + ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "reconstructs-map-node.expected" + "reconstructs-map-node/test.expected" ); } @Test public void itHasProperLineStripping() { - expectedTemplateInterpreter.assertExpectedOutput("has-proper-line-stripping"); + expectedTemplateInterpreter.assertExpectedOutput("has-proper-line-stripping/test"); } @Test public void itDefersCallTagWithDeferredArgument() { expectedTemplateInterpreter.assertExpectedOutput( - "defers-call-tag-with-deferred-argument" + "defers-call-tag-with-deferred-argument/test" ); } @Test public void itDefersCallTagWithDeferredArgumentSecondPass() { interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "defers-call-tag-with-deferred-argument/test.expected" + ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "defers-call-tag-with-deferred-argument.expected" + "defers-call-tag-with-deferred-argument/test.expected" ); } @Test public void itHandlesDuplicateVariableReferenceModification() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "handles-duplicate-variable-reference-modification" + "handles-duplicate-variable-reference-modification/test" + ); + } + + @Test + public void itHandlesDuplicateVariableReferenceSpeculativeModification() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "handles-duplicate-variable-reference-speculative-modification/test" ); } @Test public void itHandlesHigherScopeReferenceModification() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "handles-higher-scope-reference-modification" + "handles-higher-scope-reference-modification/test" ); } @Test public void itHandlesHigherScopeReferenceModificationSecondPass() { interpreter.getContext().put("deferred", "b"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-higher-scope-reference-modification/test.expected" + ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "handles-higher-scope-reference-modification.expected" + "handles-higher-scope-reference-modification/test.expected" + ); + } + + @Test + public void itHandlesReferenceModificationWhenSourceIsLost() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "handles-reference-modification-when-source-is-lost/test" ); } @Test public void itDoesNotReferentialDeferForSetVars() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "does-not-referential-defer-for-set-vars" + "does-not-referential-defer-for-set-vars/test" ); } @Test public void itKeepsScopeIsolationFromForLoops() { expectedTemplateInterpreter.assertExpectedOutput( - "keeps-scope-isolation-from-for-loops" + "keeps-scope-isolation-from-for-loops/test" ); } @Test public void itDoesNotOverrideImportModificationInFor() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "does-not-override-import-modification-in-for" + "does-not-override-import-modification-in-for/test" ); } @Test public void itDoesNotOverrideImportModificationInForSecondPass() { interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "does-not-override-import-modification-in-for/test.expected" + ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "does-not-override-import-modification-in-for.expected" + "does-not-override-import-modification-in-for/test.expected" ); } @Test public void itHandlesDeferredForLoopVarFromMacro() { expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( - "handles-deferred-for-loop-var-from-macro" + "handles-deferred-for-loop-var-from-macro/test" ); } @Test public void itHandlesDeferredForLoopVarFromMacroSecondPass() { interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferred-for-loop-var-from-macro/test.expected" + ); expectedTemplateInterpreter.assertExpectedNonEagerOutput( - "handles-deferred-for-loop-var-from-macro.expected" + "handles-deferred-for-loop-var-from-macro/test.expected" ); } + + @Test + public void itReconstructsBlockSetVariablesInForLoop() { + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-block-set-variables-in-for-loop/test" + ); + } + + @Test + public void itReconstructsNullVariablesInDeferredCaller() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "reconstructs-null-variables-in-deferred-caller/test" + ); + } + + @Test + public void itReconstructsNamespaceForSetTagsUsingPeriod() { + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-namespace-for-set-tags-using-period/test" + ); + } + + @Test + public void itReconstructsNamespaceForSetTagsUsingPeriodSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-namespace-for-set-tags-using-period/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "reconstructs-namespace-for-set-tags-using-period/test.expected" + ); + } + + @Test + public void itUsesUniqueMacroNames() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "uses-unique-macro-names/test" + ); + } + + @Test + public void itUsesUniqueMacroNamesSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "uses-unique-macro-names/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "uses-unique-macro-names/test.expected" + ); + } + + @Test + public void itReconstructsWordsFromInsideNestedExpressions() { + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-words-from-inside-nested-expressions/test" + ); + } + + @Test + public void itReconstructsWordsFromInsideNestedExpressionsSecondPass() { + interpreter.getContext().put("deferred", new PyList(new ArrayList<>())); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "reconstructs-words-from-inside-nested-expressions/test.expected" + ); + } + + @Test + public void itDoesNotReconstructExtraTimes() { + expectedTemplateInterpreter.assertExpectedOutput( + "does-not-reconstruct-extra-times/test" + ); + } + + @Test + public void itAllowsModificationInResolvedForLoop() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "allows-modification-in-resolved-for-loop/test" + ); + } + + @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/test" + ); + } + + @Test + public void itRunsMacroFunctionInDeferredExecutionMode() { + expectedTemplateInterpreter.assertExpectedOutput( + "runs-macro-function-in-deferred-execution-mode/test" + ); + } + + @Test + public void itKeepsMacroModificationsInScope() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "keeps-macro-modifications-in-scope/test" + ); + } + + @Test + public void itKeepsMacroModificationsInScopeSecondPass() { + interpreter.getContext().put("deferred", true); + expectedTemplateInterpreter.assertExpectedOutput( + "keeps-macro-modifications-in-scope/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "keeps-macro-modifications-in-scope/test.expected" + ); + } + + @Test + public void itDoesNotReconstructVariableInWrongScope() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "does-not-reconstruct-variable-in-wrong-scope/test" + ); + } + + @Test + public void itDoesNotReconstructVariableInWrongScopeSecondPass() { + interpreter.getContext().put("deferred", true); + expectedTemplateInterpreter.assertExpectedOutput( + "does-not-reconstruct-variable-in-wrong-scope/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "does-not-reconstruct-variable-in-wrong-scope/test.expected" + ); + } + + @Test + public void itReconstructsDeferredVariableEventually() { + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-deferred-variable-eventually/test" + ); + } + + @Test + public void itDoesntDoubleAppendInDeferredSet() { + expectedTemplateInterpreter.assertExpectedOutput( + "doesnt-double-append-in-deferred-set/test" + ); + } + + @Test + public void itDoesntDoubleAppendInDeferredMacro() { + expectedTemplateInterpreter.assertExpectedOutput( + "doesnt-double-append-in-deferred-macro/test" + ); + } + + @Test + public void itDoesNotReconstructVariableInSetInWrongScope() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "does-not-reconstruct-variable-in-set-in-wrong-scope/test" + ); + } + + @Test + public void itRreconstructsValueUsedInDeferredImportedMacro() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "reconstructs-value-used-in-deferred-imported-macro/test" + ); + } + + @Test + public void itRreconstructsValueUsedInDeferredImportedMacroSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-value-used-in-deferred-imported-macro/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "reconstructs-value-used-in-deferred-imported-macro/test.expected" + ); + } + + @Test + public void itAllowsDeferredLazyReferenceToGetOverridden() { + expectedTemplateInterpreter.assertExpectedOutput( + "allows-deferred-lazy-reference-to-get-overridden/test" + ); + } + + @Test + public void itAllowsDeferredLazyReferenceToGetOverriddenSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "allows-deferred-lazy-reference-to-get-overridden/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "allows-deferred-lazy-reference-to-get-overridden/test.expected" + ); + } + + @Test + public void itCommitsVariablesFromDoTagWhenPartiallyResolved() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "commits-variables-from-do-tag-when-partially-resolved/test" + ); + } + + @Test + public void itCommitsVariablesFromDoTagWhenPartiallyResolvedSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "commits-variables-from-do-tag-when-partially-resolved/test.expected" + ); + expectedTemplateInterpreter.assertExpectedOutput( + "commits-variables-from-do-tag-when-partially-resolved/test.expected" + ); + } + + @Test + public void itFindsDeferredWordsInsideReconstructedString() { + expectedTemplateInterpreter.assertExpectedOutput( + "finds-deferred-words-inside-reconstructed-string/test" + ); + } + + @Test + public void itReconstructsNestedValueInStringRepresentation() { + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-nested-value-in-string-representation/test" + ); + } + + @Test + public void itReconstructsNestedValueInStringRepresentationSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-nested-value-in-string-representation/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "reconstructs-nested-value-in-string-representation/test.expected" + ); + } + + @Test + public void itDefersLoopSettingMetaContextVar() { + interpreter.getContext().addMetaContextVariables(Collections.singleton("content")); + expectedTemplateInterpreter.assertExpectedOutput( + "defers-loop-setting-meta-context-var/test" + ); + } + + @Test + public void itDefersLoopSettingMetaContextVarSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + interpreter.getContext().addMetaContextVariables(Collections.singleton("content")); + expectedTemplateInterpreter.assertExpectedOutput( + "defers-loop-setting-meta-context-var/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "defers-loop-setting-meta-context-var/test.expected" + ); + } + + @Test + public void itAllowsVariableSharingAliasName() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "allows-variable-sharing-alias-name/test" + ); + } + + @Test + public void itFailsOnModificationInAliasedMacro() { + String input = expectedTemplateInterpreter.getFixtureTemplate( + "fails-on-modification-in-aliased-macro/test" + ); + interpreter.render(input); + // We don't support this + assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); + } + + @Test + public void itHandlesDeferredModificationInCaller() { + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferred-modification-in-caller/test" + ); + } + + @Test + public void itHandlesDeferredModificationInCallerSecondPass() { + interpreter.getContext().put("deferred", "c"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferred-modification-in-caller/test.expected" + ); + } + + @Test + public void itPreservesRawInsideDeferredSetBlock() { + expectedTemplateInterpreter.assertExpectedOutput( + "preserves-raw-inside-deferred-set-block/test" + ); + } + + @Test + public void itReconstructsAliasedMacro() { + expectedTemplateInterpreter.assertExpectedOutput("reconstructs-aliased-macro/test"); + } + + @Test + public void itReconstructsAliasedMacroSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-aliased-macro/test.expected" + ); + } + + @Test + public void itReconstructsBlockPathWhenDeferred() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "reconstructs-block-path-when-deferred/test" + ); + } + + @Test + public void itReconstructsBlockPathWhenDeferredSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-block-path-when-deferred/test.expected" + ); + } + + @Test + public void itReconstructsBlockPathWhenDeferredNested() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "reconstructs-block-path-when-deferred-nested/test" + ); + } + + @Test + public void itReconstructsBlockPathWhenDeferredNestedSecondPass() { + interpreter.getContext().put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "reconstructs-block-path-when-deferred-nested/test.expected" + ); + } + + @Test + public void itKeepsMetaContextVariablesThroughImport() { + setupWithExecutionMode( + new EagerExecutionMode() { + @Override + public void prepareContext(Context context) { + super.prepareContext(context); + context.addMetaContextVariables(Collections.singleton("meta")); + } + } + ); + interpreter.getContext().put("meta", new ArrayList<>()); + expectedTemplateInterpreter.assertExpectedOutput( + "keeps-meta-context-variables-through-import/test" + ); + } + + @Test + public void itWrapsMacroThatWouldChangeCurrentPathInChildScope() { + interpreter + .getContext() + .put(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, "starting path"); + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "wraps-macro-that-would-change-current-path-in-child-scope/test" + ); + } + + @Test + public void itHandlesDeferredBreakInForLoop() { + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferred-break-in-for-loop/test" + ); + } + + @Test + public void itHandlesDeferredBreakInForLoopSecondPass() { + localContext.put("deferred", 1); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferred-break-in-for-loop/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "handles-deferred-break-in-for-loop/test.expected" + ); + } + + @Test + public void itHandlesBreakInDeferredForLoop() { + expectedTemplateInterpreter.assertExpectedOutput( + "handles-break-in-deferred-for-loop/test" + ); + } + + @Test + public void itHandlesBreakInDeferredForLoopSecondPass() { + localContext.put("deferred", List.of(0, 1, 2, 3, 4, 5)); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-break-in-deferred-for-loop/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "handles-break-in-deferred-for-loop/test.expected" + ); + } + + @Test + public void itHandlesDeferredContinueInForLoop() { + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferred-continue-in-for-loop/test" + ); + } + + @Test + public void itHandlesDeferredContinueInForLoopSecondPass() { + localContext.put("deferred", 2); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferred-continue-in-for-loop/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "handles-deferred-continue-in-for-loop/test.expected" + ); + } + + @Test + public void itHandlesContinueInDeferredForLoop() { + expectedTemplateInterpreter.assertExpectedOutput( + "handles-continue-in-deferred-for-loop/test" + ); + } + + @Test + public void itHandlesContinueInDeferredForLoopSecondPass() { + localContext.put("deferred", List.of(0, 1, 2, 3, 4, 5)); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-continue-in-deferred-for-loop/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "handles-continue-in-deferred-for-loop/test.expected" + ); + } + + @Test + public void itReconstructsFromedMacro() { + expectedTemplateInterpreter.assertExpectedOutput("reconstructs-fromed-macro/test"); + } + + @Test + public void itHandlesModifiedIncludePath() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "handles-modified-include-path/test" + ); + } + + @Test + public void itHandlesModifiedIncludePathSecondPass() { + localContext.put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-modified-include-path/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "handles-modified-include-path/test.expected" + ); + } + + @Test + public void itDoesNotStackOverflowTryingToBuildHashcode() { + expectedTemplateInterpreter.assertExpectedOutput( + "does-not-stack-overflow-trying-to-build-hashcode/test" + ); + } + + @Test + public void itHandlesDeferredValueInRenderFilter() { + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferred-value-in-render-filter/test" + ); + } + + @Test + public void itHandlesDeferredUsedInMultipleBlockLevels() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "handles-deferred-used-in-multiple-block-levels/test" + ); + } + + @Test + public void itHandlesDeferredUsedInMultipleBlockLevelsSecondPass() { + localContext.put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "handles-deferred-used-in-multiple-block-levels/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "handles-deferred-used-in-multiple-block-levels/test.expected" + ); + } + + @Test + public void itDoesNotDeferBlockWhenOnlyMiddleDefers() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "does-not-defer-block-when-only-middle-defers/test" + ); + } + + @Test + public void itDoesNotDeferBlockWhenOnlyMiddleDefersSecondPass() { + localContext.put("deferred", "resolved"); + expectedTemplateInterpreter.assertExpectedOutput( + "does-not-defer-block-when-only-middle-defers/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "does-not-defer-block-when-only-middle-defers/test.expected" + ); + } + + @Test + public void itPreservesBlocksForReconstructionOrder() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "preserves-blocks-for-reconstruction-order/test" + ); + } + + @Test + public void itPreservesBlocksForReconstructionOrderSecondPhase() { + localContext.put("deferred", "resolved"); + String twoPhaseOutput = expectedTemplateInterpreter.assertExpectedOutput( + "preserves-blocks-for-reconstruction-order/test.expected" + ); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "preserves-blocks-for-reconstruction-order/test.expected" + ); + // Sanity check + assertThat(twoPhaseOutput) + .isEqualToIgnoringWhitespace( + expectedTemplateInterpreter.renderTemplate( + "preserves-blocks-for-reconstruction-order/test" + ) + ); + } } diff --git a/src/test/java/com/hubspot/jinjava/ExpectedNodeInterpreter.java b/src/test/java/com/hubspot/jinjava/ExpectedNodeInterpreter.java index 3bb712a01..ca438fed1 100644 --- a/src/test/java/com/hubspot/jinjava/ExpectedNodeInterpreter.java +++ b/src/test/java/com/hubspot/jinjava/ExpectedNodeInterpreter.java @@ -12,6 +12,7 @@ import java.nio.charset.StandardCharsets; public class ExpectedNodeInterpreter { + private JinjavaInterpreter interpreter; private Tag tag; private String path; @@ -25,7 +26,8 @@ public ExpectedNodeInterpreter(JinjavaInterpreter interpreter, Tag tag, String p public String assertExpectedOutput(String name) { TagNode tagNode = (TagNode) fixture(name); String output = tag.interpret(tagNode, interpreter); - assertThat(output.trim()).isEqualTo(expected(name).trim()); + assertThat(ExpectedTemplateInterpreter.prettify(output.trim())) + .isEqualTo(ExpectedTemplateInterpreter.prettify(expected(name).trim())); return output; } @@ -33,9 +35,11 @@ public Node fixture(String name) { try { return new TreeParser( interpreter, - Resources.toString( - Resources.getResource(String.format("%s/%s.jinja", path, name)), - StandardCharsets.UTF_8 + ExpectedTemplateInterpreter.simplify( + Resources.toString( + Resources.getResource(String.format("%s/%s.jinja", path, name)), + StandardCharsets.UTF_8 + ) ) ) .buildTree() @@ -48,9 +52,11 @@ public Node fixture(String name) { public String expected(String name) { try { - return Resources.toString( - Resources.getResource(String.format("%s/%s.expected.jinja", path, name)), - StandardCharsets.UTF_8 + return ExpectedTemplateInterpreter.simplify( + Resources.toString( + Resources.getResource(String.format("%s/%s.expected.jinja", path, name)), + StandardCharsets.UTF_8 + ) ); } catch (IOException e) { throw new RuntimeException(e); diff --git a/src/test/java/com/hubspot/jinjava/ExpectedTemplateInterpreter.java b/src/test/java/com/hubspot/jinjava/ExpectedTemplateInterpreter.java index 88e631fd7..8042bfc2b 100644 --- a/src/test/java/com/hubspot/jinjava/ExpectedTemplateInterpreter.java +++ b/src/test/java/com/hubspot/jinjava/ExpectedTemplateInterpreter.java @@ -5,14 +5,18 @@ import com.google.common.base.Charsets; import com.google.common.io.Resources; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; +import com.hubspot.jinjava.loader.RelativePathResolver; import com.hubspot.jinjava.mode.DefaultExecutionMode; import java.io.IOException; import java.nio.charset.StandardCharsets; public class ExpectedTemplateInterpreter { + private Jinjava jinjava; private JinjavaInterpreter interpreter; private String path; + private boolean sensibleCurrentPath = false; public ExpectedTemplateInterpreter( Jinjava jinjava, @@ -24,55 +28,141 @@ public ExpectedTemplateInterpreter( this.path = path; } + public static ExpectedTemplateInterpreter withSensibleCurrentPath( + Jinjava jinjava, + JinjavaInterpreter interpreter, + String path + ) { + return new ExpectedTemplateInterpreter(jinjava, interpreter, path, true); + } + + private ExpectedTemplateInterpreter( + Jinjava jinjava, + JinjavaInterpreter interpreter, + String path, + boolean sensibleCurrentPath + ) { + this.jinjava = jinjava; + this.interpreter = interpreter; + this.path = path; + this.sensibleCurrentPath = sensibleCurrentPath; + } + public String assertExpectedOutput(String name) { - String template = getFixtureTemplate(name); - String output = JinjavaInterpreter.getCurrent().render(template); - assertThat(output.trim()).isEqualTo(expected(name).trim()); - assertThat(JinjavaInterpreter.getCurrent().render(output).trim()) - .isEqualTo(expected(name).trim()); + String output = renderTemplate(name); + assertThat(JinjavaInterpreter.getCurrent().getContext().getDeferredNodes()) + .as("Ensure no deferred nodes were created") + .isEmpty(); + assertThat(prettify(output.trim())).isEqualTo(prettify(expected(name).trim())); + assertThat(prettify(JinjavaInterpreter.getCurrent().render(output).trim())) + .isEqualTo(prettify(expected(name).trim())); return output; } - public String assertExpectedOutputNonIdempotent(String name) { + public String renderTemplate(String name) { String template = getFixtureTemplate(name); - String output = JinjavaInterpreter.getCurrent().render(template); - assertThat(output.trim()).isEqualTo(expected(name).trim()); + return JinjavaInterpreter.getCurrent().render(template); + } + + public String assertExpectedOutputNonIdempotent(String name) { + String output = renderTemplate(name); + assertThat(JinjavaInterpreter.getCurrent().getContext().getDeferredNodes()) + .as("Ensure no deferred nodes were created") + .isEmpty(); + assertThat(prettify(output.trim())).isEqualTo(prettify(expected(name).trim())); return output; } public String assertExpectedNonEagerOutput(String name) { - JinjavaInterpreter preserveInterpreter = new JinjavaInterpreter( - jinjava, - jinjava.getGlobalContextCopy(), - JinjavaConfig - .newBuilder() - .withExecutionMode(DefaultExecutionMode.instance()) - .withNestedInterpretationEnabled(true) - .withLegacyOverrides( - LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() - ) - .withMaxMacroRecursionDepth(20) - .withEnableRecursiveMacroCalls(true) - .build() - ); + String output; try { + JinjavaInterpreter preserveInterpreter = new JinjavaInterpreter( + jinjava, + jinjava.getGlobalContextCopy(), + BaseJinjavaTest + .newConfigBuilder() + .withExecutionMode(DefaultExecutionMode.instance()) + .withNestedInterpretationEnabled(true) + .withLegacyOverrides( + LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() + ) + .withMaxMacroRecursionDepth(20) + .withEnableRecursiveMacroCalls(true) + .build() + ); JinjavaInterpreter.pushCurrent(preserveInterpreter); - preserveInterpreter.getContext().putAll(interpreter.getContext()); - String template = getFixtureTemplate(name); - String output = JinjavaInterpreter.getCurrent().render(template); - assertThat(output.trim()).isEqualTo(expected(name).trim()); - return output; + try (InterpreterScopeClosable ignored = preserveInterpreter.enterScope()) { + preserveInterpreter.getContext().putAll(interpreter.getContext()); + String template = getFixtureTemplate(name); + 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()); + } } finally { JinjavaInterpreter.popCurrent(); } + if (name.contains(".expected")) { + String originalName = name.replace(".expected", ""); + try { + JinjavaInterpreter preserveInterpreter = new JinjavaInterpreter( + jinjava, + jinjava.getGlobalContextCopy(), + BaseJinjavaTest + .newConfigBuilder() + .withExecutionMode(DefaultExecutionMode.instance()) + .withNestedInterpretationEnabled(true) + .withLegacyOverrides( + LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() + ) + .withMaxMacroRecursionDepth(20) + .withEnableRecursiveMacroCalls(true) + .build() + ); + JinjavaInterpreter.pushCurrent(preserveInterpreter); + + preserveInterpreter.getContext().putAll(interpreter.getContext()); + String template = getFixtureTemplate(originalName); + try (InterpreterScopeClosable ignored = preserveInterpreter.enterScope()) { + output = JinjavaInterpreter.getCurrent().render(template); + assertThat(JinjavaInterpreter.getCurrent().getContext().getDeferredNodes()) + .as("Ensure no deferred nodes were created") + .isEmpty(); + assertThat(prettify(output.trim())).isEqualTo(prettify(expected(name).trim())); + } + } finally { + JinjavaInterpreter.popCurrent(); + } + } + return output; + } + + static String prettify(String string) { + return string.replaceAll("([}%]})([^\\s])", "$1\\\\\n$2"); } public String getFixtureTemplate(String name) { try { - return Resources.toString( - Resources.getResource(String.format("%s/%s.jinja", path, name)), - StandardCharsets.UTF_8 + if (sensibleCurrentPath) { + JinjavaInterpreter + .getCurrent() + .getContext() + .getCurrentPathStack() + .push(String.format("%s/%s.jinja", path, name), 0, 0); + interpreter + .getContext() + .put( + RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, + String.format("%s/%s.jinja", path, name) + ); + } + return simplify( + Resources.toString( + Resources.getResource(String.format("%s/%s.jinja", path, name)), + StandardCharsets.UTF_8 + ) ); } catch (IOException e) { throw new RuntimeException(e); @@ -81,15 +171,21 @@ public String getFixtureTemplate(String name) { private String expected(String name) { try { - return Resources.toString( - Resources.getResource(String.format("%s/%s.expected.jinja", path, name)), - StandardCharsets.UTF_8 + return simplify( + Resources.toString( + Resources.getResource(String.format("%s/%s.expected.jinja", path, name)), + StandardCharsets.UTF_8 + ) ); } catch (IOException e) { throw new RuntimeException(e); } } + static String simplify(String prettified) { + return prettified.replaceAll("\\\\\n\\s*", ""); + } + public String getDeferredFixtureTemplate(String templateLocation) { try { return Resources.toString( diff --git a/src/test/java/com/hubspot/jinjava/FeaturesTest.java b/src/test/java/com/hubspot/jinjava/FeaturesTest.java new file mode 100644 index 000000000..eaaa63269 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/FeaturesTest.java @@ -0,0 +1,87 @@ +package com.hubspot.jinjava; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.features.DateTimeFeatureActivationStrategy; +import com.hubspot.jinjava.features.FeatureConfig; +import com.hubspot.jinjava.features.FeatureStrategies; +import com.hubspot.jinjava.features.Features; +import com.hubspot.jinjava.interpret.Context; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import org.junit.Before; +import org.junit.Test; + +public class FeaturesTest { + + private static final String ALWAYS_OFF = "alwaysOff"; + private static final String ALWAYS_ON = "alwaysOn"; + private static final String DATE_PAST = "datePast"; + private static final String DATE_FUTURE = "dateFuture"; + private static final String DELEGATING = "delegating"; + + private Features features; + + private boolean delegateActive = false; + + private Context context = new Context(); + + @Before + public void setUp() throws Exception { + features = + new Features( + FeatureConfig + .newBuilder() + .add(ALWAYS_OFF, FeatureStrategies.INACTIVE) + .add(ALWAYS_ON, FeatureStrategies.ACTIVE) + .add( + DATE_PAST, + DateTimeFeatureActivationStrategy.of( + ZonedDateTime.of(LocalDateTime.MIN, ZoneId.systemDefault()) + ) + ) + .add( + DATE_FUTURE, + DateTimeFeatureActivationStrategy.of( + ZonedDateTime.of(LocalDateTime.MAX, ZoneId.systemDefault()) + ) + ) + .add(DELEGATING, () -> delegateActive) + .build() + ); + } + + @Test + public void itHasEnabledFeature() { + assertThat(features.isActive(ALWAYS_ON, context)).isTrue(); + } + + @Test + public void itDoesNotHaveDisabledFeature() { + assertThat(features.isActive(ALWAYS_OFF, context)).isFalse(); + } + + @Test + public void itHasPastEnabledFeature() { + assertThat(features.isActive(DATE_PAST, context)).isTrue(); + } + + @Test + public void itDoesNotHaveFutureEnabledFeature() { + assertThat(features.isActive(DATE_FUTURE, context)).isFalse(); + } + + @Test + public void itUsesDelegate() { + delegateActive = false; + assertThat(features.isActive(DELEGATING, context)).isEqualTo(delegateActive); + delegateActive = true; + assertThat(features.isActive(DELEGATING, context)).isEqualTo(delegateActive); + } + + @Test + public void itDefaultsToFalse() { + assertThat(features.isActive("unknown", context)).isFalse(); + } +} 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..f77ccbf70 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/FilterOverrideTest.java @@ -0,0 +1,23 @@ +package com.hubspot.jinjava; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.testobjects.FilterOverrideTestObjects; +import java.util.HashMap; +import org.junit.Test; + +public class FilterOverrideTest { + + @Test + public void itAllowsUsersToOverrideBuiltInFilters() { + Jinjava jinjava = new Jinjava(BaseJinjavaTest.newConfigBuilder().build()); + String template = "{{ 5 | add(6) }}"; + + assertThat(jinjava.render(template, new HashMap<>())).isEqualTo("11"); + + jinjava + .getGlobalContext() + .registerClasses(FilterOverrideTestObjects.DescriptiveAddFilter.class); + assertThat(jinjava.render(template, new HashMap<>())).isEqualTo("5 + 6 = 11"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/FullSnippetsTest.java b/src/test/java/com/hubspot/jinjava/FullSnippetsTest.java index ad7d0fff1..6dead8a02 100644 --- a/src/test/java/com/hubspot/jinjava/FullSnippetsTest.java +++ b/src/test/java/com/hubspot/jinjava/FullSnippetsTest.java @@ -17,6 +17,7 @@ import org.junit.Test; public class FullSnippetsTest { + private JinjavaInterpreter interpreter; private Jinjava jinjava; private ExpectedTemplateInterpreter expectedTemplateInterpreter; @@ -36,8 +37,7 @@ public String getString( String fullName, Charset encoding, JinjavaInterpreter interpreter - ) - throws IOException { + ) throws IOException { return Resources.toString( Resources.getResource(String.format("tags/macrotag/%s", fullName)), StandardCharsets.UTF_8 @@ -50,8 +50,8 @@ public Optional getLocationResolver() { } } ); - JinjavaConfig config = JinjavaConfig - .newBuilder() + JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withNestedInterpretationEnabled(true) .withLegacyOverrides( LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() diff --git a/src/test/java/com/hubspot/jinjava/el/ExpressionResolverPerformanceTest.java b/src/test/java/com/hubspot/jinjava/el/ExpressionResolverPerformanceTest.java index 8338a351c..1e370331f 100644 --- a/src/test/java/com/hubspot/jinjava/el/ExpressionResolverPerformanceTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ExpressionResolverPerformanceTest.java @@ -17,6 +17,7 @@ public static void main(String[] args) { } public static class PerformanceTester { + private JinjavaInterpreter interpreter; private Context context; private long startTime; diff --git a/src/test/java/com/hubspot/jinjava/el/ExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/el/ExpressionResolverTest.java index fe0f731c3..83347281a 100644 --- a/src/test/java/com/hubspot/jinjava/el/ExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ExpressionResolverTest.java @@ -3,11 +3,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; -import com.google.common.collect.ForwardingList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.Context; @@ -18,31 +18,37 @@ import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; -import com.hubspot.jinjava.objects.PyWrapper; import com.hubspot.jinjava.objects.date.PyishDate; +import com.hubspot.jinjava.testobjects.ExpressionResolverTestObjects; import java.math.BigDecimal; -import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.function.Supplier; +import org.junit.After; import org.junit.Before; import org.junit.Test; @SuppressWarnings("unchecked") public class ExpressionResolverTest { + private JinjavaInterpreter interpreter; private Context context; private Jinjava jinjava; @Before public void setup() { - jinjava = new Jinjava(); + jinjava = new Jinjava(BaseJinjavaTest.newConfigBuilder().build()); interpreter = jinjava.newInterpreter(); context = interpreter.getContext(); + JinjavaInterpreter.pushCurrent(interpreter); + } + + @After + public void teardown() { + JinjavaInterpreter.popCurrent(); } @Test @@ -70,11 +76,11 @@ public void testTuplesAreImmutable() { public void itCanCompareStrings() { context.put("foo", "white"); assertThat( - interpreter.resolveELExpression( - "'2013-12-08 16:00:00+00:00' > '2013-12-08 13:00:00+00:00'", - -1 - ) + interpreter.resolveELExpression( + "'2013-12-08 16:00:00+00:00' > '2013-12-08 13:00:00+00:00'", + -1 ) + ) .isEqualTo(Boolean.TRUE); assertThat(interpreter.resolveELExpression("foo == \"white\"", -1)) .isEqualTo(Boolean.TRUE); @@ -162,7 +168,8 @@ public void itResolvesDictValWithDotParam() { @Test public void itResolvesMapValOnCustomObject() { - MyCustomMap dict = new MyCustomMap(); + ExpressionResolverTestObjects.MyCustomMap dict = + new ExpressionResolverTestObjects.MyCustomMap(); context.put("thedict", dict); Object val = interpreter.resolveELExpression("thedict['foo']", -1); @@ -176,7 +183,8 @@ public void itResolvesMapValOnCustomObject() { @Test public void itResolvesOtherMethodsOnCustomMapObject() { - MyCustomMap dict = new MyCustomMap(); + ExpressionResolverTestObjects.MyCustomMap dict = + new ExpressionResolverTestObjects.MyCustomMap(); context.put("thedict", dict); Object val = interpreter.resolveELExpression("thedict.size", -1); @@ -189,66 +197,6 @@ public void itResolvesOtherMethodsOnCustomMapObject() { assertThat(val2.toString()).isEqualTo("[foo=bar, two=2, size=777]"); } - public static final class MyCustomMap implements Map { - Map data = ImmutableMap.of("foo", "bar", "two", "2", "size", "777"); - - @Override - public int size() { - return data.size(); - } - - @Override - public boolean isEmpty() { - return data.isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return data.containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - return data.containsValue(value); - } - - @Override - public String get(Object key) { - return data.get(key); - } - - @Override - public String put(String key, String value) { - return null; - } - - @Override - public String remove(Object key) { - return null; - } - - @Override - public void putAll(Map m) {} - - @Override - public void clear() {} - - @Override - public Set keySet() { - return data.keySet(); - } - - @Override - public Collection values() { - return data.values(); - } - - @Override - public Set> entrySet() { - return data.entrySet(); - } - } - @Test public void itResolvesInnerDictVal() { Map dict = Maps.newHashMap(); @@ -272,23 +220,6 @@ public void itResolvesInnerListVal() { assertThat(val).isEqualTo("val"); } - public static class MyCustomList extends ForwardingList implements PyWrapper { - private final List list; - - public MyCustomList(List list) { - this.list = list; - } - - @Override - protected List delegate() { - return list; - } - - public int getTotalCount() { - return list.size(); - } - } - @Test public void itRecordsFilterNames() { Object val = interpreter.resolveELExpression("2.3 | round", -1); @@ -298,7 +229,9 @@ public void itRecordsFilterNames() { @Test public void callCustomListProperty() { - List myList = new MyCustomList<>(Lists.newArrayList(1, 2, 3, 4)); + List myList = new ExpressionResolverTestObjects.MyCustomList<>( + Lists.newArrayList(1, 2, 3, 4) + ); context.put("mylist", myList); Object val = interpreter.resolveELExpression("mylist.total_count", -1); @@ -315,8 +248,8 @@ public void complexInWithOrCondition() { assertThat(interpreter.resolveELExpression("\"
    \" in bar or \"
    \" in bar", -1)) .isEqualTo(true); assertThat( - interpreter.resolveELExpression("\"\" in foo or \"\" in foo", -1) - ) + interpreter.resolveELExpression("\"\" in foo or \"\" in foo", -1) + ) .isEqualTo(false); } @@ -325,7 +258,7 @@ public void unknownProperty() { interpreter.resolveELExpression("foo", 23); assertThat(interpreter.getErrorsCopy()).isEmpty(); - context.put("foo", new Object()); + context.put("foo", ""); interpreter.resolveELExpression("foo.bar", 23); assertThat(interpreter.getErrorsCopy()).hasSize(1); @@ -350,7 +283,7 @@ public void syntaxError() { @Test public void itWrapsDates() { - context.put("myobj", new MyClass(new Date(0))); + context.put("myobj", new ExpressionResolverTestObjects.MyClass(new Date(0))); Object result = interpreter.resolveELExpression("myobj.date", -1); assertThat(result).isInstanceOf(PyishDate.class); assertThat(result.toString()).isEqualTo("1970-01-01 00:00:00"); @@ -358,7 +291,7 @@ public void itWrapsDates() { @Test public void blackListedProperties() { - context.put("myobj", new MyClass(new Date(0))); + context.put("myobj", new ExpressionResolverTestObjects.MyClass(new Date(0))); interpreter.resolveELExpression("myobj.class.methods[0]", -1); assertThat(interpreter.getErrorsCopy()).isNotEmpty(); @@ -370,29 +303,35 @@ public void blackListedProperties() { @Test public void itWillNotReturnClassObjectProperties() { - context.put("myobj", new MyClass(new Date(0))); + context.put("myobj", new ExpressionResolverTestObjects.MyClass(new Date(0))); Object clazz = interpreter.resolveELExpression("myobj.clazz", -1); assertThat(clazz).isNull(); } @Test public void blackListedMethods() { - context.put("myobj", new MyClass(new Date(0))); + context.put("myobj", new ExpressionResolverTestObjects.MyClass(new Date(0))); interpreter.resolveELExpression("myobj.wait()", -1); assertThat(interpreter.getErrorsCopy()).isNotEmpty(); TemplateError e = interpreter.getErrorsCopy().get(0); - assertThat(e.getMessage()).contains("Cannot find method 'wait'"); + assertThat(e.getMessage()) + .contains( + "Cannot find method wait with 0 parameters in class com.hubspot.jinjava.testobjects.ExpressionResolverTestObjects$MyClass" + ); } @Test public void itWillNotReturnClassObjects() { - context.put("myobj", new MyClass(new Date(0))); + context.put("myobj", new ExpressionResolverTestObjects.MyClass(new Date(0))); interpreter.resolveELExpression("myobj.getClass()", -1); assertThat(interpreter.getErrorsCopy()).isNotEmpty(); TemplateError e = interpreter.getErrorsCopy().get(0); - assertThat(e.getMessage()).contains("Cannot find method 'getClass'"); + assertThat(e.getMessage()) + .contains( + "Cannot find method getClass with 0 parameters in class com.hubspot.jinjava.testobjects.ExpressionResolverTestObjects$MyClass" + ); } @Test @@ -464,12 +403,13 @@ public void itBlocksDisabledFunctions() { ); String template = "hi {% for i in range(1, 3) %}{{i}} {% endfor %}"; + JinjavaInterpreter.popCurrent(); String rendered = jinjava.render(template, context); assertEquals("hi 1 2 ", rendered); - final JinjavaConfig config = JinjavaConfig - .newBuilder() + final JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withDisabled(disabled) .build(); @@ -503,7 +443,7 @@ public void itBlocksDisabledExpTests() { @Test public void itStoresResolvedFunctions() { context.put("datetime", 12345); - final JinjavaConfig config = JinjavaConfig.newBuilder().build(); + final JinjavaConfig config = BaseJinjavaTest.newConfigBuilder().build(); String template = "{% for i in range(1, 5) %}{{i}} {% endfor %}\n{{ unixtimestamp(datetime) }}"; final RenderResult renderResult = jinjava.renderForResult(template, context, config); @@ -514,21 +454,27 @@ public void itStoresResolvedFunctions() { @Test public void presentOptionalProperty() { - context.put("myobj", new OptionalProperty(null, "foo")); + context.put("myobj", new ExpressionResolverTestObjects.OptionalProperty(null, "foo")); assertThat(interpreter.resolveELExpression("myobj.val", -1)).isEqualTo("foo"); assertThat(interpreter.getErrorsCopy()).isEmpty(); } @Test public void emptyOptionalProperty() { - context.put("myobj", new OptionalProperty(null, null)); + context.put("myobj", new ExpressionResolverTestObjects.OptionalProperty(null, null)); assertThat(interpreter.resolveELExpression("myobj.val", -1)).isNull(); assertThat(interpreter.getErrorsCopy()).isEmpty(); } @Test public void presentNestedOptionalProperty() { - context.put("myobj", new OptionalProperty(new MyClass(new Date(0)), "foo")); + context.put( + "myobj", + new ExpressionResolverTestObjects.OptionalProperty( + new ExpressionResolverTestObjects.MyClass(new Date(0)), + "foo" + ) + ); assertThat(Objects.toString(interpreter.resolveELExpression("myobj.nested.date", -1))) .isEqualTo("1970-01-01 00:00:00"); assertThat(interpreter.getErrorsCopy()).isEmpty(); @@ -536,7 +482,7 @@ public void presentNestedOptionalProperty() { @Test public void emptyNestedOptionalProperty() { - context.put("myobj", new OptionalProperty(null, null)); + context.put("myobj", new ExpressionResolverTestObjects.OptionalProperty(null, null)); assertThat(interpreter.resolveELExpression("myobj.nested.date", -1)).isNull(); assertThat(interpreter.getErrorsCopy()).isEmpty(); } @@ -545,18 +491,24 @@ public void emptyNestedOptionalProperty() { public void presentNestedNestedOptionalProperty() { context.put( "myobj", - new NestedOptionalProperty(new OptionalProperty(new MyClass(new Date(0)), "foo")) + new ExpressionResolverTestObjects.NestedOptionalProperty( + new ExpressionResolverTestObjects.OptionalProperty( + new ExpressionResolverTestObjects.MyClass(new Date(0)), + "foo" + ) + ) ); assertThat( - Objects.toString(interpreter.resolveELExpression("myobj.nested.nested.date", -1)) - ) + Objects.toString(interpreter.resolveELExpression("myobj.nested.nested.date", -1)) + ) .isEqualTo("1970-01-01 00:00:00"); assertThat(interpreter.getErrorsCopy()).isEmpty(); } @Test public void itResolvesLazyExpressionsToTheirUnderlyingValue() { - TestClass testClass = new TestClass(); + ExpressionResolverTestObjects.TestClass testClass = + new ExpressionResolverTestObjects.TestClass(); Supplier lazyString = () -> result("hallelujah", testClass); context.put("myobj", ImmutableMap.of("test", LazyExpression.of(lazyString, ""))); @@ -577,7 +529,8 @@ public void itResolvesNullLazyExpressions() { @Test public void itResolvesSuppliersOnlyIfResolved() { - TestClass testClass = new TestClass(); + ExpressionResolverTestObjects.TestClass testClass = + new ExpressionResolverTestObjects.TestClass(); Supplier lazyString = () -> result("hallelujah", testClass); context.put( @@ -593,7 +546,8 @@ public void itResolvesSuppliersOnlyIfResolved() { @Test public void itResolvesLazyExpressionsInNested() { - Supplier lazyObject = TestClass::new; + Supplier lazyObject = + ExpressionResolverTestObjects.TestClass::new; context.put("myobj", ImmutableMap.of("test", LazyExpression.of(lazyObject, ""))); @@ -602,71 +556,68 @@ public void itResolvesLazyExpressionsInNested() { assertThat(interpreter.getErrorsCopy()).isEmpty(); } - public String result(String value, TestClass testClass) { - testClass.touch(); - return value; - } - - public static class TestClass { - private boolean touched = false; - private String name = "Amazing test class"; - - public boolean isTouched() { - return touched; - } - - public void touch() { - this.touched = true; - } + @Test + public void itResolvesAlternateExpTestSyntax() { + assertThat(interpreter.render("{% if 2 is even %}yes{% endif %}")).isEqualTo("yes"); - public String getName() { - return name; - } + assertThat( + interpreter.render("{% if exptest:even.evaluate(2, null) %}yes{% endif %}") + ) + .isEqualTo("yes"); + assertThat( + interpreter.render("{% if exptest:false.evaluate(false, null) %}yes{% endif %}") + ) + .isEqualTo("yes"); } - public static final class MyClass { - private Date date; - - MyClass(Date date) { - this.date = date; - } - - public Class getClazz() { - return this.getClass(); - } - - public Date getDate() { - return date; - } + @Test + public void itResolvesAlternateExpTestSyntaxForTrueAndFalseExpTests() { + assertThat( + interpreter.render("{% if exptest:false.evaluate(false, null) %}yes{% endif %}") + ) + .isEqualTo("yes"); + assertThat( + interpreter.render("{% if exptest:true.evaluate(true, null) %}yes{% endif %}") + ) + .isEqualTo("yes"); } - public static final class OptionalProperty { - private MyClass nested; - private String val; - - OptionalProperty(MyClass nested, String val) { - this.nested = nested; - this.val = val; - } - - public Optional getNested() { - return Optional.ofNullable(nested); - } - - public Optional getVal() { - return Optional.ofNullable(val); - } + @Test + public void itResolvesAlternateExpTestSyntaxForInExpTests() { + assertThat( + interpreter.render("{% if exptest:in.evaluate(1, null, [1]) %}yes{% endif %}") + ) + .isEqualTo("yes"); + assertThat( + interpreter.render( + "{% if exptest:in.evaluate(2, null, [1]) %}yes{% else %}no{% endif %}" + ) + ) + .isEqualTo("no"); } - public static final class NestedOptionalProperty { - private OptionalProperty nested; + @Test + public void itAddsErrorRenderingUnclosedExpression() { + interpreter.resolveELExpression("{", 1); + assertThat(interpreter.getErrors().get(0).getMessage()) + .contains( + "Error parsing '{': syntax error at position 4, encountered 'null', expected '}'" + ); + } - public NestedOptionalProperty(OptionalProperty nested) { - this.nested = nested; - } + @Test + public void itAddsInvalidInputErrorWhenArithmeticExceptionIsThrown() { + String render = interpreter.render("{% set n = 12/0|round %}{{n}}"); + assertThat(interpreter.getErrors().get(0).getMessage()) + .contains( + "ArithmeticException when resolving expression [[ 12/0|round ]]: ArithmeticException: / by zero" + ); + assertThat(interpreter.getErrors().get(0).getReason()) + .isEqualTo(ErrorReason.INVALID_INPUT); + } - public Optional getNested() { - return Optional.ofNullable(nested); - } + public String result(String value, ExpressionResolverTestObjects.TestClass testClass) { + testClass.touch(); + return value; } } diff --git a/src/test/java/com/hubspot/jinjava/el/ExtendedSyntaxBuilderTest.java b/src/test/java/com/hubspot/jinjava/el/ExtendedSyntaxBuilderTest.java index 972198642..ac7e6d85f 100644 --- a/src/test/java/com/hubspot/jinjava/el/ExtendedSyntaxBuilderTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ExtendedSyntaxBuilderTest.java @@ -5,9 +5,13 @@ import com.google.common.collect.Lists; import com.google.common.io.Resources; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.Context; +import com.hubspot.jinjava.interpret.IndexOutOfRangeException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -20,12 +24,19 @@ @SuppressWarnings("unchecked") public class ExtendedSyntaxBuilderTest { + + private static final long MAX_STRING_LENGTH = 10_000; + private Context context; private JinjavaInterpreter interpreter; @Before public void setup() { - interpreter = new Jinjava().newInterpreter(); + interpreter = + new Jinjava( + BaseJinjavaTest.newConfigBuilder().withMaxOutputSize(MAX_STRING_LENGTH).build() + ) + .newInterpreter(); JinjavaInterpreter.pushCurrent(interpreter); context = interpreter.getContext(); @@ -95,6 +106,25 @@ public void stringConcatOperator() { assertThat(val("'foo' ~ 'bar'")).isEqualTo("foobar"); } + @Test + public void itLimitsLengthInStringConcatOperator() { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < MAX_STRING_LENGTH - 1; i++) { + stringBuilder.append("0"); + } + + String longString = stringBuilder.toString(); + + context.put("longString", longString); + assertThat(val("longString ~ ''")).isEqualTo(longString); + assertThat(interpreter.getErrors()).isEmpty(); + + assertThat(val("longString ~ 'OVER'")).isNull(); + + assertThat(interpreter.getErrors().get(0).getMessage()) + .contains(OutputTooBigException.class.getSimpleName()); + } + @Test public void stringInStringOperator() { assertThat(val("'foo' in 'foobar'")).isEqualTo(true); @@ -161,22 +191,40 @@ public void literalTuple() { @Test public void mapLiteral() { - context.put("foo", "bar"); - assertThat((Map) val("{}")).isEmpty(); - Map map = (Map) val( - "{foo: foo, \"foo2\": foo, foo3: 123, foo4: 'string', foo5: {}, foo6: [1, 2]}" - ); - assertThat(map) - .contains( - entry("foo", "bar"), - entry("foo2", "bar"), - entry("foo3", 123L), - entry("foo4", "string"), - entry("foo6", Arrays.asList(1L, 2L)) + interpreter = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withMaxOutputSize(MAX_STRING_LENGTH) + .withLegacyOverrides( + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withEvaluateMapKeys(false) + .build() + ) + .build() + ) + .newInterpreter(); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter.getContext().put("foo", "bar"); + assertThat((Map) val("{}")).isEmpty(); + Map map = (Map) val( + "{foo: foo, \"foo2\": foo, foo3: 123, foo4: 'string', foo5: {}, foo6: [1, 2]}" ); - - assertThat((Map) val("{\"address\":\"123 Main - Boston, MA 02111\"}")) - .contains(entry("address", "123 Main - Boston, MA 02111")); + assertThat(map) + .contains( + entry("foo", "bar"), + entry("foo2", "bar"), + entry("foo3", 123L), + entry("foo4", "string"), + entry("foo6", Arrays.asList(1L, 2L)) + ); + + assertThat( + (Map) val("{\"address\":\"123 Main - Boston, MA 02111\"}") + ) + .contains(entry("address", "123 Main - Boston, MA 02111")); + } } @Test @@ -186,6 +234,11 @@ public void complexMapLiteral() { assertThat((Map) map.get("Boston")).contains(entry("city", "Boston")); } + @Test + public void mapLiteralWithNumericKey() { + assertThat((Map) val("{0:'test'}")).contains(entry("0", "test")); + } + @Test public void itParsesDictWithVariableRefs() { List theList = Lists.newArrayList(1L, 2L, 3L); @@ -247,6 +300,37 @@ public void listRangeSyntax() { assertThat(val("mylist[2]")).isEqualTo(3); } + @Test + public void outOfRange() { + List emptyList = Lists.newArrayList(); + context.put("emptyList", emptyList); + + // empty case + assertThat(val("emptyList.get(0)")).isNull(); + String errorMessage = "Index %d is out of range for list of size %d"; + assertThat(interpreter.getErrors().get(0).getMessage()) + .contains(String.format(errorMessage, 0, 0)); + + // negative case + assertThat(val("emptyList.get(-1)")).isNull(); + assertThat(interpreter.getErrors().get(1).getMessage()) + .contains(String.format(errorMessage, -1, 0)); + + // out of range for filled array + List theList = Lists.newArrayList(1, 2, 3); + context.put("smallList", theList); + assertThat(val("smallList.get(3)")).isNull(); + assertThat(interpreter.getErrors().get(2).getMessage()) + .contains(String.format(errorMessage, 3, 3)); + + interpreter + .getErrors() + .forEach(e -> + assertThat(e.getException().getCause()) + .isInstanceOf(IndexOutOfRangeException.class) + ); + } + @Test public void listRangeSyntaxNegativeIndices() { List theList = Lists.newArrayList(1, 2, 3, 4, 5); @@ -339,10 +423,10 @@ public void invalidPipeOperatorExpr() { @Test public void itReturnsCorrectSyntaxErrorPositions() { assertThat( - interpreter.render( - "hi {{ missing thing }}{{ missing thing }}\nI am {{ blah blabbity }} too" - ) + interpreter.render( + "hi {{ missing thing }}{{ missing thing }}\nI am {{ blah blabbity }} too" ) + ) .isEqualTo("hi \nI am too"); assertThat(interpreter.getErrorsCopy().size()).isEqualTo(3); assertThat(interpreter.getErrorsCopy().get(0).getLineno()).isEqualTo(1); diff --git a/src/test/java/com/hubspot/jinjava/el/TypeConvertingMapELResolverTest.java b/src/test/java/com/hubspot/jinjava/el/TypeConvertingMapELResolverTest.java new file mode 100644 index 000000000..931ea762c --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/el/TypeConvertingMapELResolverTest.java @@ -0,0 +1,61 @@ +package com.hubspot.jinjava.el; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; + +public class TypeConvertingMapELResolverTest { + + private TypeConvertingMapELResolver typeConvertingMapELResolver; + + @Before + public void setup() { + typeConvertingMapELResolver = new TypeConvertingMapELResolver(false); + } + + @Test + public void itResolvesProperties() { + Map values = ImmutableMap.of("1", "value1", "2", "value2"); + assertThat(typeConvertingMapELResolver.getValue(new JinjavaELContext(), values, "2")) + .isEqualTo("value2"); + } + + @Test + public void itConvertsPropertyClassWhenResolvingProperty() { + Map values = ImmutableMap.of("1", "value1", "2", "value2"); + assertThat(typeConvertingMapELResolver.getValue(new JinjavaELContext(), values, 1)) + .isEqualTo("value1"); + } + + @Test + public void itHandlesNullKeyValuesWhenResolvingProperty() { + Map values = new HashMap<>(); + values.put(null, "nullValue"); + values.put("1", "value1"); + values.put("2", "value2"); + assertThat(typeConvertingMapELResolver.getValue(new JinjavaELContext(), values, 1)) + .isEqualTo("value1"); + } + + @Test + public void itHandlesMapWithOnlyNullKey() { + Map values = new HashMap<>(); + values.put(null, "nullValue"); + assertThat(typeConvertingMapELResolver.getValue(new JinjavaELContext(), values, 1)) + .isEqualTo(null); + } + + @Test + public void itResolvesNullPropertyValue() { + Map values = new HashMap<>(); + values.put(null, "nullValue"); + values.put("1", "value1"); + values.put("2", "value2"); + assertThat(typeConvertingMapELResolver.getValue(new JinjavaELContext(), values, null)) + .isEqualTo("nullValue"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/el/ext/AdditionOperatorTest.java b/src/test/java/com/hubspot/jinjava/el/ext/AdditionOperatorTest.java index 02aa043ed..7a8ce5dc1 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/AdditionOperatorTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/AdditionOperatorTest.java @@ -3,20 +3,35 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.OutputTooBigException; import java.util.Collection; import java.util.Map; +import org.junit.After; import org.junit.Before; import org.junit.Test; @SuppressWarnings("unchecked") public class AdditionOperatorTest { + + private static final long MAX_STRING_LENGTH = 10_000; private JinjavaInterpreter interpreter; @Before public void setup() { - interpreter = new Jinjava().newInterpreter(); + interpreter = + new Jinjava( + BaseJinjavaTest.newConfigBuilder().withMaxOutputSize(MAX_STRING_LENGTH).build() + ) + .newInterpreter(); + JinjavaInterpreter.pushCurrent(interpreter); + } + + @After + public void teardown() { + JinjavaInterpreter.popCurrent(); } @Test @@ -24,6 +39,25 @@ public void itConcatsStrings() { assertThat(interpreter.resolveELExpression("'foo' + 'bar'", -1)).isEqualTo("foobar"); } + @Test + public void itLimitsLengthOfStrings() { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < MAX_STRING_LENGTH; i++) { + stringBuilder.append("0"); + } + + String first = stringBuilder.toString(); + assertThat(interpreter.resolveELExpression("'" + first + "' + ''", -1)) + .isEqualTo(first); + assertThat(interpreter.getErrors()).isEmpty(); + + assertThat(interpreter.resolveELExpression("'" + first + "' + 'TOOBIG'", -1)) + .isNull(); + + assertThat(interpreter.getErrors().get(0).getMessage()) + .contains(OutputTooBigException.class.getSimpleName()); + } + @Test public void itAddsNumbers() { assertThat(interpreter.resolveELExpression("1 + 2", -1)).isEqualTo(3L); @@ -38,30 +72,30 @@ public void itConcatsNumberWithString() { @Test public void itCombinesTwoLists() { assertThat( - (Collection) interpreter.resolveELExpression( - "['foo', 'bar'] + ['other', 'one']", - -1 - ) + (Collection) interpreter.resolveELExpression( + "['foo', 'bar'] + ['other', 'one']", + -1 ) + ) .containsExactly("foo", "bar", "other", "one"); } @Test public void itAddsToList() { assertThat( - (Collection) interpreter.resolveELExpression("['foo'] + 'bar'", -1) - ) + (Collection) interpreter.resolveELExpression("['foo'] + 'bar'", -1) + ) .containsExactly("foo", "bar"); } @Test public void itCombinesTwoDicts() { assertThat( - (Map) interpreter.resolveELExpression( - "{'k1':'v1'} + {'k2':'v2'}", - -1 - ) + (Map) interpreter.resolveELExpression( + "{'k1':'v1'} + {'k2':'v2'}", + -1 ) + ) .containsOnly(entry("k1", "v1"), entry("k2", "v2")); } } diff --git a/src/test/java/com/hubspot/jinjava/el/ext/AllowlistGroupTest.java b/src/test/java/com/hubspot/jinjava/el/ext/AllowlistGroupTest.java new file mode 100644 index 000000000..454372f5f --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/el/ext/AllowlistGroupTest.java @@ -0,0 +1,27 @@ +package com.hubspot.jinjava.el.ext; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseJinjavaTest; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; + +public class AllowlistGroupTest extends BaseJinjavaTest { + + @Test + public void itResolvesNamedParameterNameThroughAllowlist() { + Map context = new HashMap<>(); + context.put("np", new NamedParameter("greeting", "hello")); + String result = jinjava.render("{{ np.name }}", context); + assertThat(result).isEqualTo("greeting"); + } + + @Test + public void itResolvesNamedParameterValueThroughAllowlist() { + Map context = new HashMap<>(); + context.put("np", new NamedParameter("greeting", "hello")); + String result = jinjava.render("{{ np.value }}", context); + assertThat(result).isEqualTo("hello"); + } +} 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..c5bbc0d6d 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/AstDictTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/AstDictTest.java @@ -3,88 +3,104 @@ import static org.assertj.core.api.Assertions.assertThat; import com.google.common.collect.ImmutableMap; -import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateError.ErrorType; -import java.util.Map; +import com.hubspot.jinjava.testobjects.AstDictTestObjects; import java.util.Set; -import org.junit.Before; import org.junit.Test; -public class AstDictTest { - private JinjavaInterpreter interpreter; - - @Before - public void setup() { - interpreter = new Jinjava().newInterpreter(); - } +public class AstDictTest extends BaseJinjavaTest { @Test public void itGetsDictValues() { - interpreter.getContext().put("foo", ImmutableMap.of("bar", "test")); - assertThat(interpreter.resolveELExpression("foo.bar", -1)).isEqualTo("test"); + try ( + var a = JinjavaInterpreter.closeablePushCurrent(jinjava.newInterpreter()).get() + ) { + JinjavaInterpreter interpreter = a.value(); + interpreter.getContext().put("foo", ImmutableMap.of("bar", "test")); + assertThat(interpreter.resolveELExpression("foo.bar", -1)).isEqualTo("test"); + } } @Test public void itGetsDictValuesWithEnumKeys() { - interpreter.getContext().put("foo", ImmutableMap.of(ErrorType.FATAL, "test")); - assertThat(interpreter.resolveELExpression("foo.FATAL", -1)).isEqualTo("test"); + try ( + var a = JinjavaInterpreter.closeablePushCurrent(jinjava.newInterpreter()).get() + ) { + JinjavaInterpreter interpreter = a.value(); + interpreter.getContext().put("foo", ImmutableMap.of(ErrorType.FATAL, "test")); + assertThat(interpreter.resolveELExpression("foo.FATAL", -1)).isEqualTo("test"); + } } @Test public void itGetsDictValuesWithEnumKeysUsingToString() { - interpreter.getContext().put("foo", ImmutableMap.of(TestEnum.BAR, "test")); - assertThat(interpreter.resolveELExpression("foo.barName", -1)).isEqualTo("test"); + try ( + var a = JinjavaInterpreter.closeablePushCurrent(jinjava.newInterpreter()).get() + ) { + JinjavaInterpreter interpreter = a.value(); + interpreter + .getContext() + .put("foo", ImmutableMap.of(AstDictTestObjects.TestEnum.BAR, "test")); + assertThat(interpreter.resolveELExpression("foo.barName", -1)).isEqualTo("test"); + } } @Test public void itDoesItemsMethodCall() { - interpreter.getContext().put("foo", ImmutableMap.of(TestEnum.BAR, "test")); - assertThat(interpreter.resolveELExpression("foo.items()", -1)) - .isInstanceOf(Set.class); + try ( + var a = JinjavaInterpreter.closeablePushCurrent(jinjava.newInterpreter()).get() + ) { + JinjavaInterpreter interpreter = a.value(); + interpreter + .getContext() + .put("foo", ImmutableMap.of(AstDictTestObjects.TestEnum.BAR, "test")); + assertThat(interpreter.resolveELExpression("foo.items()", -1)) + .isInstanceOf(Set.class); + } } @Test - public void itHandlesEmptyMaps() { - interpreter.getContext().put("foo", ImmutableMap.of()); - assertThat(interpreter.resolveELExpression("foo.FATAL", -1)).isNull(); - assertThat(interpreter.getErrors()).isEmpty(); + public void itDoesKeysMethodCall() { + try ( + var a = JinjavaInterpreter.closeablePushCurrent(jinjava.newInterpreter()).get() + ) { + JinjavaInterpreter interpreter = a.value(); + interpreter + .getContext() + .put("foo", ImmutableMap.of(AstDictTestObjects.TestEnum.BAR, "test")); + assertThat(interpreter.resolveELExpression("foo.keys()", -1)) + .isInstanceOf(Set.class); + } } @Test - public void itGetsDictValuesWithEnumKeysInObjects() { - interpreter - .getContext() - .put("test", new TestClass(ImmutableMap.of(ErrorType.FATAL, "test"))); - assertThat(interpreter.resolveELExpression("test.my_map.FATAL", -1)) - .isEqualTo("test"); - } - - public class TestClass { - private Map myMap; - - public TestClass(Map myMap) { - this.myMap = myMap; - } - - public Map getMyMap() { - return myMap; + public void itHandlesEmptyMaps() { + try ( + var a = JinjavaInterpreter.closeablePushCurrent(jinjava.newInterpreter()).get() + ) { + JinjavaInterpreter interpreter = a.value(); + interpreter.getContext().put("foo", ImmutableMap.of()); + assertThat(interpreter.resolveELExpression("foo.FATAL", -1)).isNull(); + assertThat(interpreter.getErrors()).isEmpty(); } } - public enum TestEnum { - FOO("fooName"), - BAR("barName"); - - private String name; - - TestEnum(String name) { - this.name = name; - } - - @Override - public String toString() { - return name; + @Test + public void itGetsDictValuesWithEnumKeysInObjects() { + try ( + var a = JinjavaInterpreter.closeablePushCurrent(jinjava.newInterpreter()).get() + ) { + JinjavaInterpreter interpreter = a.value(); + interpreter + .getContext() + .put( + "test", + new AstDictTestObjects.TestClass(ImmutableMap.of(ErrorType.FATAL, "test")) + ); + assertThat(interpreter.resolveELExpression("test.my_map.FATAL", -1)) + .isEqualTo("test"); } } } diff --git a/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainParityTest.java b/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainParityTest.java new file mode 100644 index 000000000..e9ee8bb76 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainParityTest.java @@ -0,0 +1,530 @@ +package com.hubspot.jinjava.el.ext; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.LegacyOverrides; +import com.hubspot.jinjava.interpret.RenderResult; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; + +public class AstFilterChainParityTest { + + private Jinjava jinjavaOptimized; + private Jinjava jinjavaUnoptimized; + private Map context; + + @Before + public void setup() { + LegacyOverrides legacyOverrides = LegacyOverrides + .newBuilder() + .withUsePyishObjectMapper(true) + .withKeepNullableLoopValues(true) + .build(); + + jinjavaOptimized = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withEnableFilterChainOptimization(true) + .withLegacyOverrides(legacyOverrides) + .build() + ); + + jinjavaUnoptimized = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withEnableFilterChainOptimization(false) + .withLegacyOverrides(legacyOverrides) + .build() + ); + + context = new HashMap<>(); + context.put("name", " Hello World "); + context.put("text", "the quick brown fox jumps over the lazy dog"); + context.put("number", 12345); + context.put("float_num", 3.14159); + context.put("negative", -42); + context.put("items", Arrays.asList("apple", "banana", "cherry")); + context.put("empty_list", ImmutableList.of()); + context.put("numbers", Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6)); + context.put("html", ""); + context.put("null_value", null); + context.put( + "nested", + ImmutableMap.of("key", "value", "num", 100, "list", Arrays.asList(1, 2, 3)) + ); + context.put( + "objects", + Arrays.asList( + ImmutableMap.of("name", "Alice", "age", 30), + ImmutableMap.of("name", "Bob", "age", 25), + ImmutableMap.of("name", "Charlie", "age", 35) + ) + ); + context.put("mixed_case", "HeLLo WoRLd"); + context.put("whitespace", " lots of spaces "); + context.put("unicode", "héllo wörld 你好"); + context.put("special_chars", "a&bd\"e'f"); + context.put("json_string", "{\"key\": \"value\", \"num\": 42}"); + context.put("long_text", "word ".repeat(100)); + context.put("arg_value", 10); + } + + @Test + public void itProducesSameResultsForSingleFilters() { + List templates = ImmutableList.of( + "{{ name|trim }}", + "{{ name|lower }}", + "{{ name|upper }}", + "{{ name|length }}", + "{{ number|string }}", + "{{ number|abs }}", + "{{ float_num|round }}", + "{{ float_num|int }}", + "{{ items|first }}", + "{{ items|last }}", + "{{ items|length }}", + "{{ items|reverse }}", + "{{ items|sort }}", + "{{ html|escape }}", + "{{ html|e }}", + "{{ text|capitalize }}", + "{{ text|title }}", + "{{ text|wordcount }}", + "{{ negative|abs }}", + "{{ mixed_case|lower }}", + "{{ mixed_case|upper }}", + "{{ whitespace|trim }}", + "{{ unicode|upper }}", + "{{ unicode|lower }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForChainedFilters() { + List templates = ImmutableList.of( + "{{ name|trim|lower }}", + "{{ name|trim|upper }}", + "{{ name|trim|lower|capitalize }}", + "{{ name|trim|lower|upper }}", + "{{ text|upper|lower|capitalize }}", + "{{ text|capitalize|lower|upper }}", + "{{ number|string|length }}", + "{{ number|string|upper }}", + "{{ items|first|upper }}", + "{{ items|last|lower }}", + "{{ items|reverse|first }}", + "{{ items|sort|last }}", + "{{ items|sort|reverse|first }}", + "{{ html|escape|upper }}", + "{{ float_num|round|string|length }}", + "{{ whitespace|trim|lower|capitalize }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForFiltersWithPositionalArgs() { + List templates = ImmutableList.of( + "{{ text|truncate(20) }}", + "{{ text|truncate(20, True) }}", + "{{ text|truncate(20, True, '...') }}", + "{{ text|truncate(10, False) }}", + "{{ items|join(', ') }}", + "{{ items|join(' - ') }}", + "{{ items|join('') }}", + "{{ text|replace('the', 'a') }}", + "{{ text|replace('o', '0') }}", + "{{ text|split(' ') }}", + "{{ text|split(' ', 3) }}", + "{{ number|default(0) }}", + "{{ null_value|default('fallback') }}", + "{{ null_value|default(42) }}", + "{{ float_num|round(2) }}", + "{{ float_num|round(0) }}", + "{{ text|center(50) }}", + "{{ text|center(50, '-') }}", + "{{ numbers|batch(3) }}", + "{{ numbers|slice(3) }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForFiltersWithNamedParams() { + List templates = ImmutableList.of( + "{{ text|truncate(length=20) }}", + "{{ text|truncate(length=20, killwords=True) }}", + "{{ text|truncate(length=20, end='!!!') }}", + "{{ text|truncate(length=15, killwords=False, end='...') }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForMixedPositionalAndNamedParams() { + List templates = ImmutableList.of( + "{{ text|truncate(20, killwords=True) }}", + "{{ text|truncate(20, end='!') }}", + "{{ items|join(', ')|truncate(length=15) }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForChainedFiltersWithArgs() { + List templates = ImmutableList.of( + "{{ text|truncate(20)|upper }}", + "{{ text|upper|truncate(20) }}", + "{{ text|replace('the', 'a')|upper }}", + "{{ text|upper|replace('THE', 'a') }}", + "{{ text|truncate(30)|replace('...', '!')|upper }}", + "{{ items|join(', ')|upper }}", + "{{ items|join(', ')|truncate(10) }}", + "{{ items|sort|join(' - ')|upper }}", + "{{ items|reverse|join(', ')|lower }}", + "{{ numbers|sort|join('-') }}", + "{{ numbers|reverse|join(', ')|length }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForFilterArgsWithExpressions() { + List templates = ImmutableList.of( + "{{ text|truncate(arg_value) }}", + "{{ text|truncate(arg_value + 5) }}", + "{{ text|truncate(arg_value * 2) }}", + "{{ items|join(name|trim) }}", + "{{ text|replace(items|first, items|last) }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForNullAndUndefinedHandling() { + List templates = ImmutableList.of( + "{{ null_value|default('fallback') }}", + "{{ null_value|default('fallback')|upper }}", + "{{ undefined_var|default('missing') }}", + "{{ undefined_var|default('missing')|lower }}", + "{{ null_value|string }}", + "{{ null_value|e }}", + "{{ nested.missing|default('not found') }}", + "{{ nested.missing|default('')|length }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForSafeStringHandling() { + context.put("safe_html", "Bold"); + + List templates = ImmutableList.of( + "{{ safe_html|safe }}", + "{{ safe_html|safe|upper }}", + "{{ safe_html|upper|safe }}", + "{{ safe_html|safe|length }}", + "{{ safe_html|safe|trim }}", + "{{ safe_html|safe|lower|capitalize }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForCollectionFilters() { + List templates = ImmutableList.of( + "{{ items|list }}", + "{{ items|unique }}", + "{{ numbers|sum }}", + "{{ numbers|sort }}", + "{{ numbers|sort|reverse }}", + "{{ objects|map(attribute='name') }}", + "{{ objects|map(attribute='name')|join(', ') }}", + "{{ objects|selectattr('age', '>', 28) }}", + "{{ objects|rejectattr('age', '<', 30) }}", + "{{ numbers|select('>', 3) }}", + "{{ numbers|reject('==', 1) }}", + "{{ items|batch(2)|list }}", + "{{ numbers|slice(3)|list }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForStringManipulationFilters() { + List templates = ImmutableList.of( + "{{ text|format }}", + "{{ text|striptags }}", + "{{ html|striptags }}", + "{{ text|urlize }}", + "{{ special_chars|escape }}", + "{{ special_chars|urlencode }}", + "{{ text|regex_replace('\\\\s+', '_') }}", + "{{ text|replace(' ', '_') }}", + "{{ name|trim|replace(' ', '-')|lower }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForNumericFilters() { + List templates = ImmutableList.of( + "{{ number|filesizeformat }}", + "{{ float_num|round }}", + "{{ float_num|round(2) }}", + "{{ float_num|round(2, 'floor') }}", + "{{ float_num|round(2, 'ceil') }}", + "{{ negative|abs }}", + "{{ number|float }}", + "{{ float_num|int }}", + "{{ number|divide(100) }}", + "{{ number|multiply(2) }}", + "{{ float_num|log }}", + "{{ number|root }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForDateTimeFilters() { + context.put("timestamp", 1609459200000L); + context.put("date_string", "2021-01-01"); + + List templates = ImmutableList.of( + "{{ timestamp|datetimeformat }}", + "{{ timestamp|unixtimestamp }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForJsonFilters() { + List templates = ImmutableList.of( + "{{ nested|tojson }}", + "{{ items|tojson }}", + "{{ json_string|fromjson }}", + "{{ json_string|fromjson|tojson }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForMultipleFilterChainsInTemplate() { + List templates = ImmutableList.of( + "{{ name|trim|lower }} and {{ text|upper|truncate(10) }}", + "Hello {{ name|trim }}, you have {{ items|length }} items", + "{{ items|first|upper }} - {{ items|last|lower }}", + "{{ number|string }} is {{ number|string|length }} digits", + "Name: {{ name|trim|lower|capitalize }}, Count: {{ items|length }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForNestedPropertyAccess() { + List templates = ImmutableList.of( + "{{ nested.key|upper }}", + "{{ nested.num|string }}", + "{{ nested.list|first }}", + "{{ nested.list|join('-') }}", + "{{ nested.key|upper|lower|capitalize }}", + "{{ objects[0].name|upper }}", + "{{ objects[0].name|upper|truncate(3) }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForFilterChainInConditions() { + List templates = ImmutableList.of( + "{% if name|trim|length > 5 %}long{% else %}short{% endif %}", + "{% if items|length > 2 %}many{% else %}few{% endif %}", + "{% if name|trim|lower == 'hello world' %}match{% else %}no match{% endif %}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForFilterChainInLoops() { + List templates = ImmutableList.of( + "{% for item in items|sort %}{{ item|upper }}{% endfor %}", + "{% for item in items|reverse %}{{ item|capitalize }}{% endfor %}", + "{% for n in numbers|sort|unique %}{{ n }}{% endfor %}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForLongFilterChains() { + List templates = ImmutableList.of( + "{{ text|upper|lower|capitalize|trim }}", + "{{ text|trim|lower|upper|lower|capitalize }}", + "{{ name|trim|lower|upper|lower|upper|lower }}", + "{{ text|replace('the', 'a')|upper|lower|capitalize|trim }}", + "{{ items|sort|reverse|join(', ')|upper|truncate(20) }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itTracksResolvedValuesConsistently() { + String template = "{{ name|trim|lower|upper }}"; + + RenderResult optimizedResult = jinjavaOptimized.renderForResult(template, context); + RenderResult unoptimizedResult = jinjavaUnoptimized.renderForResult( + template, + context + ); + + assertThat(optimizedResult.getOutput()) + .as("Output should match") + .isEqualTo(unoptimizedResult.getOutput()); + + Set optimizedResolved = optimizedResult.getContext().getResolvedValues(); + Set unoptimizedResolved = unoptimizedResult.getContext().getResolvedValues(); + + assertThat(optimizedResolved).as("Resolved filter:trim").contains("filter:trim"); + assertThat(optimizedResolved).as("Resolved filter:lower").contains("filter:lower"); + assertThat(optimizedResolved).as("Resolved filter:upper").contains("filter:upper"); + + assertThat(unoptimizedResolved) + .as("Unoptimized resolved filter:trim") + .contains("filter:trim"); + assertThat(unoptimizedResolved) + .as("Unoptimized resolved filter:lower") + .contains("filter:lower"); + assertThat(unoptimizedResolved) + .as("Unoptimized resolved filter:upper") + .contains("filter:upper"); + } + + @Test + public void itHandlesUnknownFiltersConsistently() { + String template = "{{ name|unknownfilter }}"; + + RenderResult optimizedResult = jinjavaOptimized.renderForResult(template, context); + RenderResult unoptimizedResult = jinjavaUnoptimized.renderForResult( + template, + context + ); + + assertThat(optimizedResult.getOutput()) + .as("Both paths should return empty for unknown filter") + .isEqualTo(unoptimizedResult.getOutput()); + } + + @Test + public void itProducesSameResultsForEmptyInputs() { + context.put("empty_string", ""); + + List templates = ImmutableList.of( + "{{ empty_string|upper }}", + "{{ empty_string|trim }}", + "{{ empty_string|default('fallback') }}", + "{{ empty_string|length }}", + "{{ empty_list|join(', ') }}", + "{{ empty_list|first }}", + "{{ empty_list|last }}", + "{{ empty_list|length }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForSpecialCharacters() { + List templates = ImmutableList.of( + "{{ special_chars|escape }}", + "{{ special_chars|escape|upper }}", + "{{ special_chars|urlencode }}", + "{{ special_chars|replace('&', 'and') }}", + "{{ unicode|upper }}", + "{{ unicode|lower }}", + "{{ unicode|length }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForBase64Filters() { + context.put("plain_text", "Hello, World!"); + context.put("base64_text", "SGVsbG8sIFdvcmxkIQ=="); + + List templates = ImmutableList.of( + "{{ plain_text|b64encode }}", + "{{ base64_text|b64decode }}", + "{{ plain_text|b64encode|b64decode }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForSelectAndRejectFilters() { + List templates = ImmutableList.of( + "{{ numbers|select('even')|list }}", + "{{ numbers|select('odd')|list }}", + "{{ numbers|reject('even')|list }}", + "{{ numbers|select('>', 3)|list }}", + "{{ numbers|select('>=', 4)|list }}", + "{{ numbers|reject('>', 5)|list }}" + ); + + assertParityForTemplates(templates); + } + + @Test + public void itProducesSameResultsForAttrFilters() { + List templates = ImmutableList.of( + "{{ objects|map(attribute='name')|list }}", + "{{ objects|map(attribute='age')|list }}", + "{{ objects|selectattr('age', '>', 28)|map(attribute='name')|list }}", + "{{ objects|rejectattr('age', '<', 30)|map(attribute='name')|list }}", + "{{ objects|groupby('age') }}" + ); + + assertParityForTemplates(templates); + } + + private void assertParityForTemplates(List templates) { + for (String template : templates) { + String optimizedResult = jinjavaOptimized.render(template, context); + String unoptimizedResult = jinjavaUnoptimized.render(template, context); + assertThat(optimizedResult) + .as("Template: %s", template) + .isEqualTo(unoptimizedResult); + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainPerformanceTest.java b/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainPerformanceTest.java new file mode 100644 index 000000000..7373b359a --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainPerformanceTest.java @@ -0,0 +1,173 @@ +package com.hubspot.jinjava.el.ext; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Performance tests for the filter chain optimization. + * + * Run manually with: mvn test -Dtest=AstFilterChainPerformanceTest + * Or run the main() method directly for detailed output. + */ +public class AstFilterChainPerformanceTest { + + private Jinjava jinjavaOptimized; + private Jinjava jinjavaUnoptimized; + private Map context; + + @Before + public void setup() { + jinjavaOptimized = + new Jinjava( + BaseJinjavaTest.newConfigBuilder().withEnableFilterChainOptimization(true).build() + ); + + jinjavaUnoptimized = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withEnableFilterChainOptimization(false) + .build() + ); + + context = new HashMap<>(); + context.put("name", " Hello World "); + context.put("text", "the quick brown fox jumps over the lazy dog"); + context.put("number", 12345); + context.put("items", new String[] { "apple", "banana", "cherry" }); + context.put("content", Map.of("text", "the quick brown fox jumps over the lazy dog")); + } + + public static void main(String[] args) { + AstFilterChainPerformanceTest test = new AstFilterChainPerformanceTest(); + test.setup(); + test.runPerformanceComparison(); + } + + /** + * Run this test manually to see detailed performance comparison. + * Use main() method or run with -Dtest=AstFilterChainPerformanceTest#runPerformanceComparison + */ + @Test + @Ignore("Manual performance test - run explicitly when needed") + public void runPerformanceComparison() { + int warmupIterations = 10000; + int testIterations = 100000; + + System.out.println("=== Filter Chain Performance Test ===\n"); + System.out.println("Warming up..."); + + runFilterTests(jinjavaOptimized, warmupIterations); + runFilterTests(jinjavaUnoptimized, warmupIterations); + + System.out.println( + "Running performance tests with " + testIterations + " iterations each\n" + ); + + comparePerformance("Single filter: {{ name|trim }}", testIterations); + comparePerformance("Two filters: {{ name|trim|lower }}", testIterations); + comparePerformance("Three filters: {{ name|trim|lower|capitalize }}", testIterations); + comparePerformance( + "Five filters: {{ text|upper|replace('THE', 'a')|trim|lower|title }}", + testIterations + ); + comparePerformance( + "Filters with args: {{ text|truncate(20)|upper }}", + testIterations + ); + comparePerformance( + "Multiple chains: {{ name|trim|lower }} and {{ text|upper|truncate(10) }}", + testIterations + ); + } + + @Test + public void optimizedVersionShouldBeFaster() { + int warmupIterations = 100; + int testIterations = 1000; + String template = "{{ content.text|upper|replace('THE', 'a')|trim|lower|title }}"; + + for (int i = 0; i < warmupIterations; i++) { + jinjavaOptimized.render(template, context); + jinjavaUnoptimized.render(template, context); + } + + long totalOptimizedTime = 0; + long totalUnoptimizedTime = 0; + int rounds = 3; + + for (int round = 0; round < rounds; round++) { + totalUnoptimizedTime += timeExecution(jinjavaUnoptimized, template, testIterations); + totalOptimizedTime += timeExecution(jinjavaOptimized, template, testIterations); + } + + long avgUnoptimizedTime = totalUnoptimizedTime / rounds; + long avgOptimizedTime = totalOptimizedTime / rounds; + + System.out.printf( + "Performance test: Optimized=%d ms, Unoptimized=%d ms, Speedup=%.2fx%n", + avgOptimizedTime, + avgUnoptimizedTime, + (1.0 * avgUnoptimizedTime) / avgOptimizedTime + ); + + assertThat(avgOptimizedTime) + .as( + "Optimized (%d ms) should be faster than unoptimized (%d ms)", + avgOptimizedTime, + avgUnoptimizedTime + ) + .isLessThan((avgUnoptimizedTime * 95) / 100); + } + + private void comparePerformance(String description, int iterations) { + String template = description.substring(description.indexOf("{{")); + if (description.contains(":")) { + template = description.substring(description.indexOf(":") + 2); + } + + System.out.println(description); + + long optimizedTime = timeExecution(jinjavaOptimized, template, iterations); + long unoptimizedTime = timeExecution(jinjavaUnoptimized, template, iterations); + + double speedup = (1.0 * unoptimizedTime) / optimizedTime; + System.out.printf( + " Optimized: %d ms, Unoptimized: %d ms, Speedup: %.2fx%n%n", + optimizedTime, + unoptimizedTime, + speedup + ); + } + + private long timeExecution(Jinjava jinjava, String template, int iterations) { + long startTime = System.currentTimeMillis(); + for (int i = 0; i < iterations; i++) { + jinjava.render(template, context); + } + return System.currentTimeMillis() - startTime; + } + + private void runFilterTests(Jinjava jinjava, int iterations) { + String[] templates = { + "{{ name|trim }}", + "{{ name|trim|lower }}", + "{{ name|trim|lower|capitalize }}", + "{{ text|upper|replace('THE', 'a')|trim|lower|title }}", + "{{ text|truncate(20)|upper }}", + }; + + for (String template : templates) { + for (int i = 0; i < iterations; i++) { + jinjava.render(template, context); + } + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainTest.java b/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainTest.java new file mode 100644 index 000000000..c71050414 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainTest.java @@ -0,0 +1,171 @@ +package com.hubspot.jinjava.el.ext; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.interpret.Context; +import com.hubspot.jinjava.interpret.RenderResult; +import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; +import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; +import com.hubspot.jinjava.objects.date.PyishDate; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; + +public class AstFilterChainTest { + + private Jinjava jinjava; + private Map context; + + @Before + public void setup() { + jinjava = + new Jinjava( + BaseJinjavaTest.newConfigBuilder().withEnableFilterChainOptimization(true).build() + ); + + context = new HashMap<>(); + context.put("name", " Hello World "); + context.put("text", "the quick brown fox jumps over the lazy dog"); + context.put("number", 12345); + context.put("items", new String[] { "apple", "banana", "cherry" }); + } + + @Test + public void itHandlesSingleFilter() { + String result = jinjava.render("{{ name|trim }}", context); + assertThat(result).isEqualTo("Hello World"); + } + + @Test + public void itHandlesChainedFilters() { + String result = jinjava.render("{{ name|trim|lower }}", context); + assertThat(result).isEqualTo("hello world"); + } + + @Test + public void itHandlesFiltersWithArguments() { + String result = jinjava.render("{{ text|truncate(20)|upper }}", context); + assertThat(result).isNotEmpty(); + assertThat(result).isUpperCase(); + } + + @Test + public void itHandlesComplexFilterChain() { + String result = jinjava.render( + "{{ text|upper|replace('THE', 'a')|trim|lower|capitalize }}", + context + ); + assertThat(result).isNotEmpty(); + } + + @Test + public void itHandlesFilterWithJoin() { + String result = jinjava.render("{{ items|join(', ')|upper }}", context); + assertThat(result).isEqualTo("APPLE, BANANA, CHERRY"); + } + + @Test + public void itHandlesFilterWithStringConversion() { + String result = jinjava.render("{{ number|string|length }}", context); + assertThat(result).isEqualTo("5"); + } + + @Test + public void itHandlesUnknownFilterInChain() { + context.put("module", new PyishDate(ZonedDateTime.parse("2024-01-15T10:30:00Z"))); + RenderResult renderResult = jinjava.renderForResult( + "{% set mid = module | local_dt|unixtimestamp | pprint | md5 %}{{ mid }}", + context + ); + assertThat(renderResult.getOutput()) + .as("Should produce MD5 output since chain continues past unknown filter") + .hasSize(32); + assertThat( + renderResult + .getErrors() + .stream() + .noneMatch(e -> e.getMessage().contains("Unknown filter")) + ) + .as("Should not report 'Unknown filter' error") + .isTrue(); + } + + @Test + public void itMatchesNonChainedBehaviorForUnknownFilter() { + String template = "{{ name | unknown_filter | lower | md5 }}"; + Jinjava jinjavaUnoptimized = new Jinjava( + BaseJinjavaTest.newConfigBuilder().withEnableFilterChainOptimization(false).build() + ); + RenderResult optimizedResult = jinjava.renderForResult(template, context); + RenderResult unoptimizedResult = jinjavaUnoptimized.renderForResult( + template, + context + ); + assertThat(optimizedResult.getOutput()) + .as("Optimized should match un-optimized for unknown filter in chain") + .isEqualTo(unoptimizedResult.getOutput()); + } + + @Test + public void itSkipsDisabledFilterAndContinuesChain() { + Map> disabled = ImmutableMap.of( + Context.Library.FILTER, + ImmutableSet.of("lower") + ); + Jinjava jinjavaWithDisabled = new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withEnableFilterChainOptimization(true) + .withDisabled(disabled) + .build() + ); + + RenderResult result = jinjavaWithDisabled.renderForResult( + "{{ name|trim|lower|capitalize }}", + context + ); + + assertThat(result.getErrors()).isNotEmpty(); + assertThat(result.getErrors().get(0).getItem()).isEqualTo(ErrorItem.FILTER); + assertThat(result.getErrors().get(0).getReason()).isEqualTo(ErrorReason.DISABLED); + assertThat(result.getErrors().get(0).getMessage()).contains("lower"); + } + + @Test + public void itMatchesNonChainedBehaviorForDisabledFilter() { + Map> disabled = ImmutableMap.of( + Context.Library.FILTER, + ImmutableSet.of("lower") + ); + String template = "{{ name|trim|lower|capitalize }}"; + + Jinjava optimized = new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withEnableFilterChainOptimization(true) + .withDisabled(disabled) + .build() + ); + Jinjava unoptimized = new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withEnableFilterChainOptimization(false) + .withDisabled(disabled) + .build() + ); + + RenderResult optimizedResult = optimized.renderForResult(template, context); + RenderResult unoptimizedResult = unoptimized.renderForResult(template, context); + + assertThat(optimizedResult.getOutput()) + .as("Optimized should match un-optimized for disabled filter in chain") + .isEqualTo(unoptimizedResult.getOutput()); + } +} diff --git a/src/test/java/com/hubspot/jinjava/el/ext/CollectionMembershipOperatorTest.java b/src/test/java/com/hubspot/jinjava/el/ext/CollectionMembershipOperatorTest.java index 4804c671e..5d9b1094f 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/CollectionMembershipOperatorTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/CollectionMembershipOperatorTest.java @@ -4,10 +4,34 @@ import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import java.util.AbstractMap; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; import org.junit.Before; import org.junit.Test; public class CollectionMembershipOperatorTest { + + static class NoKeySetMap extends AbstractMap { + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public Set keySet() { + return Collections.emptySet(); + } + + @Override + public Set> entrySet() { + return Collections.emptySet(); + } + } + private JinjavaInterpreter interpreter; @Before @@ -35,4 +59,23 @@ public void itChecksIfDictionaryContainsKey() { assertThat(interpreter.resolveELExpression("'c' in {'a': 1, 'b': 2}", -1)) .isEqualTo(false); } + + @Test + public void itChecksIfDictionaryContainsNullKey() { + Map map = new HashMap(); + map.put(null, "null"); + map.put("a", 1); + interpreter.getContext().put("map", map); + assertThat(interpreter.resolveELExpression("'a' in map", -1)).isEqualTo(true); + assertThat(interpreter.resolveELExpression("null in map", -1)).isEqualTo(true); + assertThat(interpreter.resolveELExpression("'b' in map", -1)).isEqualTo(false); + } + + @Test + public void itCheckEmptyKeySet() { + // The map is "not" empty, but keySet() is empty. + Map map = new NoKeySetMap<>(); + interpreter.getContext().put("map", map); + assertThat(interpreter.resolveELExpression("'a' in map", -1)).isEqualTo(false); + } } diff --git a/src/test/java/com/hubspot/jinjava/el/ext/CollectionNonMembershipOperatorTest.java b/src/test/java/com/hubspot/jinjava/el/ext/CollectionNonMembershipOperatorTest.java index cc5520bf9..92a68aa5a 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/CollectionNonMembershipOperatorTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/CollectionNonMembershipOperatorTest.java @@ -8,6 +8,7 @@ import org.junit.Test; public class CollectionNonMembershipOperatorTest { + private JinjavaInterpreter interpreter; @Before 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..39cdc6124 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/ExtendedParserTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/ExtendedParserTest.java @@ -4,7 +4,9 @@ import static org.assertj.core.api.Assertions.fail; 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 +129,41 @@ 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, null)}"); + + 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, null)}"); + 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, null)}"); + assertThat(trueExpTest).isInstanceOf(AstMethod.class); + assertThat(trueExpTest.getChild(0)).isInstanceOf(AstDot.class); + assertThat(trueExpTest.getChild(1)).isInstanceOf(AstParameters.class); + } + + @Test + public void itResolvesAlternateExpTestSyntaxForInExpTest() { + AstNode inExpTest = buildExpressionNodes("#{exptest:in.evaluate(2, null, [])}"); + assertThat(inExpTest).isInstanceOf(AstMethod.class); + assertThat(inExpTest.getChild(0)).isInstanceOf(AstDot.class); + assertThat(inExpTest.getChild(1)).isInstanceOf(AstParameters.class); + } + private void assertForExpression( AstNode astNode, String leftExpected, @@ -164,7 +201,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(); 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..0456b1491 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolverTest.java @@ -0,0 +1,151 @@ +package com.hubspot.jinjava.el.ext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.el.JinjavaELContext; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.testobjects.JinjavaBeanELResolverTestObjects; +import javax.el.ELContext; +import javax.el.PropertyNotFoundException; +import org.junit.Before; +import org.junit.Test; + +public class JinjavaBeanELResolverTest { + + private JinjavaBeanELResolver jinjavaBeanELResolver; + private ELContext elContext; + private Jinjava jinjava; + + @Before + public void setUp() throws Exception { + jinjavaBeanELResolver = new JinjavaBeanELResolver(); + elContext = new JinjavaELContext(); + jinjava = new Jinjava(BaseJinjavaTest.newConfigBuilder().build()); + } + + @Test + public void itInvokesProperStringReplace() { + try ( + var a = JinjavaInterpreter.closeablePushCurrent(jinjava.newInterpreter()).get() + ) { + 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() { + try ( + var a = JinjavaInterpreter.closeablePushCurrent(jinjava.newInterpreter()).get() + ) { + JinjavaBeanELResolverTestObjects.TempItInvokesBestMethodWithSingleParam var = + new JinjavaBeanELResolverTestObjects.TempItInvokesBestMethodWithSingleParam(); + 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() { + try ( + var a = JinjavaInterpreter.closeablePushCurrent(jinjava.newInterpreter()).get() + ) { + JinjavaBeanELResolverTestObjects.TempItPrefersPrimitives var = + new JinjavaBeanELResolverTestObjects.TempItPrefersPrimitives(); + 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 + } + } + + @Test + public void itDoesNotAllowAccessingPropertiesOfInterpreter() { + try ( + AutoCloseableSupplier.AutoCloseableImpl a = JinjavaInterpreter + .closeablePushCurrent(jinjava.newInterpreter()) + .get() + ) { + assertThatThrownBy(() -> + jinjavaBeanELResolver.getValue(elContext, a.value(), "config") + ) + .isInstanceOf(PropertyNotFoundException.class); + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/el/ext/PowerOfTest.java b/src/test/java/com/hubspot/jinjava/el/ext/PowerOfTest.java index 3eb5314d2..2163eb12d 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/PowerOfTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/PowerOfTest.java @@ -36,7 +36,7 @@ public void testPowerOfInteger() { { "{% set x = base ** negativeExponent %}{{x}}", "0" }, { "{% set x = 2 ** -8 %}{{x}}", "0" }, { "{% set x = negativeBase ** oddExponent %}{{x}}", "-128" }, - { "{% set x = -2 ** 7 %}{{x}}", "-128" } + { "{% set x = -2 ** 7 %}{{x}}", "-128" }, }; for (String[] testCase : testCases) { @@ -66,7 +66,7 @@ public void testPowerOfFractional() { { "{% set x = base ** negativeExponent %}{{x}}", "0.00390625" }, { "{% set x = 2 ** -8.0 %}{{x}}", "0.00390625" }, { "{% set x = negativeBase ** oddExponent %}{{x}}", "-128.0" }, - { "{% set x = -2 ** 7.0 %}{{x}}", "-128.0" } + { "{% set x = -2 ** 7.0 %}{{x}}", "-128.0" }, }; for (String[] testCase : testCases) { diff --git a/src/test/java/com/hubspot/jinjava/el/ext/RangeStringTest.java b/src/test/java/com/hubspot/jinjava/el/ext/RangeStringTest.java index d99487167..b5f98d4a6 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/RangeStringTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/RangeStringTest.java @@ -25,13 +25,14 @@ * Created by anev on 11/05/16. */ public class RangeStringTest { + private Jinjava jinjava; private Map context; @Before public void setup() { jinjava = new Jinjava(); - context = ImmutableMap.of("theString", "theSimpleString"); + context = ImmutableMap.of("theString", "theSimpleString", "emptyString", ""); } @Test @@ -55,6 +56,11 @@ public void testStringRangeNegative() { assertThat(jinjava.render("{{ theString[-7:-4] }}", context)).isEqualTo("eSt"); } + @Test + public void testStringEmpty() { + assertThat(jinjava.render("{{ emptyString[-1:0] }}", context)).isEmpty(); + } + @Test public void testStringRangeRightOnly() { assertThat(jinjava.render("{{ theString[3:] }}", context)).isEqualTo("SimpleString"); diff --git a/src/test/java/com/hubspot/jinjava/el/ext/TruncDivTest.java b/src/test/java/com/hubspot/jinjava/el/ext/TruncDivTest.java index 5fe4553fd..f9fbaae96 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/TruncDivTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/TruncDivTest.java @@ -11,6 +11,7 @@ import org.junit.Test; public class TruncDivTest { + private Jinjava jinja; @Before @@ -39,7 +40,7 @@ public void testTruncDivInteger() { { "{% set x = dividend // negativeDivisor %}{{x}}", "-3" }, { "{% set x = 5 // -2 %}{{x}}", "-3" }, { "{% set x = negativeDividend // negativeDivisor %}{{x}}", "2" }, - { "{% set x = -5 // -2 %}{{x}}", "2" } + { "{% set x = -5 // -2 %}{{x}}", "2" }, }; for (String[] testCase : testCases) { @@ -71,7 +72,7 @@ public void testTruncDivFractional() { { "{% set x = dividend // negativeDivisor %}{{x}}", "-3.0" }, { "{% set x = 5.0 // -2 %}{{x}}", "-3.0" }, { "{% set x = negativeDividend // negativeDivisor %}{{x}}", "2.0" }, - { "{% set x = -5.0 // -2 %}{{x}}", "2.0" } + { "{% set x = -5.0 // -2 %}{{x}}", "2.0" }, }; for (String[] testCase : testCases) { diff --git a/src/test/java/com/hubspot/jinjava/el/ext/ValidatorConfigBannedConstructsTest.java b/src/test/java/com/hubspot/jinjava/el/ext/ValidatorConfigBannedConstructsTest.java new file mode 100644 index 000000000..a22fbe017 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/el/ext/ValidatorConfigBannedConstructsTest.java @@ -0,0 +1,227 @@ +package com.hubspot.jinjava.el.ext; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.exptest.ExpTest; +import com.hubspot.jinjava.lib.filter.Filter; +import java.lang.reflect.Method; +import org.junit.Test; + +public class ValidatorConfigBannedConstructsTest { + + // MethodValidatorConfig: allowedMethods() path + + @Test + public void itRejectsObjectMethodInAllowedMethods() throws NoSuchMethodException { + Method toStringMethod = Object.class.getMethod("toString"); + assertThatThrownBy(() -> + MethodValidatorConfig.builder().addAllowedMethods(toStringMethod).build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + @Test + public void itRejectsClassMethodInAllowedMethods() throws NoSuchMethodException { + Method getNameMethod = Class.class.getMethod("getName"); + assertThatThrownBy(() -> + MethodValidatorConfig.builder().addAllowedMethods(getNameMethod).build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + // MethodValidatorConfig: allowedDeclaredMethodsFromCanonicalClassNames() path + + @Test + public void itRejectsObjectClassInAllowedDeclaredMethodClassNames() { + assertThatThrownBy(() -> + MethodValidatorConfig + .builder() + .addAllowedDeclaredMethodsFromCanonicalClassNames( + Object.class.getCanonicalName() + ) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + @Test + public void itRejectsClassClassInAllowedDeclaredMethodClassNames() { + assertThatThrownBy(() -> + MethodValidatorConfig + .builder() + .addAllowedDeclaredMethodsFromCanonicalClassNames( + Class.class.getCanonicalName() + ) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + @Test + public void itRejectsObjectMapperInAllowedDeclaredMethodClassNames() { + assertThatThrownBy(() -> + MethodValidatorConfig + .builder() + .addAllowedDeclaredMethodsFromCanonicalClassNames( + ObjectMapper.class.getCanonicalName() + ) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + @Test + public void itRejectsJinjavaInterpreterInAllowedDeclaredMethodClassNames() { + assertThatThrownBy(() -> + MethodValidatorConfig + .builder() + .addAllowedDeclaredMethodsFromCanonicalClassNames( + JinjavaInterpreter.class.getCanonicalName() + ) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + // MethodValidatorConfig: allowedDeclaredMethodsFromCanonicalClassPrefixes() path + + @Test + public void itRejectsReflectPackageInAllowedDeclaredMethodPrefixes() { + assertThatThrownBy(() -> + MethodValidatorConfig + .builder() + .addAllowedDeclaredMethodsFromCanonicalClassPrefixes( + Method.class.getPackageName() + ) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + @Test + public void itRejectsJacksonDatabindPackageInAllowedDeclaredMethodPrefixes() { + assertThatThrownBy(() -> + MethodValidatorConfig + .builder() + .addAllowedDeclaredMethodsFromCanonicalClassPrefixes( + ObjectMapper.class.getPackageName() + ) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + @Test + public void itRejectsEvilJinjavaFilterPathInAllowedDeclaredMethodPrefixes() { + assertThatThrownBy(() -> + MethodValidatorConfig + .builder() + .addAllowedDeclaredMethodsFromCanonicalClassPrefixes( + Filter.class.getPackageName() + "_evil" + ) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + @Test + public void itRejectsEvilJinjavaExptestPathInAllowedDeclaredMethodPrefixes() { + assertThatThrownBy(() -> + MethodValidatorConfig + .builder() + .addAllowedDeclaredMethodsFromCanonicalClassPrefixes( + ExpTest.class.getPackageName() + "_evil" + ) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + // ReturnTypeValidatorConfig: allowedCanonicalClassNames() path + + @Test + public void itRejectsObjectClassInAllowedReturnTypeClassNames() { + assertThatThrownBy(() -> + ReturnTypeValidatorConfig + .builder() + .addAllowedCanonicalClassNames(Object.class.getCanonicalName()) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + @Test + public void itRejectsClassClassInAllowedReturnTypeClassNames() { + assertThatThrownBy(() -> + ReturnTypeValidatorConfig + .builder() + .addAllowedCanonicalClassNames(Class.class.getCanonicalName()) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + @Test + public void itRejectsObjectMapperInAllowedReturnTypeClassNames() { + assertThatThrownBy(() -> + ReturnTypeValidatorConfig + .builder() + .addAllowedCanonicalClassNames(ObjectMapper.class.getCanonicalName()) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + @Test + public void itRejectsJinjavaInterpreterInAllowedReturnTypeClassNames() { + assertThatThrownBy(() -> + ReturnTypeValidatorConfig + .builder() + .addAllowedCanonicalClassNames(JinjavaInterpreter.class.getCanonicalName()) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + // ReturnTypeValidatorConfig: allowedCanonicalClassPrefixes() path + + @Test + public void itRejectsReflectPackageInAllowedReturnTypePrefixes() { + assertThatThrownBy(() -> + ReturnTypeValidatorConfig + .builder() + .addAllowedCanonicalClassPrefixes(Method.class.getPackageName()) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } + + @Test + public void itRejectsJacksonDatabindPackageInAllowedReturnTypePrefixes() { + assertThatThrownBy(() -> + ReturnTypeValidatorConfig + .builder() + .addAllowedCanonicalClassPrefixes(ObjectMapper.class.getPackageName()) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Banned classes or prefixes"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstBinaryTest.java b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstBinaryTest.java index cc5350667..f11f372cd 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstBinaryTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstBinaryTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.fail; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.el.ext.DeferredParsingException; @@ -22,8 +23,8 @@ public class EagerAstBinaryTest extends BaseInterpretingTest { @Before public void setup() { - JinjavaConfig config = JinjavaConfig - .newBuilder() + JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withRandomNumberGeneratorStrategy(RandomNumberGeneratorStrategy.DEFERRED) .withExecutionMode(EagerExecutionMode.instance()) .withNestedInterpretationEnabled(true) diff --git a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstChoiceTest.java b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstChoiceTest.java index e87edf167..f594a46e9 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstChoiceTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstChoiceTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.fail; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.el.ext.DeferredParsingException; @@ -23,8 +24,8 @@ public class EagerAstChoiceTest extends BaseInterpretingTest { @Before public void setup() { - JinjavaConfig config = JinjavaConfig - .newBuilder() + JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withRandomNumberGeneratorStrategy(RandomNumberGeneratorStrategy.DEFERRED) .withExecutionMode(EagerExecutionMode.instance()) .withNestedInterpretationEnabled(true) diff --git a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstDotTest.java b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstDotTest.java index fb208f3a1..6b13a7bc8 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstDotTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstDotTest.java @@ -2,16 +2,17 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValue; -import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.interpret.PartiallyDeferredValue; import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy; +import com.hubspot.jinjava.testobjects.EagerAstDotTestObjects; import org.junit.Before; import org.junit.Test; @@ -19,14 +20,16 @@ public class EagerAstDotTest extends BaseInterpretingTest { @Before public void setup() { - JinjavaConfig config = JinjavaConfig - .newBuilder() + JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withRandomNumberGeneratorStrategy(RandomNumberGeneratorStrategy.DEFERRED) .withExecutionMode(EagerExecutionMode.instance()) .withNestedInterpretationEnabled(true) .withLegacyOverrides( LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() ) + .withMethodValidator(BaseJinjavaTest.METHOD_VALIDATOR) + .withReturnTypeValidator(BaseJinjavaTest.RETURN_TYPE_VALIDATOR) .withMaxMacroRecursionDepth(5) .withEnableRecursiveMacroCalls(true) .build(); @@ -42,29 +45,34 @@ public void setup() { @Test public void itDefersWhenDotThrowsDeferredValueException() { - interpreter.getContext().put("foo", new Foo()); - assertThat(interpreter.render("{{ foo.deferred }}")).isEqualTo("{{ foo.deferred }}"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter.getContext().put("foo", new EagerAstDotTestObjects.Foo()); + assertThat(interpreter.render("{{ foo.deferred }}")) + .isEqualTo("{{ foo.deferred }}"); + } } @Test public void itResolvedDeferredMapWithDot() { - interpreter.getContext().put("foo", new Foo()); + interpreter.getContext().put("foo", new EagerAstDotTestObjects.Foo()); assertThat(interpreter.render("{{ foo.resolved }}")).isEqualTo("resolved"); } - public static class Foo implements PartiallyDeferredValue { - - public String getDeferred() { - throw new DeferredValueException("foo.deferred is deferred"); - } - - public String getResolved() { - return "resolved"; - } + @Test + public void itResolvedNestedDeferredMapWithDot() { + interpreter + .getContext() + .put("foo_map", ImmutableMap.of("bar", new EagerAstDotTestObjects.Foo())); + assertThat(interpreter.render("{{ foo_map.bar.resolved }}")).isEqualTo("resolved"); + } - @Override - public Object getOriginalValue() { - return null; - } + @Test + public void itDefersNodeWhenNestedDeferredMapDotThrowsDeferredValueException() { + interpreter + .getContext() + .put("foo_map", ImmutableMap.of("bar", new EagerAstDotTestObjects.Foo())); + assertThat(interpreter.render("{{ foo_map.bar.deferred }}")) + .isEqualTo("{{ foo_map.bar.deferred }}"); + assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); } } diff --git a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifierTest.java b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifierTest.java index e618e3d66..2aa6fd09f 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifierTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifierTest.java @@ -12,6 +12,7 @@ import org.junit.Test; public class EagerAstIdentifierTest extends BaseInterpretingTest { + private JinjavaELContext elContext; @Before @@ -29,7 +30,7 @@ public void itSavesNullEvalResult() { new ValueExpression[] { jinjava .getEagerExpressionFactory() - .createValueExpression(elContext, "#{foo}", Object.class) + .createValueExpression(elContext, "#{foo}", Object.class), } ), elContext diff --git a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethodTest.java b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethodTest.java index 98c31584f..d794f6c8e 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethodTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethodTest.java @@ -4,10 +4,10 @@ import static org.junit.Assert.fail; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.el.ext.DeferredParsingException; -import com.hubspot.jinjava.el.ext.eager.EagerAstDotTest.Foo; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; @@ -15,7 +15,9 @@ import com.hubspot.jinjava.objects.collections.PyList; import com.hubspot.jinjava.objects.collections.PyMap; import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy; +import com.hubspot.jinjava.testobjects.EagerAstDotTestObjects; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -26,14 +28,16 @@ public class EagerAstMethodTest extends BaseInterpretingTest { @Before public void setup() { - JinjavaConfig config = JinjavaConfig - .newBuilder() + JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withRandomNumberGeneratorStrategy(RandomNumberGeneratorStrategy.DEFERRED) .withExecutionMode(EagerExecutionMode.instance()) .withNestedInterpretationEnabled(true) .withLegacyOverrides( LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() ) + .withMethodValidator(BaseJinjavaTest.METHOD_VALIDATOR) + .withReturnTypeValidator(BaseJinjavaTest.RETURN_TYPE_VALIDATOR) .withMaxMacroRecursionDepth(5) .withEnableRecursiveMacroCalls(true) .build(); @@ -69,6 +73,27 @@ public void itPreservesIdentifier() { } } + @Test + public void itPreservesNonDeferredIdentifier() { + try { + interpreter.resolveELExpression("deferred.modify(foo_map)", -1); + fail("Should throw DeferredParsingException"); + } catch (DeferredParsingException e) { + assertThat(e.getDeferredEvalResult()).isEqualTo("deferred.modify(foo_map)"); + } + } + + @Test + public void itPreservesNonDeferredIdentifierWhenSecondParamIsDeferred() { + try { + interpreter.resolveELExpression("foo_list.modify(foo_map, deferred)", -1); + fail("Should throw DeferredParsingException"); + } catch (DeferredParsingException e) { + assertThat(e.getDeferredEvalResult()) + .isEqualTo("foo_list.modify(foo_map, deferred)"); + } + } + @Test public void itPreservesAstDot() { try { @@ -203,7 +228,8 @@ public void itPreservesAstTuple() { @Test public void itPreservesUnresolvable() { - interpreter.getContext().put("foo_object", new Foo()); + interpreter.getContext().put("foo_object", new EagerAstDotTestObjects.Foo()); + interpreter.getContext().addMetaContextVariables(Collections.singleton("foo_object")); try { interpreter.resolveELExpression("foo_object.deferred|upper", -1); fail("Should throw DeferredParsingException"); diff --git a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstRangeBracketTest.java b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstRangeBracketTest.java index e501965d1..7ff83ab3b 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstRangeBracketTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstRangeBracketTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.Context; @@ -17,8 +18,8 @@ public class EagerAstRangeBracketTest extends BaseInterpretingTest { @Before public void setup() { - JinjavaConfig config = JinjavaConfig - .newBuilder() + JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withRandomNumberGeneratorStrategy(RandomNumberGeneratorStrategy.DEFERRED) .withExecutionMode(EagerExecutionMode.instance()) .withNestedInterpretationEnabled(true) diff --git a/src/test/java/com/hubspot/jinjava/interpret/ContextTest.java b/src/test/java/com/hubspot/jinjava/interpret/ContextTest.java index e3565fcb2..71a310a63 100644 --- a/src/test/java/com/hubspot/jinjava/interpret/ContextTest.java +++ b/src/test/java/com/hubspot/jinjava/interpret/ContextTest.java @@ -6,10 +6,14 @@ import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.lib.fn.ELFunctionDefinition; +import com.hubspot.jinjava.loader.RelativePathResolver; +import com.hubspot.jinjava.mode.EagerExecutionMode; +import java.util.Collections; import org.junit.Before; import org.junit.Test; public class ContextTest { + private static final String RESOLVED_EXPRESSION = "exp"; private static final String RESOLVED_FUNCTION = "func"; private static final String RESOLVED_VALUE = "val"; @@ -92,6 +96,30 @@ public void itThrowsFromChildContext() throws Exception { } } + @Test + public void itRemovesOtherMetaContextVariables() { + String variableName = "foo"; + EagerExecutionMode.instance().prepareContext(context); + context.addMetaContextVariables(Collections.singleton(variableName)); + assertThat(context.getComputedMetaContextVariables()).contains(variableName); + context.addNonMetaContextVariables(Collections.singleton(variableName)); + assertThat(context.getComputedMetaContextVariables()).doesNotContain(variableName); + context.removeNonMetaContextVariables(Collections.singleton(variableName)); + assertThat(context.getComputedMetaContextVariables()).contains(variableName); + } + + @Test + public void itDoesNotRemoveStaticMetaContextVariables() { + EagerExecutionMode.instance().prepareContext(context); + assertThat(context.getComputedMetaContextVariables()) + .contains(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY); + context.addNonMetaContextVariables( + Collections.singleton(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY) + ); + assertThat(context.getComputedMetaContextVariables()) + .contains(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY); + } + public static void throwException() { throw new RuntimeException(); } diff --git a/src/test/java/com/hubspot/jinjava/interpret/DeferredTest.java b/src/test/java/com/hubspot/jinjava/interpret/DeferredTest.java index c254e89bb..ceb5c0c1e 100644 --- a/src/test/java/com/hubspot/jinjava/interpret/DeferredTest.java +++ b/src/test/java/com/hubspot/jinjava/interpret/DeferredTest.java @@ -6,6 +6,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy; @@ -17,6 +18,7 @@ import org.junit.Test; public class DeferredTest { + private JinjavaInterpreter interpreter; private Jinjava jinjava = new Jinjava(); Context globalContext = new Context(); @@ -24,8 +26,8 @@ public class DeferredTest { @Before public void setup() { - JinjavaConfig config = JinjavaConfig - .newBuilder() + JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withRandomNumberGeneratorStrategy(RandomNumberGeneratorStrategy.DEFERRED) .build(); JinjavaInterpreter parentInterpreter = new JinjavaInterpreter( @@ -251,9 +253,8 @@ public void itDefersAllVariablesUsedInDeferredNode() { DeferredValue varInScopeDeferred = (DeferredValue) varInScope; assertThat(varInScopeDeferred.getOriginalValue()).isEqualTo("outside if statement"); - HashMap deferredContext = DeferredValueUtils.getDeferredContextWithOriginalValues( - localContext - ); + HashMap deferredContext = + DeferredValueUtils.getDeferredContextWithOriginalValues(localContext); deferredContext.forEach(localContext::put); String secondRender = interpreter.render(output); assertThat(secondRender).isEqualTo("outside if statement entered if statement"); @@ -266,7 +267,7 @@ public void itDefersAllVariablesUsedInDeferredNode() { public void itDefersDependantVariables() { String template = ""; template += - "{% set resolved_variable = 'resolved' %} {% set deferred_variable = deferred + '-' + resolved_variable %}"; + "{% set resolved_variable = 'resolved' %} {% set deferred_variable = deferred + '-' + resolved_variable %}"; template += "{{ deferred_variable }}"; interpreter.render(template); localContext.get("resolved_variable"); @@ -317,9 +318,8 @@ public void puttingDeferredVariablesOnParentScopesDoesNotBreakSetTag() { DeferredValue varSetInsideDeferred = (DeferredValue) varSetInside; assertThat(varSetInsideDeferred.getOriginalValue()).isEqualTo("inside first scope"); - HashMap deferredContext = DeferredValueUtils.getDeferredContextWithOriginalValues( - localContext - ); + HashMap deferredContext = + DeferredValueUtils.getDeferredContextWithOriginalValues(localContext); deferredContext.forEach(localContext::put); String secondRender = interpreter.render(output); assertThat(secondRender.trim()) @@ -357,7 +357,11 @@ public void itMarksVariablesUsedAsMapKeysAsDeferred() { String output = interpreter.render(template); assertThat(localContext).containsKey("deferredValue2"); Object deferredValue2 = localContext.get("deferredValue2"); - DeferredValueUtils.findAndMarkDeferredProperties(localContext); + localContext + .getDeferredNodes() + .forEach(node -> + DeferredValueUtils.findAndMarkDeferredProperties(localContext, node) + ); assertThat(deferredValue2).isInstanceOf(DeferredValue.class); assertThat(output) .contains("{% set varSetInside = imported.map[deferredValue2.nonexistentprop] %}"); diff --git a/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java b/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java index 7b606d6f1..b6f1524fe 100644 --- a/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java +++ b/src/test/java/com/hubspot/jinjava/interpret/JinjavaInterpreterTest.java @@ -3,34 +3,51 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.LegacyOverrides; +import com.hubspot.jinjava.features.FeatureConfig; +import com.hubspot.jinjava.features.FeatureStrategies; import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; import com.hubspot.jinjava.interpret.TemplateError.ErrorType; +import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.mode.PreserveRawExecutionMode; +import com.hubspot.jinjava.objects.date.FormattedDate; +import com.hubspot.jinjava.objects.date.StrftimeFormatter; +import com.hubspot.jinjava.testobjects.JinjavaInterpreterTestObjects; import com.hubspot.jinjava.tree.TextNode; import com.hubspot.jinjava.tree.output.BlockInfo; +import com.hubspot.jinjava.tree.output.OutputList; 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; public class JinjavaInterpreterTest { + private Jinjava jinjava; private JinjavaInterpreter interpreter; private TokenScannerSymbols symbols; @Before public void setup() { - jinjava = new Jinjava(); + jinjava = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withTimeZone(ZoneId.of("America/New_York")) + .build() + ); interpreter = jinjava.newInterpreter(); symbols = interpreter.getConfig().getTokenScannerSymbols(); } @@ -90,106 +107,120 @@ public void resolveBlockStubsWithCycle() { // Ex VariableChain stuff - static class Foo { - private String bar; - - public Foo(String bar) { - this.bar = bar; - } - - public String getBar() { - return bar; - } - - public String getBarFoo() { - return bar; - } - - public String getBarFoo1() { - return bar; - } - - @JsonIgnore - public String getBarHidden() { - return bar; - } - } - @Test public void singleWordProperty() { - assertThat(interpreter.resolveProperty(new Foo("a"), "bar")).isEqualTo("a"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + assertThat( + interpreter.resolveProperty(new JinjavaInterpreterTestObjects.Foo("a"), "bar") + ) + .isEqualTo("a"); + } } @Test public void multiWordCamelCase() { - assertThat(interpreter.resolveProperty(new Foo("a"), "barFoo")).isEqualTo("a"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + assertThat( + interpreter.resolveProperty(new JinjavaInterpreterTestObjects.Foo("a"), "barFoo") + ) + .isEqualTo("a"); + } } @Test public void multiWordSnakeCase() { - assertThat(interpreter.resolveProperty(new Foo("a"), "bar_foo")).isEqualTo("a"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + assertThat( + interpreter.resolveProperty(new JinjavaInterpreterTestObjects.Foo("a"), "bar_foo") + ) + .isEqualTo("a"); + } } @Test public void multiWordNumberSnakeCase() { - assertThat(interpreter.resolveProperty(new Foo("a"), "bar_foo_1")).isEqualTo("a"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + assertThat( + interpreter.resolveProperty( + new JinjavaInterpreterTestObjects.Foo("a"), + "bar_foo_1" + ) + ) + .isEqualTo("a"); + } } @Test public void jsonIgnore() { - assertThat(interpreter.resolveProperty(new Foo("a"), "barHidden")).isEqualTo("a"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + assertThat( + interpreter.resolveProperty( + new JinjavaInterpreterTestObjects.Foo("a"), + "barHidden" + ) + ) + .isEqualTo("a"); + } } @Test public void triesBeanMethodFirst() { - assertThat( + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + assertThat( interpreter .resolveProperty(ZonedDateTime.parse("2013-09-19T12:12:12+00:00"), "year") .toString() ) - .isEqualTo("2013"); + .isEqualTo("2013"); + } } @Test public void enterScopeTryFinally() { - interpreter.getContext().put("foo", "parent"); - - interpreter.enterScope(); - try { - interpreter.getContext().put("foo", "child"); - assertThat(interpreter.resolveELExpression("foo", 1)).isEqualTo("child"); - } finally { - interpreter.leaveScope(); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter.getContext().put("foo", "parent"); + + interpreter.enterScope(); + try { + interpreter.getContext().put("foo", "child"); + assertThat(interpreter.resolveELExpression("foo", 1)).isEqualTo("child"); + } finally { + interpreter.leaveScope(); + } + + assertThat(interpreter.resolveELExpression("foo", 1)).isEqualTo("parent"); } - - assertThat(interpreter.resolveELExpression("foo", 1)).isEqualTo("parent"); } @Test public void enterScopeTryWithResources() { - interpreter.getContext().put("foo", "parent"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter.getContext().put("foo", "parent"); - try (InterpreterScopeClosable c = interpreter.enterScope()) { - interpreter.getContext().put("foo", "child"); - assertThat(interpreter.resolveELExpression("foo", 1)).isEqualTo("child"); - } + try (InterpreterScopeClosable c = interpreter.enterScope()) { + interpreter.getContext().put("foo", "child"); + assertThat(interpreter.resolveELExpression("foo", 1)).isEqualTo("child"); + } - assertThat(interpreter.resolveELExpression("foo", 1)).isEqualTo("parent"); + assertThat(interpreter.resolveELExpression("foo", 1)).isEqualTo("parent"); + } } @Test public void bubbleUpDependenciesFromLowerScope() { - String dependencyType = "foo"; - String dependencyIdentifier = "123"; - - interpreter.enterScope(); - interpreter.getContext().addDependency(dependencyType, dependencyIdentifier); - assertThat(interpreter.getContext().getDependencies().get(dependencyType)) - .contains(dependencyIdentifier); - interpreter.leaveScope(); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + String dependencyType = "foo"; + String dependencyIdentifier = "123"; + + interpreter.enterScope(); + interpreter.getContext().addDependency(dependencyType, dependencyIdentifier); + assertThat(interpreter.getContext().getDependencies().get(dependencyType)) + .contains(dependencyIdentifier); + interpreter.leaveScope(); - assertThat(interpreter.getContext().getDependencies().get(dependencyType)) - .contains(dependencyIdentifier); + assertThat(interpreter.getContext().getDependencies().get(dependencyType)) + .contains(dependencyIdentifier); + } } @Test @@ -201,8 +232,8 @@ public void parseWithSyntaxError() { @Test public void itLimitsOutputSize() { - JinjavaConfig outputSizeLimitedConfig = JinjavaConfig - .newBuilder() + JinjavaConfig outputSizeLimitedConfig = BaseJinjavaTest + .newConfigBuilder() .withMaxOutputSize(20) .build(); String output = "123456789012345678901234567890"; @@ -219,8 +250,8 @@ public void itLimitsOutputSize() { @Test public void itLimitsOutputSizeOnTagNode() { - JinjavaConfig outputSizeLimitedConfig = JinjavaConfig - .newBuilder() + JinjavaConfig outputSizeLimitedConfig = BaseJinjavaTest + .newConfigBuilder() .withMaxOutputSize(10) .build(); String output = "{% for i in range(20) %} {{ i }} {% endfor %}"; @@ -242,8 +273,8 @@ public void itLimitsOutputSizeOnTagNode() { @Test public void itLimitsOutputSizeWhenSumOfNodeSizesExceedsMax() { - JinjavaConfig outputSizeLimitedConfig = JinjavaConfig - .newBuilder() + JinjavaConfig outputSizeLimitedConfig = BaseJinjavaTest + .newConfigBuilder() .withMaxOutputSize(19) .build(); String input = "1234567890{% block testchild %}1234567890{% endblock %}"; @@ -262,8 +293,8 @@ public void itLimitsOutputSizeWhenSumOfNodeSizesExceedsMax() { @Test public void itCanPreserveRawTags() { - JinjavaConfig preserveConfig = JinjavaConfig - .newBuilder() + JinjavaConfig preserveConfig = BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(PreserveRawExecutionMode.instance()) .build(); String input = "1{% raw %}2{% endraw %}3"; @@ -287,7 +318,7 @@ public void itKnowsThatMethodIsResolved() { "{% set a, b = {}, [] %}{% macro a.foo()%} 1-{{ b.bar() }}. {% endmacro %} {{ a.foo() }}"; RenderResult renderResult = new Jinjava() - .renderForResult(input, ImmutableMap.of("deferred", DeferredValue.instance())); + .renderForResult(input, ImmutableMap.of("deferred", DeferredValue.instance())); assertThat(renderResult.getOutput().trim()).isEqualTo("1-."); // Does not contain an error about 'a.foo()' being unknown. assertThat(renderResult.getErrors()).hasSize(1); @@ -296,20 +327,19 @@ public void itKnowsThatMethodIsResolved() { @Test public void itThrowsFatalErrors() { interpreter.getContext().setThrowInterpreterErrors(true); - assertThatThrownBy( - () -> - interpreter.addError( - new TemplateError( - ErrorType.FATAL, - ErrorReason.UNKNOWN, - ErrorItem.PROPERTY, - "", - "", - interpreter.getLineNumber(), - interpreter.getPosition(), - new RuntimeException() - ) + assertThatThrownBy(() -> + interpreter.addError( + new TemplateError( + ErrorType.FATAL, + ErrorReason.UNKNOWN, + ErrorItem.PROPERTY, + "", + "", + interpreter.getLineNumber(), + interpreter.getPosition(), + new RuntimeException() ) + ) ) .isInstanceOf(TemplateSyntaxException.class); assertThat(interpreter.getErrors()).isEmpty(); @@ -351,6 +381,266 @@ public void itInterpretsWhitespaceControl() { @Test public void itInterpretsEmptyExpressions() { + jinjava = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withTimeZone(ZoneId.of("America/New_York")) + .withLegacyOverrides( + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withParseWhitespaceControlStrictly(false) + .build() + ) + .build() + ); + interpreter = jinjava.newInterpreter(); 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).isIn("Oct 20, 2022, 5:09:43 PM", "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")) + ); + } + + @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); + } + + @Test + public void itPreventsAccidentalExpressions() { + String makeExpression = "if (true) {\n{%- print deferred -%}\n}"; + String makeTag = "if (true) {\n{%- print '% print 123 %' -%}\n}"; + String makeNote = "if (true) {\n{%- print '# note #' -%}\n}"; + jinjava.getGlobalContext().put("deferred", DeferredValue.instance()); + + JinjavaInterpreter normalInterpreter = new JinjavaInterpreter( + jinjava, + jinjava.getGlobalContext(), + BaseJinjavaTest + .newConfigBuilder() + .withExecutionMode(EagerExecutionMode.instance()) + .build() + ); + JinjavaInterpreter preventingInterpreter = new JinjavaInterpreter( + jinjava, + jinjava.getGlobalContext(), + BaseJinjavaTest + .newConfigBuilder() + .withFeatureConfig( + FeatureConfig + .newBuilder() + .add(OutputList.PREVENT_ACCIDENTAL_EXPRESSIONS, FeatureStrategies.ACTIVE) + .build() + ) + .withExecutionMode(EagerExecutionMode.instance()) + .build() + ); + JinjavaInterpreter.pushCurrent(normalInterpreter); + try { + assertThat(normalInterpreter.render(makeExpression)) + .isEqualTo("if (true) {{% print deferred %}}"); + assertThat(normalInterpreter.render(makeTag)) + .isEqualTo("if (true) {% print 123 %}"); + assertThat(normalInterpreter.render(makeNote)).isEqualTo("if (true) {# note #}"); + } finally { + JinjavaInterpreter.popCurrent(); + } + JinjavaInterpreter.pushCurrent(preventingInterpreter); + try { + assertThat(preventingInterpreter.render(makeExpression)) + .isEqualTo("if (true) {\n" + "{#- #}{% print deferred %}}"); + assertThat(preventingInterpreter.render(makeTag)) + .isEqualTo("if (true) {\n" + "{#- #}% print 123 %}"); + assertThat(preventingInterpreter.render(makeNote)) + .isEqualTo("if (true) {\n" + "{#- #}# note #}"); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + + @Test + public void itOutputsUndefinedVariableError() { + String template = "{% set foo=123 %}{{ foo }}{{ bar }}"; + + JinjavaInterpreter normalInterpreter = new JinjavaInterpreter( + jinjava, + jinjava.getGlobalContext(), + BaseJinjavaTest + .newConfigBuilder() + .withExecutionMode(EagerExecutionMode.instance()) + .build() + ); + JinjavaInterpreter outputtingErrorInterpreters = new JinjavaInterpreter( + jinjava, + jinjava.getGlobalContext(), + BaseJinjavaTest + .newConfigBuilder() + .withFeatureConfig( + FeatureConfig + .newBuilder() + .add( + JinjavaInterpreter.OUTPUT_UNDEFINED_VARIABLES_ERROR, + FeatureStrategies.ACTIVE + ) + .build() + ) + .withExecutionMode(EagerExecutionMode.instance()) + .build() + ); + + String normalRenderResult = normalInterpreter.render(template); + String outputtingErrorRenderResult = outputtingErrorInterpreters.render(template); + assertThat(normalRenderResult).isEqualTo("123"); + assertThat(outputtingErrorRenderResult).isEqualTo("123"); + assertThat(normalInterpreter.getErrors()).isEmpty(); + assertThat(outputtingErrorInterpreters.getErrors().size()).isEqualTo(1); + assertThat(outputtingErrorInterpreters.getErrors().get(0).getMessage()) + .contains("Undefined variable: 'bar'"); + assertThat(outputtingErrorInterpreters.getErrors().get(0).getReason()) + .isEqualTo(ErrorReason.UNKNOWN); + assertThat(outputtingErrorInterpreters.getErrors().get(0).getSeverity()) + .isEqualTo(ErrorType.WARNING); + assertThat(outputtingErrorInterpreters.getErrors().get(0).getCategoryErrors()) + .isEqualTo(ImmutableMap.of("variable", "bar")); + } + + @Test + public void itDoesNotAllowAccessingPropertiesOfInterpreter() { + assertThat(jinjava.render("{{ null.config }}", new HashMap<>())).isEqualTo(""); + } } diff --git a/src/test/java/com/hubspot/jinjava/interpret/LegacyOperatorPrecedenceTest.java b/src/test/java/com/hubspot/jinjava/interpret/LegacyOperatorPrecedenceTest.java index 649e6ec02..c9849a9a0 100644 --- a/src/test/java/com/hubspot/jinjava/interpret/LegacyOperatorPrecedenceTest.java +++ b/src/test/java/com/hubspot/jinjava/interpret/LegacyOperatorPrecedenceTest.java @@ -3,14 +3,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; -import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; import java.util.HashMap; import org.junit.Before; import org.junit.Test; public class LegacyOperatorPrecedenceTest { + Jinjava legacy; Jinjava modern; @@ -18,12 +19,15 @@ public class LegacyOperatorPrecedenceTest { public void setUp() throws Exception { legacy = new Jinjava( - JinjavaConfig.newBuilder().withLegacyOverrides(LegacyOverrides.NONE).build() + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides(LegacyOverrides.NONE) + .build() ); modern = new Jinjava( - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withLegacyOverrides( LegacyOverrides.newBuilder().withUseNaturalOperatorPrecedence(true).build() ) diff --git a/src/test/java/com/hubspot/jinjava/interpret/LegacyWhitespaceControlParsingTest.java b/src/test/java/com/hubspot/jinjava/interpret/LegacyWhitespaceControlParsingTest.java index c87e0228c..09289d4b5 100644 --- a/src/test/java/com/hubspot/jinjava/interpret/LegacyWhitespaceControlParsingTest.java +++ b/src/test/java/com/hubspot/jinjava/interpret/LegacyWhitespaceControlParsingTest.java @@ -3,14 +3,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; -import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; import java.util.HashMap; import org.junit.Before; import org.junit.Test; public class LegacyWhitespaceControlParsingTest { + Jinjava legacy; Jinjava modern; @@ -18,12 +19,15 @@ public class LegacyWhitespaceControlParsingTest { public void setUp() throws Exception { legacy = new Jinjava( - JinjavaConfig.newBuilder().withLegacyOverrides(LegacyOverrides.NONE).build() + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides(LegacyOverrides.NONE) + .build() ); modern = new Jinjava( - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withLegacyOverrides( LegacyOverrides.newBuilder().withParseWhitespaceControlStrictly(true).build() ) diff --git a/src/test/java/com/hubspot/jinjava/interpret/NullValueTest.java b/src/test/java/com/hubspot/jinjava/interpret/NullValueTest.java new file mode 100644 index 000000000..ab484ce60 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/interpret/NullValueTest.java @@ -0,0 +1,23 @@ +package com.hubspot.jinjava.interpret; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import org.junit.Test; + +public class NullValueTest { + + @Test + public void itSerializesUnderlyingValue() throws JsonProcessingException { + LazyExpression expression = LazyExpression.of( + () -> ImmutableMap.of("test", "hello", "test2", NullValue.instance()), + "{}" + ); + Object evaluated = expression.get(); + assertThat(evaluated).isNotNull(); + assertThat(new ObjectMapper().writeValueAsString(expression)) + .isEqualTo("{\"test\":\"hello\",\"test2\":null}"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/interpret/PartiallyDeferredValueTest.java b/src/test/java/com/hubspot/jinjava/interpret/PartiallyDeferredValueTest.java new file mode 100644 index 000000000..55a86027c --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/interpret/PartiallyDeferredValueTest.java @@ -0,0 +1,193 @@ +package com.hubspot.jinjava.interpret; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.LegacyOverrides; +import com.hubspot.jinjava.mode.EagerExecutionMode; +import com.hubspot.jinjava.objects.collections.PyMap; +import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy; +import com.hubspot.jinjava.testobjects.PartiallyDeferredValueTestObjects; +import org.junit.Before; +import org.junit.Test; + +public class PartiallyDeferredValueTest extends BaseInterpretingTest { + + @Before + public void setup() { + JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() + .withRandomNumberGeneratorStrategy(RandomNumberGeneratorStrategy.DEFERRED) + .withExecutionMode(EagerExecutionMode.instance()) + .withNestedInterpretationEnabled(true) + .withLegacyOverrides( + LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() + ) + .withMaxMacroRecursionDepth(5) + .withEnableRecursiveMacroCalls(true) + .build(); + JinjavaInterpreter parentInterpreter = new JinjavaInterpreter( + jinjava, + new Context(), + config + ); + interpreter = new JinjavaInterpreter(parentInterpreter); + + interpreter.getContext().put("deferred", DeferredValue.instance()); + } + + @Test + public void itDefersNodeWhenCannotSerializePartiallyDeferredValue() { + interpreter + .getContext() + .put("foo", new PartiallyDeferredValueTestObjects.BadSerialization()); + assertThat(interpreter.render("{% set bar = foo %}{{ bar.resolved }}")) + .isEqualTo("resolved"); + assertThat(interpreter.render("{% set bar = foo %}{{ bar.deferred }}")) + .isEqualTo("{{ bar.deferred }}"); + assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); + } + + @Test + public void itDefersNodeWhenCannotCallPartiallyDeferredMapEntrySet() { + interpreter + .getContext() + .put( + "foo", + new PartiallyDeferredValueTestObjects.BadEntrySet( + ImmutableMap.of("resolved", "resolved") + ) + ); + assertThat(interpreter.render("{% set bar = foo %}{{ bar.resolved }}")) + .isEqualTo("resolved"); + assertThat(interpreter.render("{% set bar = foo %}{{ bar.deferred }}")) + .isEqualTo("{{ bar.deferred }}"); + assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); + } + + @Test + public void itDefersNodeWhenPyishSerializationFails() { + interpreter + .getContext() + .put("foo", new PartiallyDeferredValueTestObjects.BadPyishSerializable()); + assertThat(interpreter.render("{% set bar = foo %}{{ bar.resolved }}")) + .isEqualTo("resolved"); + assertThat(interpreter.render("{% set bar = foo %}{{ bar.deferred }}")) + .isEqualTo("{{ bar.deferred }}"); + assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); + } + + @Test + public void itSerializesWhenPyishSerializationIsGood() { + interpreter + .getContext() + .put("foo", new PartiallyDeferredValueTestObjects.GoodPyishSerializable()); + assertThat(interpreter.render("{% set bar = foo %}{{ bar.resolved }}")) + .isEqualTo("resolved"); + assertThat(interpreter.render("{% set bar = foo %}{{ bar.deferred }}")) + .isEqualTo("{{ good.deferred }}"); + assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); + } + + @Test + public void itSerializesWhenEntrySetIsBadButItIsPyishSerializable() { + interpreter + .getContext() + .put( + "foo", + new PartiallyDeferredValueTestObjects.BadEntrySetButPyishSerializable( + ImmutableMap.of("resolved", "resolved") + ) + ); + assertThat(interpreter.render("{% set bar = foo %}{{ bar.resolved }}")) + .isEqualTo("resolved"); + assertThat(interpreter.render("{% set bar = foo %}{{ bar.deferred }}")) + .isEqualTo("{{ hello.deferred }}"); + assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); + } + + @Test + public void itSerializesPartiallyDeferredValueIsInsideAMap() { + interpreter + .getContext() + .put( + "foo_map", + new PyMap( + ImmutableMap.of( + "foo", + new PartiallyDeferredValueTestObjects.GoodPyishSerializable() + ) + ) + ); + assertThat(interpreter.render("{% set bar = foo_map %}{{ bar.foo.resolved }}")) + .isEqualTo("resolved"); + assertThat(interpreter.render("{% set bar = foo_map.foo %}{{ bar.resolved }}")) + .isEqualTo("resolved"); + assertThat(interpreter.render("{% set bar = foo_map %}{{ bar.foo.deferred }}")) + .isEqualTo("{{ good.deferred }}"); + assertThat(interpreter.render("{% set bar = foo_map.foo %}{{ bar.deferred }}")) + .isEqualTo("{{ good.deferred }}"); + assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); + } + + @Test + public void itSerializesPartiallyDeferredValueIsPutInsideAMap() { + interpreter + .getContext() + .put("foo", new PartiallyDeferredValueTestObjects.GoodPyishSerializable()); + assertThat( + interpreter.render("{% set bar = {'my_key': foo} %}{% print bar.my_key.resolved %}") + ) + .isEqualTo("resolved"); + assertThat( + interpreter.render("{% set bar = {'my_key': foo} %}{% print bar.my_key.deferred %}") + ) + .isEqualTo("{% print good.deferred %}"); + assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); + } + + @Test + public void itSerializesPartiallyDeferredValueIsPutInsideAMapInComplexExpression() { + interpreter + .getContext() + .put("foo", new PartiallyDeferredValueTestObjects.GoodPyishSerializable()); + assertThat( + interpreter.render( + "{% set bar = {'my_key': foo} %}{% print (1 + 1 == 3 || bar.my_key.resolved) ~ '.' %}" + ) + ) + .isEqualTo("resolved."); + assertThat( + interpreter.render( + "{% set bar = {'my_key': foo} %}{% print (1 + 1 == 3 || bar.my_key.deferred) ~ '.' %}" + ) + ) + .isEqualTo("{% print (false || good.deferred) ~ '.' %}"); + assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); + } + + @Test + public void itSerializesPartiallyDeferredValueInsteadOfPreservingOriginalIdentifier() { + interpreter + .getContext() + .put("foo", new PartiallyDeferredValueTestObjects.GoodPyishSerializable()); + assertThat( + interpreter.render( + "{% set list = [] %}{% set bar = foo %}{% do list.append(bar['resolved']) %}{% print list %}" + ) + ) + .isEqualTo("['resolved']"); + assertThat( + interpreter.render( + "{% set list = [] %}{% set bar = foo %}{% do list.append(bar['deferred']) %}{% print list %}" + ) + ) + .isEqualTo( + "{% set list = [] %}{% do list.append(good['deferred']) %}{% print list %}" + ); + assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); + } +} diff --git a/src/test/java/com/hubspot/jinjava/interpret/TemplateErrorTest.java b/src/test/java/com/hubspot/jinjava/interpret/TemplateErrorTest.java index c0e4d9ad2..7abeec039 100644 --- a/src/test/java/com/hubspot/jinjava/interpret/TemplateErrorTest.java +++ b/src/test/java/com/hubspot/jinjava/interpret/TemplateErrorTest.java @@ -56,7 +56,7 @@ public void itRetainsFieldNameCaseForUnknownToken() { @Test public void itSetsFieldNameCaseForSyntaxErrorInFor() { RenderResult renderResult = new Jinjava() - .renderForResult("{% for item inna navigation %}{% endfor %}", ImmutableMap.of()); + .renderForResult("{% for item inna navigation %}{% endfor %}", ImmutableMap.of()); assertThat(renderResult.getErrors().get(0).getFieldName()) .isEqualTo("item inna navigation"); } diff --git a/src/test/java/com/hubspot/jinjava/interpret/VariableFunctionTest.java b/src/test/java/com/hubspot/jinjava/interpret/VariableFunctionTest.java index 132935be3..a0b8169f7 100644 --- a/src/test/java/com/hubspot/jinjava/interpret/VariableFunctionTest.java +++ b/src/test/java/com/hubspot/jinjava/interpret/VariableFunctionTest.java @@ -21,6 +21,7 @@ import org.junit.Test; public class VariableFunctionTest { + private static final DynamicVariableResolver VARIABLE_FUNCTION = s -> { switch (s) { case "name": 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..01fb8b56a 100644 --- a/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategyTest.java @@ -3,8 +3,11 @@ import static org.assertj.core.api.Assertions.assertThat; import com.google.common.collect.ImmutableMap; -import com.hubspot.jinjava.JinjavaConfig; +import com.google.common.collect.ImmutableSet; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.LegacyOverrides; +import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.Context.Library; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; @@ -14,15 +17,24 @@ import com.hubspot.jinjava.objects.collections.PyList; import com.hubspot.jinjava.tree.ExpressionNodeTest; import java.util.ArrayList; -import java.util.Collections; -import org.junit.After; import org.junit.Before; import org.junit.Test; public class EagerExpressionStrategyTest extends ExpressionNodeTest { + private Jinjava jinjava; + + class EagerExecutionModeNoRaw extends EagerExecutionMode { + + @Override + public boolean isPreserveRawTags() { + return false; // So that we can run all the ExpressionNodeTest tests without having the extra `{% raw %}` tags inserted + } + } + @Before public void eagerSetup() throws Exception { + jinjava = new Jinjava(BaseJinjavaTest.newConfigBuilder().build()); jinjava .getGlobalContext() .registerFunction( @@ -35,19 +47,27 @@ public void eagerSetup() throws Exception { interpreter = new JinjavaInterpreter( jinjava, - context, - JinjavaConfig - .newBuilder() + new Context(), + BaseJinjavaTest + .newConfigBuilder() + .withExecutionMode(new EagerExecutionModeNoRaw()) + .build() + ); + nestedInterpreter = + new JinjavaInterpreter( + jinjava, + interpreter.getContext(), + BaseJinjavaTest + .newConfigBuilder() + .withNestedInterpretationEnabled(true) + .withLegacyOverrides( + LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() + ) .withExecutionMode(EagerExecutionMode.instance()) .build() ); - JinjavaInterpreter.pushCurrent(interpreter); - context.put("deferred", DeferredValue.instance()); - } - - @After - public void teardown() { - JinjavaInterpreter.popCurrent(); + interpreter.getContext().put("deferred", DeferredValue.instance()); + nestedInterpreter.getContext().put("deferred", DeferredValue.instance()); } @Test @@ -55,31 +75,24 @@ public void itPreservesRawTags() { interpreter = new JinjavaInterpreter( jinjava, - context, - JinjavaConfig - .newBuilder() - .withNestedInterpretationEnabled(false) - .withLegacyOverrides( - LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() - ) + new Context(), + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(EagerExecutionMode.instance()) .build() ); - JinjavaInterpreter.pushCurrent(interpreter); - try { - assertExpectedOutput( - "{{ '{{ foo }}' }} {{ '{% something %}' }} {{ 'not needed' }}", - "{% raw %}{{ foo }}{% endraw %} {% raw %}{% something %}{% endraw %} not needed" - ); - } finally { - JinjavaInterpreter.popCurrent(); - } + assertExpectedOutput( + interpreter, + "{{ '{{ foo }}' }} {{ '{% something %}' }} {{ 'not needed' }}", + "{% raw %}{{ foo }}{% endraw %} {% raw %}{% something %}{% endraw %} not needed" + ); } @Test public void itPreservesRawTagsNestedInterpretation() { - context.put("bar", "bar"); + nestedInterpreter.getContext().put("bar", "bar"); assertExpectedOutput( + nestedInterpreter, "{{ '{{ 12345 }}' }} {{ '{% print bar %}' }} {{ 'not needed' }}", "12345 bar not needed" ); @@ -88,6 +101,7 @@ public void itPreservesRawTagsNestedInterpretation() { @Test public void itPrependsMacro() { assertExpectedOutput( + interpreter, "{% macro foo(bar) %} {{ bar }} {% endmacro %}{{ foo(deferred) }}", "{% macro foo(bar) %} {{ bar }} {% endmacro %}{{ foo(deferred) }}" ); @@ -95,8 +109,9 @@ public void itPrependsMacro() { @Test public void itPrependsSet() { - context.put("foo", new PyList(new ArrayList<>())); + interpreter.getContext().put("foo", new PyList(new ArrayList<>())); assertExpectedOutput( + interpreter, "{{ foo.append(deferred) }}", "{% set foo = [] %}{{ foo.append(deferred) }}" ); @@ -104,8 +119,9 @@ public void itPrependsSet() { @Test public void itDoesConcatenation() { - context.put("foo", "y'all"); + interpreter.getContext().put("foo", "y'all"); assertExpectedOutput( + interpreter, "{{ 'oh, ' ~ foo ~ foo ~ ' toaster' }}", "oh, y'ally'all toaster" ); @@ -116,6 +132,7 @@ public void itHandlesQuotesLikeJinja() { // {{ 'a|\'|\\\'|\\\\\'|"|\"|\\"|\\\\"|a ' ~ " b|\"|\\\"|\\\\\"|'|\'|\\'|\\\\'|b" }} // --> a|'|\'|\\'|"|"|\"|\\"|a b|"|\"|\\"|'|'|\'|\\'|b assertExpectedOutput( + interpreter, "{{ 'a|\\'|\\\\\\'|\\\\\\\\\\'|\"|\\\"|\\\\\"|\\\\\\\\\"|a ' " + "~ \" b|\\\"|\\\\\\\"|\\\\\\\\\\\"|'|\\'|\\\\'|\\\\\\\\'|b\" }}", "a|'|\\'|\\\\'|\"|\"|\\\"|\\\\\"|a b|\"|\\\"|\\\\\"|'|'|\\'|\\\\'|b" @@ -125,6 +142,7 @@ public void itHandlesQuotesLikeJinja() { @Test public void itGoesIntoDeferredExecutionMode() { assertExpectedOutput( + interpreter, "{{ is_deferred_execution_mode() }}" + "{% if deferred %}{{ is_deferred_execution_mode() }}{% endif %}" + "{{ is_deferred_execution_mode() }}", @@ -133,32 +151,41 @@ public void itGoesIntoDeferredExecutionMode() { } @Test - public void itDoesNotGoIntoDeferredExecutionModeWithMacro() { + public void itGoesIntoDeferredExecutionModeWithMacro() { assertExpectedOutput( + interpreter, "{% 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" ); } @Test public void itDoesNotGoIntoDeferredExecutionModeUnnecessarily() { - assertExpectedOutput("{{ is_deferred_execution_mode() }}", "false"); + assertExpectedOutput(interpreter, "{{ is_deferred_execution_mode() }}", "false"); interpreter.getContext().setDeferredExecutionMode(true); - assertExpectedOutput("{{ is_deferred_execution_mode() }}", "true"); + assertExpectedOutput(interpreter, "{{ is_deferred_execution_mode() }}", "true"); } @Test public void itDoesNotNestedInterpretIfThereAreFakeNotes() { - assertExpectedOutput("{{ '{#something_to_{{keep}}' }}", "{#something_to_{{keep}}"); + assertExpectedOutput( + nestedInterpreter, + "{{ '{#something_to_{{keep}}' }}", + "{#something_to_{{keep}}" + ); } @Test public void itDoesNotReconstructWithDoubleCurlyBraces() { interpreter.getContext().put("foo", ImmutableMap.of("foo", ImmutableMap.of())); - assertExpectedOutput("{{ deferred ~ foo }}", "{{ deferred ~ {'foo': {} } }}"); + assertExpectedOutput( + interpreter, + "{{ deferred ~ foo }}", + "{{ deferred ~ {'foo': {} } }}" + ); } @Test @@ -167,6 +194,7 @@ public void itDoesNotReconstructWithNestedDoubleCurlyBraces() { .getContext() .put("foo", ImmutableMap.of("foo", ImmutableMap.of("bar", ImmutableMap.of()))); assertExpectedOutput( + interpreter, "{{ deferred ~ foo }}", "{{ deferred ~ {'foo': {'bar': {} } } }}" ); @@ -175,6 +203,7 @@ public void itDoesNotReconstructWithNestedDoubleCurlyBraces() { @Test public void itDoesNotReconstructDirectlyWrittenWithDoubleCurlyBraces() { assertExpectedOutput( + interpreter, "{{ deferred ~ {\n'foo': {\n'bar': deferred\n}\n}\n }}", "{{ deferred ~ {'foo': {'bar': deferred} } }}" ); @@ -184,6 +213,7 @@ public void itDoesNotReconstructDirectlyWrittenWithDoubleCurlyBraces() { public void itReconstructsWithNestedInterpretation() { interpreter.getContext().put("foo", "{{ print 'bar' }}"); assertExpectedOutput( + interpreter, "{{ deferred ~ foo }}", "{{ deferred ~ '{{ print \\'bar\\' }}' }}" ); @@ -192,18 +222,24 @@ public void itReconstructsWithNestedInterpretation() { @Test public void itDoesNotDoNestedInterpretationWithSyntaxErrors() { try ( - InterpreterScopeClosable c = interpreter.enterScope( - ImmutableMap.of(Library.TAG, Collections.singleton("print")) + InterpreterScopeClosable c = nestedInterpreter.enterScope( + ImmutableMap.of(Library.TAG, ImmutableSet.of("print")) ) ) { - interpreter.getContext().put("foo", "{% print 'bar' %}"); + nestedInterpreter.getContext().put("foo", "{% print 'bar' %}"); // Rather than rendering this to an empty string - assertThat(interpreter.render("{{ foo }}")).isEqualTo("{% print 'bar' %}"); + assertExpectedOutput(nestedInterpreter, "{{ foo }}", "{% print 'bar' %}"); } } - private void assertExpectedOutput(String inputTemplate, String expectedOutput) { - assertThat(interpreter.render(inputTemplate)).isEqualTo(expectedOutput); + private void assertExpectedOutput( + JinjavaInterpreter interpreter, + String inputTemplate, + String expectedOutput + ) { + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + assertThat(a.value().render(inputTemplate)).isEqualTo(expectedOutput); + } } public static boolean isDeferredExecutionMode() { diff --git a/src/test/java/com/hubspot/jinjava/lib/exptest/IsContainingAllExpTestTest.java b/src/test/java/com/hubspot/jinjava/lib/exptest/IsContainingAllExpTestTest.java index 89d381816..37e2d5cbd 100644 --- a/src/test/java/com/hubspot/jinjava/lib/exptest/IsContainingAllExpTestTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/exptest/IsContainingAllExpTestTest.java @@ -7,72 +7,73 @@ import org.junit.Test; public class IsContainingAllExpTestTest extends BaseJinjavaTest { + private static final String CONTAINING_TEMPLATE = "{%% if %s is containingall %s %%}pass{%% else %%}fail{%% endif %%}"; @Test public void itPassesOnContainedValues() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "[1, 2]"), - new HashMap<>() - ) + jinjava.render( + String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "[1, 2]"), + new HashMap<>() ) + ) .isEqualTo("pass"); } @Test public void itPassesOnContainedDuplicatedValues() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "[1, 2, 2]"), - new HashMap<>() - ) + jinjava.render( + String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "[1, 2, 2]"), + new HashMap<>() ) + ) .isEqualTo("pass"); } @Test public void itFailsOnOnlySomeContainedValues() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "[1, 2, 4]"), - new HashMap<>() - ) + jinjava.render( + String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "[1, 2, 4]"), + new HashMap<>() ) + ) .isEqualTo("fail"); } @Test public void itFailsOnNullSequence() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "null", "[1, 2, 4]"), - new HashMap<>() - ) + jinjava.render( + String.format(CONTAINING_TEMPLATE, "null", "[1, 2, 4]"), + new HashMap<>() ) + ) .isEqualTo("fail"); } @Test public void itFailsOnNullValues() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "null"), - new HashMap<>() - ) + jinjava.render( + String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "null"), + new HashMap<>() ) + ) .isEqualTo("fail"); } @Test public void itPerformsTypeConversion() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "['2', '3']"), - new HashMap<>() - ) + jinjava.render( + String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "['2', '3']"), + new HashMap<>() ) + ) .isEqualTo("pass"); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/exptest/IsContainingExpTestTest.java b/src/test/java/com/hubspot/jinjava/lib/exptest/IsContainingExpTestTest.java index 652270fc6..bceab173d 100644 --- a/src/test/java/com/hubspot/jinjava/lib/exptest/IsContainingExpTestTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/exptest/IsContainingExpTestTest.java @@ -7,80 +7,78 @@ import org.junit.Test; public class IsContainingExpTestTest extends BaseJinjavaTest { + private static final String CONTAINING_TEMPLATE = "{%% if %s is containing %s %%}pass{%% else %%}fail{%% endif %%}"; @Test public void itPassesOnContainedValue() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "2"), - new HashMap<>() - ) + jinjava.render( + String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "2"), + new HashMap<>() ) + ) .isEqualTo("pass"); } @Test public void itFailsOnNullContainedValue() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "[1, 2, null]", "null"), - new HashMap<>() - ) + jinjava.render( + String.format(CONTAINING_TEMPLATE, "[1, 2, null]", "null"), + new HashMap<>() ) + ) .isEqualTo("fail"); } @Test public void itFailsOnMissingValue() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "4"), - new HashMap<>() - ) + jinjava.render( + String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "4"), + new HashMap<>() ) + ) .isEqualTo("fail"); } @Test public void itFailsOnEmptyValue() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", ""), - new HashMap<>() - ) - ) + jinjava.render(String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", ""), new HashMap<>()) + ) .isEqualTo("fail"); } @Test public void itFailsOnNullValue() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "null"), - new HashMap<>() - ) + jinjava.render( + String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "null"), + new HashMap<>() ) + ) .isEqualTo("fail"); } @Test public void itFailsOnNullSequence() { assertThat( - jinjava.render(String.format(CONTAINING_TEMPLATE, "null", "2"), new HashMap<>()) - ) + jinjava.render(String.format(CONTAINING_TEMPLATE, "null", "2"), new HashMap<>()) + ) .isEqualTo("fail"); } @Test public void itPerformsTypeConversion() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "'2'"), - new HashMap<>() - ) + jinjava.render( + String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "'2'"), + new HashMap<>() ) + ) .isEqualTo("pass"); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/exptest/IsEqualToExpTestTest.java b/src/test/java/com/hubspot/jinjava/lib/exptest/IsEqualToExpTestTest.java index 695dadd71..12c774ca4 100644 --- a/src/test/java/com/hubspot/jinjava/lib/exptest/IsEqualToExpTestTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/exptest/IsEqualToExpTestTest.java @@ -3,10 +3,13 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hubspot.jinjava.BaseJinjavaTest; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import org.junit.Test; public class IsEqualToExpTestTest extends BaseJinjavaTest { + private static final String EQUAL_TEMPLATE = "{{ %s is equalto %s }}"; @Test @@ -20,50 +23,94 @@ public void itEquatesNumbers() { @Test public void itEquatesStrings() { assertThat( - jinjava.render( - String.format(EQUAL_TEMPLATE, "\"jinjava\"", "\"jinjava\""), - new HashMap<>() - ) + jinjava.render( + String.format(EQUAL_TEMPLATE, "\"jinjava\"", "\"jinjava\""), + new HashMap<>() ) + ) .isEqualTo("true"); assertThat( - jinjava.render( - String.format(EQUAL_TEMPLATE, "\"jinjava\"", "\"not jinjava\""), - new HashMap<>() - ) + jinjava.render( + String.format(EQUAL_TEMPLATE, "\"jinjava\"", "\"not jinjava\""), + new HashMap<>() ) + ) .isEqualTo("false"); } @Test - public void itEquatesBooleans() { + public void itEquatesCollectionsToStrings() { assertThat( - jinjava.render(String.format(EQUAL_TEMPLATE, "true", "true"), new HashMap<>()) + jinjava.render( + String.format(EQUAL_TEMPLATE, "\"[1, 2, 3]\"", "[1, 2, 3]"), + new HashMap<>() ) + ) .isEqualTo("true"); + assertThat( - jinjava.render(String.format(EQUAL_TEMPLATE, "true", "false"), new HashMap<>()) + jinjava.render( + String.format(EQUAL_TEMPLATE, "\"[1, 2, 3]\"", "[1, 2, 4]"), + new HashMap<>() ) + ) + .isEqualTo("false"); + } + + @Test + public void itEquatesLargeCollectionsAndStrings() { + assertThat(compareStringAndCollection(100_000)).isEqualTo("true"); + } + + @Test + public void itDoesNotEquateHugeCollectionsAndStrings() { + assertThat(compareStringAndCollection(500_000)).isEqualTo("false"); + } + + private String compareStringAndCollection(int size) { + List bigList = new ArrayList<>(); + + for (int i = 0; i < size; i++) { + bigList.add(1); + } + + String bigString = bigList.toString(); + + return jinjava.render( + String.format(EQUAL_TEMPLATE, "\"" + bigString + "\"", bigString), + new HashMap<>() + ); + } + + @Test + public void itEquatesBooleans() { + assertThat( + jinjava.render(String.format(EQUAL_TEMPLATE, "true", "true"), new HashMap<>()) + ) + .isEqualTo("true"); + assertThat( + jinjava.render(String.format(EQUAL_TEMPLATE, "true", "false"), new HashMap<>()) + ) .isEqualTo("false"); } @Test public void itEquatesDifferentTypes() { assertThat( - jinjava.render(String.format(EQUAL_TEMPLATE, "4", "\"4\""), new HashMap<>()) - ) + jinjava.render(String.format(EQUAL_TEMPLATE, "4", "\"4\""), new HashMap<>()) + ) .isEqualTo("true"); assertThat( - jinjava.render(String.format(EQUAL_TEMPLATE, "4", "\"5\""), new HashMap<>()) - ) + jinjava.render(String.format(EQUAL_TEMPLATE, "4", "\"5\""), new HashMap<>()) + ) .isEqualTo("false"); assertThat( - jinjava.render(String.format(EQUAL_TEMPLATE, "'c'", "\"c\""), new HashMap<>()) - ) + jinjava.render(String.format(EQUAL_TEMPLATE, "'c'", "\"c\""), new HashMap<>()) + ) .isEqualTo("true"); assertThat( - jinjava.render(String.format(EQUAL_TEMPLATE, "'c'", "\"b\""), new HashMap<>()) - ) + jinjava.render(String.format(EQUAL_TEMPLATE, "'c'", "\"b\""), new HashMap<>()) + ) .isEqualTo("false"); } diff --git a/src/test/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTestTest.java b/src/test/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTestTest.java index e93272d27..9667a6bbe 100644 --- a/src/test/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTestTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTestTest.java @@ -34,17 +34,14 @@ public void testWithAddFilter() { assertThat(jinjava.render("{{ (4|add(-4.5)) is float }}", new HashMap<>())) .isEqualTo("true"); assertThat( - jinjava.render( - "{{ (4|add(4.0000000000000000000001)) is float }}", - new HashMap<>() - ) - ) + jinjava.render("{{ (4|add(4.0000000000000000000001)) is float }}", new HashMap<>()) + ) .isEqualTo("true"); assertThat(jinjava.render("{{ (4|add(40.0)) is float }}", new HashMap<>())) .isEqualTo("true"); assertThat( - jinjava.render("{{ (4|add(1000000000000000000)) is float }}", new HashMap<>()) - ) + jinjava.render("{{ (4|add(1000000000000000000)) is float }}", new HashMap<>()) + ) .isEqualTo("false"); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/exptest/IsIntegerExpTestTest.java b/src/test/java/com/hubspot/jinjava/lib/exptest/IsIntegerExpTestTest.java index 62ae20da4..458767471 100644 --- a/src/test/java/com/hubspot/jinjava/lib/exptest/IsIntegerExpTestTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/exptest/IsIntegerExpTestTest.java @@ -14,8 +14,8 @@ public void testValidIntegers() { assertThat(jinjava.render("{{ -1 is integer }}", new HashMap<>())).isEqualTo("true"); long number = Integer.MAX_VALUE; assertThat( - jinjava.render(String.format("{{ %d is integer }}", number + 1), new HashMap<>()) - ) + jinjava.render(String.format("{{ %d is integer }}", number + 1), new HashMap<>()) + ) .isEqualTo("true"); assertThat(jinjava.render("{{ 1000000000000000000 is integer }}", new HashMap<>())) .isEqualTo("true"); @@ -40,17 +40,17 @@ public void testWithAddFilter() { assertThat(jinjava.render("{{ (4|add(-4.5)) is integer }}", new HashMap<>())) .isEqualTo("false"); assertThat( - jinjava.render( - "{{ (4|add(4.0000000000000000000001)) is integer }}", - new HashMap<>() - ) + jinjava.render( + "{{ (4|add(4.0000000000000000000001)) is integer }}", + new HashMap<>() ) + ) .isEqualTo("false"); assertThat(jinjava.render("{{ (4|add(40.0)) is integer }}", new HashMap<>())) .isEqualTo("false"); assertThat( - jinjava.render("{{ (4|add(1000000000000000000)) is integer }}", new HashMap<>()) - ) + jinjava.render("{{ (4|add(1000000000000000000)) is integer }}", new HashMap<>()) + ) .isEqualTo("true"); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/exptest/IsStringContainingExpTestTest.java b/src/test/java/com/hubspot/jinjava/lib/exptest/IsStringContainingExpTestTest.java index 268445679..e582316f0 100644 --- a/src/test/java/com/hubspot/jinjava/lib/exptest/IsStringContainingExpTestTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/exptest/IsStringContainingExpTestTest.java @@ -8,38 +8,39 @@ import org.junit.Test; public class IsStringContainingExpTestTest extends BaseJinjavaTest { + private static final String CONTAINING_TEMPLATE = "{{ var is string_containing arg }}"; @Test public void itReturnsTrueForContainedString() { assertThat( - jinjava.render( - CONTAINING_TEMPLATE, - ImmutableMap.of("var", "testing", "arg", "esti") - ) + jinjava.render( + CONTAINING_TEMPLATE, + ImmutableMap.of("var", "testing", "arg", "esti") ) + ) .isEqualTo("true"); assertThat( - jinjava.render(CONTAINING_TEMPLATE, ImmutableMap.of("var", "testing", "arg", "")) - ) + jinjava.render(CONTAINING_TEMPLATE, ImmutableMap.of("var", "testing", "arg", "")) + ) .isEqualTo("true"); assertThat( - jinjava.render( - CONTAINING_TEMPLATE, - ImmutableMap.of("var", "testing", "arg", "testing") - ) + jinjava.render( + CONTAINING_TEMPLATE, + ImmutableMap.of("var", "testing", "arg", "testing") ) + ) .isEqualTo("true"); } @Test public void itReturnsFalseForExcludedString() { assertThat( - jinjava.render( - CONTAINING_TEMPLATE, - ImmutableMap.of("var", "testing", "arg", "blah") - ) + jinjava.render( + CONTAINING_TEMPLATE, + ImmutableMap.of("var", "testing", "arg", "blah") ) + ) .isEqualTo("false"); } @@ -52,11 +53,11 @@ public void itReturnsFalseForNull() { @Test public void itWorksForSafeString() { assertThat( - jinjava.render( - CONTAINING_TEMPLATE, - ImmutableMap.of("var", "testing", "arg", new SafeString("testing")) - ) + jinjava.render( + CONTAINING_TEMPLATE, + ImmutableMap.of("var", "testing", "arg", new SafeString("testing")) ) + ) .isEqualTo("true"); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/exptest/IsStringStartingWithExpTestTest.java b/src/test/java/com/hubspot/jinjava/lib/exptest/IsStringStartingWithExpTestTest.java index 4793bd376..0c3aa585c 100644 --- a/src/test/java/com/hubspot/jinjava/lib/exptest/IsStringStartingWithExpTestTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/exptest/IsStringStartingWithExpTestTest.java @@ -8,35 +8,36 @@ import org.junit.Test; public class IsStringStartingWithExpTestTest extends BaseJinjavaTest { + private static final String STARTING_TEMPLATE = "{{ var is string_startingwith arg }}"; @Test public void itReturnsTrueForContainedString() { assertThat( - jinjava.render(STARTING_TEMPLATE, ImmutableMap.of("var", "testing", "arg", "tes")) - ) + jinjava.render(STARTING_TEMPLATE, ImmutableMap.of("var", "testing", "arg", "tes")) + ) .isEqualTo("true"); assertThat( - jinjava.render(STARTING_TEMPLATE, ImmutableMap.of("var", "testing", "arg", "")) - ) + jinjava.render(STARTING_TEMPLATE, ImmutableMap.of("var", "testing", "arg", "")) + ) .isEqualTo("true"); assertThat( - jinjava.render( - STARTING_TEMPLATE, - ImmutableMap.of("var", "testing", "arg", "testing") - ) + jinjava.render( + STARTING_TEMPLATE, + ImmutableMap.of("var", "testing", "arg", "testing") ) + ) .isEqualTo("true"); } @Test public void itReturnsFalseForExcludedString() { assertThat( - jinjava.render( - STARTING_TEMPLATE, - ImmutableMap.of("var", "testing", "arg", "esting") - ) + jinjava.render( + STARTING_TEMPLATE, + ImmutableMap.of("var", "testing", "arg", "esting") ) + ) .isEqualTo("false"); } @@ -49,11 +50,11 @@ public void itReturnsFalseForNull() { @Test public void itWorksForSafeString() { assertThat( - jinjava.render( - STARTING_TEMPLATE, - ImmutableMap.of("var", "testing", "arg", new SafeString("tes")) - ) + jinjava.render( + STARTING_TEMPLATE, + ImmutableMap.of("var", "testing", "arg", new SafeString("tes")) ) + ) .isEqualTo("true"); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/exptest/IsUpperExpTestTest.java b/src/test/java/com/hubspot/jinjava/lib/exptest/IsUpperExpTestTest.java index e0a7dfd94..e6de57dbc 100644 --- a/src/test/java/com/hubspot/jinjava/lib/exptest/IsUpperExpTestTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/exptest/IsUpperExpTestTest.java @@ -8,6 +8,7 @@ import org.junit.Test; public class IsUpperExpTestTest extends BaseJinjavaTest { + private static final String STARTING_TEMPLATE = "{{ var is upper }}"; private static final String SAFE_TEMPLATE = "{{ (var|safe) is upper }}"; @@ -26,8 +27,8 @@ public void itReturnsFalseForLowerString() { @Test public void itWorksForSafeStrings() { assertThat( - jinjava.render(STARTING_TEMPLATE, ImmutableMap.of("var", new SafeString("UPPER"))) - ) + jinjava.render(STARTING_TEMPLATE, ImmutableMap.of("var", new SafeString("UPPER"))) + ) .isEqualTo("true"); assertThat(jinjava.render(SAFE_TEMPLATE, ImmutableMap.of("var", "UPPER"))) .isEqualTo("true"); diff --git a/src/test/java/com/hubspot/jinjava/lib/exptest/IsWithinExpTestTest.java b/src/test/java/com/hubspot/jinjava/lib/exptest/IsWithinExpTestTest.java index 62b6b705b..664ec5e1b 100644 --- a/src/test/java/com/hubspot/jinjava/lib/exptest/IsWithinExpTestTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/exptest/IsWithinExpTestTest.java @@ -7,14 +7,15 @@ import org.junit.Test; public class IsWithinExpTestTest extends BaseJinjavaTest { + private static final String IN_TEMPLATE = "{%% if %s is within %s %%}pass{%% else %%}fail{%% endif %%}"; @Test public void itPassesOnValueInSequence() { assertThat( - jinjava.render(String.format(IN_TEMPLATE, "2", "[1, 2, 3]"), new HashMap<>()) - ) + jinjava.render(String.format(IN_TEMPLATE, "2", "[1, 2, 3]"), new HashMap<>()) + ) .isEqualTo("pass"); } @@ -33,27 +34,27 @@ public void itPassesOnValueInSequence() { @Test public void itFailsOnValueNotInSequence() { assertThat( - jinjava.render(String.format(IN_TEMPLATE, "4", "[1, 2, 3]"), new HashMap<>()) - ) + jinjava.render(String.format(IN_TEMPLATE, "4", "[1, 2, 3]"), new HashMap<>()) + ) .isEqualTo("fail"); } @Test public void itFailsOnNullValueNotInSequence() { assertThat( - jinjava.render(String.format(IN_TEMPLATE, "null", "[1, 2, 3]"), new HashMap<>()) - ) + jinjava.render(String.format(IN_TEMPLATE, "null", "[1, 2, 3]"), new HashMap<>()) + ) .isEqualTo("fail"); } @Test public void itPerformsTypeConversion() { assertThat( - jinjava.render( - String.format(IN_TEMPLATE, "'1'", "[100000000000, 1]"), - new HashMap<>() - ) + jinjava.render( + String.format(IN_TEMPLATE, "'1'", "[100000000000, 1]"), + new HashMap<>() ) + ) .isEqualTo("pass"); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/exptest/NegatedExpTestTest.java b/src/test/java/com/hubspot/jinjava/lib/exptest/NegatedExpTestTest.java index 2720b983e..58f3c0700 100644 --- a/src/test/java/com/hubspot/jinjava/lib/exptest/NegatedExpTestTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/exptest/NegatedExpTestTest.java @@ -7,6 +7,7 @@ import org.junit.Test; public class NegatedExpTestTest extends BaseJinjavaTest { + private static final String TEMPLATE = "{%% if %s is %s %s %%}pass{%% else %%}fail{%% endif %%}"; private static final String CONTAINING_TEMPLATE = @@ -15,23 +16,23 @@ public class NegatedExpTestTest extends BaseJinjavaTest { @Test public void itNegatesDefined() { assertThat( - jinjava.render(String.format(TEMPLATE, "blah", "", "defined"), new HashMap<>()) - ) + jinjava.render(String.format(TEMPLATE, "blah", "", "defined"), new HashMap<>()) + ) .isEqualTo("fail"); assertThat( - jinjava.render(String.format(TEMPLATE, "blah", "not", "defined"), new HashMap<>()) - ) + jinjava.render(String.format(TEMPLATE, "blah", "not", "defined"), new HashMap<>()) + ) .isEqualTo("pass"); } @Test public void itNegatesContaining() { assertThat( - jinjava.render( - String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "4"), - new HashMap<>() - ) + jinjava.render( + String.format(CONTAINING_TEMPLATE, "[1, 2, 3]", "4"), + new HashMap<>() ) + ) .isEqualTo("pass"); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/exptest/isDivisibleByExpTestTest.java b/src/test/java/com/hubspot/jinjava/lib/exptest/isDivisibleByExpTestTest.java index f01e4d7bc..a9976a30c 100644 --- a/src/test/java/com/hubspot/jinjava/lib/exptest/isDivisibleByExpTestTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/exptest/isDivisibleByExpTestTest.java @@ -11,6 +11,7 @@ import org.junit.Test; public class isDivisibleByExpTestTest extends BaseJinjavaTest { + private static final String DIVISIBLE_BY_TEMPLATE = "{{ %s is divisibleby %s }}"; @Test diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/AbstractFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/AbstractFilterTest.java index 35a45bb43..5a449af72 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/AbstractFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/AbstractFilterTest.java @@ -15,6 +15,7 @@ import org.junit.Test; public class AbstractFilterTest extends BaseInterpretingTest { + private ArgCapturingFilter filter; public static class NoJinjavaDocFilter extends ArgCapturingFilter {} @@ -37,7 +38,7 @@ public void itDoesNotRequireParams() { params = { @JinjavaParam(value = "1st", desc = "1st"), @JinjavaParam(value = "2nd", desc = "2nd"), - @JinjavaParam(value = "3rd", desc = "3rd") + @JinjavaParam(value = "3rd", desc = "3rd"), } ) public static class TwoParamTypesFilter extends ArgCapturingFilter {} @@ -65,7 +66,7 @@ public void itSupportsMixingOfPositionalAndNamedArgs() { @JinjavaParam(value = "double", type = "double", desc = "double"), @JinjavaParam(value = "number", type = "number", desc = "number"), @JinjavaParam(value = "object", type = "object", desc = "object"), - @JinjavaParam(value = "dict", type = "dict", desc = "dict") + @JinjavaParam(value = "dict", type = "dict", desc = "dict"), } ) public static class AllParamTypesFilter extends ArgCapturingFilter {} @@ -107,8 +108,8 @@ public void itParsesNumericAndBooleanInput() { public void itValidatesRequiredArgs() { filter = new AllParamTypesFilter(); - assertThatThrownBy( - () -> filter.filter(null, interpreter, new Object[] {}, Collections.emptyMap()) + assertThatThrownBy(() -> + filter.filter(null, interpreter, new Object[] {}, Collections.emptyMap()) ) .hasMessageContaining("Argument named 'boolean' is required"); } @@ -117,14 +118,13 @@ public void itValidatesRequiredArgs() { public void itErrorsOnTooManyArgs() { filter = new AllParamTypesFilter(); - assertThatThrownBy( - () -> - filter.filter( - null, - interpreter, - new Object[] { true, null, null, null, null, null, null, null, null }, - Collections.emptyMap() - ) + assertThatThrownBy(() -> + filter.filter( + null, + interpreter, + new Object[] { true, null, null, null, null, null, null, null, null }, + Collections.emptyMap() + ) ) .hasMessageContaining("Argument at index") .hasMessageContaining("is invalid"); @@ -134,19 +134,19 @@ public void itErrorsOnTooManyArgs() { public void itErrorsUnknownNamedArg() { filter = new AllParamTypesFilter(); - assertThatThrownBy( - () -> - filter.filter( - null, - interpreter, - new Object[] { true }, - ImmutableMap.of("unknown", "unknown") - ) + assertThatThrownBy(() -> + filter.filter( + null, + interpreter, + new Object[] { true }, + ImmutableMap.of("unknown", "unknown") + ) ) .hasMessageContaining("Argument named 'unknown' is invalid"); } public static class ArgCapturingFilter extends AbstractFilter { + public Map parsedArgs; @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..dde125123 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/AbstractSetFilterTest.java @@ -0,0 +1,139 @@ +package com.hubspot.jinjava.lib.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.features.BuiltInFeatures; +import com.hubspot.jinjava.features.FeatureConfig; +import com.hubspot.jinjava.features.FeatureStrategies; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.RenderResult; +import com.hubspot.jinjava.interpret.TemplateError; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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." + ); + } + + @Test + public void itDoesNotThrowWarningOnIntegerLongMismatch() { + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + + Set varSet = concreteSetFilter.objectToSet(new Long[] { 1L, 2L, 3L }); + Set argSet = concreteSetFilter.objectToSet(new Integer[] { 1, 2, 3 }); + concreteSetFilter.attachMismatchedTypesWarning(interpreter, varSet, argSet); + + assertThat(interpreter.getErrors()).isEmpty(); + } + + @Test + public void itConvertsIntegerToLongWhenFeatureActive() { + Jinjava jinjavaWithFeature = new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withFeatureConfig( + FeatureConfig + .newBuilder() + .add(BuiltInFeatures.INTEGER_SET_TO_LONG_CONVERSION, FeatureStrategies.ACTIVE) + .build() + ) + .build() + ); + + Map vars = new HashMap<>(); + vars.put("longList", new Long[] { 1L, 2L, 3L }); + vars.put("intList", new Integer[] { 2, 3, 4 }); + + String result = jinjavaWithFeature.render("{{ longList|intersect(intList) }}", vars); + + assertThat(result).isEqualTo("[2, 3]"); + } + + @Test + public void itDoesNotConvertWhenFeatureInactive() { + Map vars = new HashMap<>(); + vars.put("longList", new Long[] { 1L, 2L, 3L }); + vars.put("intList", new Integer[] { 2, 3, 4 }); + + RenderResult renderResult = jinjava.renderForResult( + "{{ longList|intersect(intList) }}", + vars + ); + + assertThat(renderResult.getOutput()).isEqualTo("[]"); + assertThat(renderResult.getErrors()).isEmpty(); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/AdvancedFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/AdvancedFilterTest.java index ef16c4cab..6988a94f6 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/AdvancedFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/AdvancedFilterTest.java @@ -28,7 +28,6 @@ public void testOnlyArgs() { public void testOnlyKwargs() { Object[] expectedArgs = new Object[] {}; Map expectedKwargs = new HashMap() { - { put("named10", "str"); put("named2", 3L); @@ -41,11 +40,11 @@ public void testOnlyKwargs() { .registerFilter(new MyMirrorFilter(expectedArgs, expectedKwargs)); assertThat( - jinjava.render( - "{{ 'test'|mirror(named2=3, named10='str', namedB=true) }}", - new HashMap<>() - ) + jinjava.render( + "{{ 'test'|mirror(named2=3, named10='str', namedB=true) }}", + new HashMap<>() ) + ) .isEqualTo("test"); } @@ -53,7 +52,6 @@ public void testOnlyKwargs() { public void itTestsNullKwargs() { Object[] expectedArgs = new Object[] {}; Map expectedKwargs = new HashMap() { - { put("named1", null); } @@ -71,7 +69,6 @@ public void itTestsNullKwargs() { public void testMixedArgsAndKwargs() { Object[] expectedArgs = new Object[] { 1L, 2L }; Map expectedKwargs = new HashMap() { - { put("named", "test"); } @@ -89,7 +86,6 @@ public void testMixedArgsAndKwargs() { public void testUnorderedArgsAndKwargs() { Object[] expectedArgs = new Object[] { "1", 2L }; Map expectedKwargs = new HashMap() { - { put("named", "test"); } @@ -100,8 +96,8 @@ public void testUnorderedArgsAndKwargs() { .registerFilter(new MyMirrorFilter(expectedArgs, expectedKwargs)); assertThat( - jinjava.render("{{ 'test'|mirror('1', named='test', 2) }}", new HashMap<>()) - ) + jinjava.render("{{ 'test'|mirror('1', named='test', 2) }}", new HashMap<>()) + ) .isEqualTo("test"); } @@ -109,7 +105,6 @@ public void testUnorderedArgsAndKwargs() { public void testRepeatedKwargs() { Object[] expectedArgs = new Object[] { true }; Map expectedKwargs = new HashMap() { - { put("named", "overwrite"); } @@ -120,15 +115,16 @@ public void testRepeatedKwargs() { .registerFilter(new MyMirrorFilter(expectedArgs, expectedKwargs)); assertThat( - jinjava.render( - "{{ 'test'|mirror(true, named='test', named='overwrite') }}", - new HashMap<>() - ) + jinjava.render( + "{{ 'test'|mirror(true, named='test', named='overwrite') }}", + new HashMap<>() ) + ) .isEqualTo("test"); } private static class MyMirrorFilter implements AdvancedFilter { + private Object[] expectedArgs; private Map expectedKwargs; 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"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/Base64FilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/Base64FilterTest.java index e9e5d915d..eb4563949 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/Base64FilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/Base64FilterTest.java @@ -20,41 +20,41 @@ public void itEncodesWithDefaultCharset() { @Test public void itEncodesWithUtf16Le() { assertThat( - jinjava.render( - "{{ '\uD801\uDC37'|b64encode(encoding='utf-16le') }}", - Collections.emptyMap() - ) + jinjava.render( + "{{ '\uD801\uDC37'|b64encode(encoding='utf-16le') }}", + Collections.emptyMap() ) + ) .isEqualTo("Adg33A=="); } @Test public void itDecodesWithUtf16Le() { assertThat( - jinjava.render( - "{{ 'Adg33A=='|b64decode(encoding='utf-16le') }}", - Collections.emptyMap() - ) + jinjava.render( + "{{ 'Adg33A=='|b64decode(encoding='utf-16le') }}", + Collections.emptyMap() ) + ) .isEqualTo("\uD801\uDC37"); } @Test public void itEncodesAndDecodesDefaultCharset() { assertThat( - jinjava.render("{{ 123456789|b64encode|b64decode }}", Collections.emptyMap()) - ) + jinjava.render("{{ 123456789|b64encode|b64decode }}", Collections.emptyMap()) + ) .isEqualTo("123456789"); } @Test public void itEncodesAndDecodesUtf16Le() { assertThat( - jinjava.render( - "{{ 123456789|b64encode(encoding='utf-16le')|b64decode(encoding='utf-16le') }}", - Collections.emptyMap() - ) + jinjava.render( + "{{ 123456789|b64encode(encoding='utf-16le')|b64decode(encoding='utf-16le') }}", + Collections.emptyMap() ) + ) .isEqualTo("123456789"); } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/BetweenTimesFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/BetweenTimesFilterTest.java index b2b84e785..710d13780 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/BetweenTimesFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/BetweenTimesFilterTest.java @@ -9,16 +9,10 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Map; -import org.junit.Before; import org.junit.Test; public class BetweenTimesFilterTest extends BaseJinjavaTest { - @Before - public void setup() { - jinjava.getGlobalContext().registerClasses(EscapeJsFilter.class); - } - @Test public void itGetsDurationBetweenTimes() { long timestamp = 1543354954000L; diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/BoolFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/BoolFilterTest.java index d3700982b..e34a2a97a 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/BoolFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/BoolFilterTest.java @@ -20,6 +20,7 @@ import org.junit.Test; public class BoolFilterTest extends BaseInterpretingTest { + BoolFilter filter; @Before diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/CloseHtmlFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/CloseHtmlFilterTest.java new file mode 100644 index 000000000..c733d9835 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/CloseHtmlFilterTest.java @@ -0,0 +1,36 @@ +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 CloseHtmlFilterTest extends BaseInterpretingTest { + + CloseHtmlFilter f; + + @Before + public void setup() { + f = new CloseHtmlFilter(); + } + + @Test + public void itClosesTags() { + String openTags = "

    Hello, world!"; + assertThat(f.filter(openTags, interpreter)).isEqualTo("

    Hello, world!

    "); + } + + @Test + public void itIgnoresClosedTags() { + String openTags = "

    Hello, world!

    "; + assertThat(f.filter(openTags, interpreter)).isEqualTo("

    Hello, world!

    "); + } + + @Test + public void itClosesMultipleTags() { + String openTags = "

    Hello, world!"; + assertThat(f.filter(openTags, interpreter)) + .isEqualTo("

    Hello, world!

    "); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/DAliasedDefaultFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/DAliasedDefaultFilterTest.java index 5e95309ce..3234d0111 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/DAliasedDefaultFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DAliasedDefaultFilterTest.java @@ -11,8 +11,8 @@ public class DAliasedDefaultFilterTest extends BaseJinjavaTest { @Test public void itSetsDefaultStringValues() { assertThat( - jinjava.render("{% set d=d |d(\"some random value\") %}{{ d }}", new HashMap<>()) - ) + jinjava.render("{% set d=d |d(\"some random value\") %}{{ d }}", new HashMap<>()) + ) .isEqualTo("some random value"); } } 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..5c84262c8 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DateTimeFormatFilterTest.java @@ -1,9 +1,19 @@ package com.hubspot.jinjava.lib.filter; +import static com.hubspot.jinjava.lib.filter.time.DateTimeFormatHelper.FIXED_DATE_TIME_FILTER_NULL_ARG; import static org.assertj.core.api.Assertions.assertThat; +import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.features.DateTimeFeatureActivationStrategy; +import com.hubspot.jinjava.features.FeatureConfig; import com.hubspot.jinjava.interpret.InvalidArgumentException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +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; @@ -14,6 +24,7 @@ import org.junit.Test; public class DateTimeFormatFilterTest extends BaseInterpretingTest { + DateTimeFormatFilter filter; ZonedDateTime d; @@ -26,29 +37,31 @@ public void setup() { } @Test - public void itUsesTodayIfNoDateProvided() throws Exception { + public void itUsesTodayIfNoDateProvided() { assertThat(filter.filter(null, interpreter)) .isEqualTo(StrftimeFormatter.format(ZonedDateTime.now(ZoneOffset.UTC))); + assertThat(interpreter.getErrors().get(0).getMessage()) + .contains("datetimeformat filter called with null datetime"); } @Test - public void itSupportsLongAsInput() throws Exception { + public void itSupportsLongAsInput() { assertThat(filter.filter(d, interpreter)).isEqualTo(StrftimeFormatter.format(d)); } @Test - public void itUsesDefaultFormatStringIfNoneSpecified() throws Exception { + public void itUsesDefaultFormatStringIfNoneSpecified() { assertThat(filter.filter(d, interpreter)).isEqualTo("14:22 / 06-11-2013"); } @Test - public void itUsesSpecifiedFormatString() throws Exception { + public void itUsesSpecifiedFormatString() { assertThat(filter.filter(d, interpreter, "%B %d, %Y, at %I:%M %p")) .isEqualTo("November 06, 2013, at 02:22 PM"); } @Test - public void itHandlesVarsAndLiterals() throws Exception { + public void itHandlesVarsAndLiterals() { interpreter.getContext().put("d", d); interpreter.getContext().put("foo", "%Y-%m"); @@ -60,26 +73,26 @@ public void itHandlesVarsAndLiterals() throws Exception { } @Test - public void itSupportsTimezones() throws Exception { + public void itSupportsTimezones() { assertThat(filter.filter(1539277785000L, interpreter, "%B %d, %Y, at %I:%M %p")) .isEqualTo("October 11, 2018, at 05:09 PM"); assertThat( - filter.filter( - 1539277785000L, - interpreter, - "%B %d, %Y, at %I:%M %p", - "America/New_York" - ) + filter.filter( + 1539277785000L, + interpreter, + "%B %d, %Y, at %I:%M %p", + "America/New_York" ) + ) .isEqualTo("October 11, 2018, at 01:09 PM"); assertThat( - filter.filter(1539277785000L, interpreter, "%B %d, %Y, at %I:%M %p", "UTC+8") - ) + filter.filter(1539277785000L, interpreter, "%B %d, %Y, at %I:%M %p", "UTC+8") + ) .isEqualTo("October 12, 2018, at 01:09 AM"); } @Test(expected = InvalidArgumentException.class) - public void itThrowsExceptionOnInvalidTimezone() throws Exception { + public void itThrowsExceptionOnInvalidTimezone() { filter.filter( 1539277785000L, interpreter, @@ -89,7 +102,7 @@ public void itThrowsExceptionOnInvalidTimezone() throws Exception { } @Test(expected = InvalidDateFormatException.class) - public void itThrowsExceptionOnInvalidDateformat() throws Exception { + public void itThrowsExceptionOnInvalidDateformat() { filter.filter(1539277785000L, interpreter, "Not a format"); } @@ -101,15 +114,40 @@ public void itConvertsDatetimesByLocales() { .isEqualTo("onsdag, 6 november"); } + @Test + public void itConvertsToLocaleSpecificDateTimeFormat() { + assertThat( + filter.filter( + 1539277785000L, + interpreter, + "%x %X - %c", + "America/New_York", + "en-US" + ) + ) + .isIn( + "10/11/18 1:09:45 PM - Oct 11, 2018, 1:09:45 PM", + "10/11/18 1:09:45 PM - Oct 11, 2018, 1:09:45 PM" + ); + assertThat( + filter.filter( + 1539277785000L, + interpreter, + "%x %X - %c", + "America/New_York", + "de-DE" + ) + ) + .isEqualTo("11.10.18 13:09:45 - 11.10.2018, 13:09:45"); + } + @Test public void itDefaultsToEnglishForBadLocaleValues() { interpreter.getContext().put("d", d); assertThat( - interpreter.renderFlat( - "{{ d|datetimeformat('%A, %e %B', 'UTC', 'not_a_locale') }}" - ) - ) + interpreter.renderFlat("{{ d|datetimeformat('%A, %e %B', 'UTC', 'not_a_locale') }}") + ) .isEqualTo(Functions.dateTimeFormat(d, "%A, %e %B", "UTC", "America/Los_Angeles")); } @@ -118,10 +156,55 @@ public void itDefaultsToUtcForNullTimezone() { interpreter.getContext().put("d", d); assertThat( - interpreter.renderFlat( - "{{ d|datetimeformat('%A, %e %B, %I:%M %p', null, 'sv') }}" - ) - ) + interpreter.renderFlat("{{ d|datetimeformat('%A, %e %B, %I:%M %p', null, 'sv') }}") + ) .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 '%é': unknown format code 'é'"); + } + + @Test + public void itUsesDeprecationDateIfNoDateProvided() { + ZonedDateTime now = ZonedDateTime.now(); + + Jinjava jinjava = new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withFeatureConfig( + FeatureConfig + .newBuilder() + .add( + FIXED_DATE_TIME_FILTER_NULL_ARG, + DateTimeFeatureActivationStrategy.of(now) + ) + .build() + ) + .build() + ); + + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + JinjavaInterpreter.pushCurrent(interpreter); + try { + assertThat(filter.filter(null, interpreter)) + .isEqualTo(StrftimeFormatter.format(now)); + assertThat(interpreter.getErrors().get(0).getMessage()) + .contains("datetimeformat filter called with null datetime"); + } finally { + JinjavaInterpreter.popCurrent(); + } + } } 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..0faa5d4bc 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/DefaultFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DefaultFilterTest.java @@ -21,62 +21,84 @@ public void setup() { @Test public void itSetsDefaultStringValues() { assertThat( - jinjava.render( - "{% set d=d | default(\"some random value\") %}{{ d }}", - new HashMap<>() - ) + jinjava.render( + "{% set d=d | default(\"some random value\") %}{{ d }}", + new HashMap<>() ) + ) .isEqualTo("some random value"); } @Test public void itSetsDefaultObjectValue() { assertThat( - jinjava.render( - "{% set d=d | default({\"key\": \"value\"}) %}Value = {{ d.key }}", - new HashMap<>() - ) + jinjava.render( + "{% set d=d | default({\"key\": \"value\"}) %}Value = {{ d.key }}", + new HashMap<>() ) + ) .isEqualTo("Value = value"); } @Test public void itChecksForType() { assertThat( - jinjava.render( - "{% set d=d | default({\"key\": \"value\"}) %}Type = {{ type(d.key) }}", - new HashMap<>() - ) + jinjava.render( + "{% set d=d | default({\"key\": \"value\"}) %}Type = {{ type(d.key) }}", + new HashMap<>() ) + ) .isEqualTo("Type = str"); assertThat( - jinjava.render( - "{% set d=d | default(\"some random value\") %}{{ type(d) }}", - new HashMap<>() - ) + jinjava.render( + "{% set d=d | default(\"some random value\") %}{{ type(d) }}", + new HashMap<>() ) + ) .isEqualTo("str"); } @Test public void itCorrectlyProcessesNamedParameters() { assertThat( - jinjava.render( - "{% set d=d | default(truthy=False, default_value={\"key\": \"value\"}) %}Type = {{ type(d.key) }}", - new HashMap<>() - ) + jinjava.render( + "{% set d=d | default(truthy=False, default_value={\"key\": \"value\"}) %}Type = {{ type(d.key) }}", + new HashMap<>() ) + ) .isEqualTo("Type = str"); } @Test public void itIgnoresBadTruthyValue() { assertThat( - jinjava.render( - "{% set d=d | default({\"key\": \"value\"}, \"Blah\") %}Type = {{ type(d.key) }}", - new HashMap<>() - ) + jinjava.render( + "{% set d=d | default({\"key\": \"value\"}, \"Blah\") %}Type = {{ type(d.key) }}", + new HashMap<>() ) + ) .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/filter/DictSortFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/DictSortFilterTest.java new file mode 100644 index 000000000..37c7ad299 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DictSortFilterTest.java @@ -0,0 +1,58 @@ +package com.hubspot.jinjava.lib.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseJinjavaTest; +import java.util.HashMap; +import java.util.Map; +import org.junit.BeforeClass; +import org.junit.Test; + +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); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/DifferenceFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/DifferenceFilterTest.java index 4a2ce8d0a..d5269d990 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/DifferenceFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DifferenceFilterTest.java @@ -17,12 +17,12 @@ public void setup() { @Test public void itComputesSetDifferences() { assertThat( - jinjava.render("{{ [1, 2, 3, 3, 4]|difference([1, 2, 5, 6]) }}", new HashMap<>()) - ) + jinjava.render("{{ [1, 2, 3, 3, 4]|difference([1, 2, 5, 6]) }}", new HashMap<>()) + ) .isEqualTo("[3, 4]"); assertThat( - jinjava.render("{{ ['do', 'ray']|difference(['ray', 'me']) }}", new HashMap<>()) - ) + jinjava.render("{{ ['do', 'ray']|difference(['ray', 'me']) }}", new HashMap<>()) + ) .isEqualTo("['do']"); } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/DivideFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/DivideFilterTest.java index 4f2bcf59c..6ba1793b8 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/DivideFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/DivideFilterTest.java @@ -14,44 +14,44 @@ public class DivideFilterTest extends BaseJinjavaTest { @Test public void itDivides() { assertThat( - jinjava.render( - "{{ numerator|divide(denominator) }}", - ImmutableMap.of("numerator", 10, "denominator", 2) - ) + jinjava.render( + "{{ numerator|divide(denominator) }}", + ImmutableMap.of("numerator", 10, "denominator", 2) ) + ) .isEqualTo("5"); assertThat( - jinjava.render( - "{{ numerator // denominator }}", - ImmutableMap.of("numerator", 10, "denominator", 2) - ) + jinjava.render( + "{{ numerator // denominator }}", + ImmutableMap.of("numerator", 10, "denominator", 2) ) + ) .isEqualTo("5"); assertThat( - jinjava.render( - "{{ numerator / denominator }}", - ImmutableMap.of("numerator", 10, "denominator", 2) - ) + jinjava.render( + "{{ numerator / denominator }}", + ImmutableMap.of("numerator", 10, "denominator", 2) ) + ) .isEqualTo("5.0"); } @Test public void itDividesIntegersWithNonIntegerResult() { assertThat( - jinjava.render( - "{{ numerator|divide(denominator) }} {{ numerator / denominator }}", - ImmutableMap.of("numerator", 9, "denominator", 10) - ) + jinjava.render( + "{{ numerator|divide(denominator) }} {{ numerator / denominator }}", + ImmutableMap.of("numerator", 9, "denominator", 10) ) + ) .isEqualTo("1 0.9"); } @Test public void itRendersWithMorePrecisionWithConfigOption() { Jinjava customJinjava = new Jinjava( - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withLegacyOverrides( LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() ) @@ -60,19 +60,19 @@ public void itRendersWithMorePrecisionWithConfigOption() { ); assertThat( - jinjava.render( - "{{ numerator|divide(denominator) }}", - ImmutableMap.of("numerator", 2, "denominator", 100) - ) + jinjava.render( + "{{ numerator|divide(denominator) }}", + ImmutableMap.of("numerator", 2, "denominator", 100) ) + ) .isEqualTo("0"); assertThat( - customJinjava.render( - "{{ numerator|divide(denominator) }}", - ImmutableMap.of("numerator", 2, "denominator", 100) - ) + customJinjava.render( + "{{ numerator|divide(denominator) }}", + ImmutableMap.of("numerator", 2, "denominator", 100) ) + ) .isEqualTo("0.02"); } } 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..8516c95d0 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeFilterTest.java @@ -2,12 +2,19 @@ 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.Ignore; import org.junit.Test; public class EscapeFilterTest extends BaseInterpretingTest { + EscapeFilter f; @Before @@ -27,12 +34,49 @@ public void testEscape() { @Test public void testSafeStringCanBeEscaped() { assertThat( - f - .filter(new SafeString("Previously marked as safe"), interpreter) - .toString() - ) + f.filter(new SafeString("Previously marked as safe"), interpreter).toString() + ) .isEqualTo("<a>Previously marked as safe<a/>"); assertThat(f.filter(new SafeString("Previously marked as safe"), interpreter)) .isInstanceOf(SafeString.class); } + + @Ignore + 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); + System.out.printf("New: %d Old:%d\n", newTime.toMillis(), oldTime.toMillis()); + 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) { + try { + return Resources.toString(Resources.getResource(name), StandardCharsets.UTF_8); + } catch (IOException e) { + 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/lib/filter/EscapeJinjavaFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilterTest.java index b5b8b1118..b5d6be5b6 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJinjavaFilterTest.java @@ -8,6 +8,7 @@ import org.junit.Test; public class EscapeJinjavaFilterTest extends BaseInterpretingTest { + EscapeJinjavaFilter f; @Before @@ -30,4 +31,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 %}'}" + ); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJsFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJsFilterTest.java index 1b53149f8..e10fd6283 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJsFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJsFilterTest.java @@ -44,19 +44,16 @@ public void testHandlesWhitespace() { @Test public void testHandlesDoubleQuotes() { assertThat( - jinjava.render( - "{{ 'Testing a \"quote for the week\"'|escapejs }}", - new HashMap<>() - ) - ) + jinjava.render("{{ 'Testing a \"quote for the week\"'|escapejs }}", new HashMap<>()) + ) .isEqualTo("Testing a \\\"quote for the week\\\""); } @Test public void testSafeStringCanBeEscaped() { assertThat( - jinjava.render("{{ 'Testing\nlineb\"reak\n'|safe|escapejs }}", new HashMap<>()) - ) + jinjava.render("{{ 'Testing\nlineb\"reak\n'|safe|escapejs }}", new HashMap<>()) + ) .isEqualTo("Testing\\nlineb\\\"reak\\n"); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJsonFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJsonFilterTest.java index deed2d99a..c4569eef9 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJsonFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/EscapeJsonFilterTest.java @@ -41,11 +41,11 @@ public void testHandlesWhitespace() { @Test public void testHandlesDoubleQuotes() { assertThat( - jinjava.render( - "{{ 'Testing a \"quote for the week\"'|escapejson }}", - new HashMap<>() - ) + jinjava.render( + "{{ 'Testing a \"quote for the week\"'|escapejson }}", + new HashMap<>() ) + ) .isEqualTo("Testing a \\\"quote for the week\\\""); } @@ -62,8 +62,8 @@ public void testHandleSingleQuote() { @Test public void testSafeStringCanBeEscaped() { assertThat( - jinjava.render("{{ 'Testing\nlineb\"reak\n'|safe|escapejs }}", new HashMap<>()) - ) + jinjava.render("{{ 'Testing\nlineb\"reak\n'|safe|escapejs }}", new HashMap<>()) + ) .isEqualTo("Testing\\nlineb\\\"reak\\n"); } } 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..58449a4b5 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FileSizeFormatFilterTest.java @@ -22,12 +22,16 @@ public void testFileSizeFormatFilter() { assertThat(jinjava.render("{{1000|filesizeformat}}", new HashMap())) .isEqualTo("1.0 KB"); assertThat( - jinjava.render("{{1024|filesizeformat(true)}}", new HashMap()) - ) + jinjava.render("{{1024|filesizeformat(true)}}", new HashMap()) + ) .isEqualTo("1.0 KiB"); assertThat( - jinjava.render("{{3531836|filesizeformat(true)}}", new HashMap()) - ) + jinjava.render("{{3531836|filesizeformat(true)}}", new HashMap()) + ) .isEqualTo("3.4 MiB"); + assertThat( + jinjava.render("{{1000000000|filesizeformat}}", new HashMap()) + ) + .isEqualTo("1.0 GB"); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/FirstFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/FirstFilterTest.java index b2dce50af..badd01a44 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/FirstFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FirstFilterTest.java @@ -7,6 +7,7 @@ import org.junit.Test; public class FirstFilterTest { + FirstFilter filter; @Before 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..7f6e759fd 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/FloatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FloatFilterTest.java @@ -18,22 +18,25 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; 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; import org.junit.Test; public class FloatFilterTest extends BaseInterpretingTest { + private static final Locale FRENCH_LOCALE = new Locale("fr", "FR"); - private static final JinjavaConfig FRENCH_LOCALE_CONFIG = new JinjavaConfig( - StandardCharsets.UTF_8, - FRENCH_LOCALE, - ZoneOffset.UTC, - 10 - ); + private static final JinjavaConfig FRENCH_LOCALE_CONFIG = BaseJinjavaTest + .newConfigBuilder() + .withCharset(StandardCharsets.UTF_8) + .withLocale(FRENCH_LOCALE) + .withTimeZone(ZoneOffset.UTC) + .build(); FloatFilter filter; @@ -114,6 +117,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/FormatFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/FormatFilterTest.java index c333d426f..c7e156ab4 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/FormatFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FormatFilterTest.java @@ -20,8 +20,8 @@ public static void beforeClass() { @Test public void testFormatFilter() { assertThat( - jinjava.render("{{ '%s - %s'|format(\"Hello?\", \"Foo!\") }}", new HashMap<>()) - ) + jinjava.render("{{ '%s - %s'|format(\"Hello?\", \"Foo!\") }}", new HashMap<>()) + ) .isEqualTo("Hello? - Foo!"); } @@ -33,8 +33,8 @@ public void testFormatNumber() { @Test public void itThrowsExceptionOnMissingFormatArgument() { - assertThatThrownBy( - () -> jinjava.render("{{ '%s %s'|format(10000) }}", new HashMap<>()) + assertThatThrownBy(() -> + jinjava.render("{{ '%s %s'|format(10000) }}", new HashMap<>()) ) .isInstanceOf(FatalTemplateErrorsException.class) .hasMessageContaining("Missing format argument"); @@ -49,8 +49,7 @@ public void itThrowsExceptionOnBadConversion() { @Test public void itThrowsExceptionOnFormat() { - assertThatThrownBy( - () -> jinjava.render("{{ '%0.0f'|format(1000) }}", new HashMap<>()) + assertThatThrownBy(() -> jinjava.render("{{ '%0.0f'|format(1000) }}", new HashMap<>()) ) .isInstanceOf(FatalTemplateErrorsException.class) .hasMessageContaining("'%0.0f' is missing a width"); diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java new file mode 100644 index 000000000..aff5df24b --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java @@ -0,0 +1,85 @@ +package com.hubspot.jinjava.lib.filter; + +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; + +public class FormatNumberFilterTest extends BaseJinjavaTest { + + @Before + public void setup() {} + + @Test + public void testFormatNumberFilter() { + 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( + String.format( + "1%s000", + DecimalFormatSymbols.getInstance(Locale.FRENCH).getGroupingSeparator() + ) + ); + assertThat( + jinjava.render("{{ 1000.333|format_number('fr') }}", new HashMap()) + ) + .isEqualTo( + String.format( + "1%s000,333", + DecimalFormatSymbols.getInstance(Locale.FRENCH).getGroupingSeparator() + ) + ); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('fr', 2) }}", + new HashMap() + ) + ) + .isEqualTo( + String.format( + "1%s000,33", + DecimalFormatSymbols.getInstance(Locale.FRENCH).getGroupingSeparator() + ) + ); + + 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"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/FromJsonFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/FromJsonFilterTest.java index ac7ff4f19..d8d337936 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/FromJsonFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FromJsonFilterTest.java @@ -10,6 +10,7 @@ import org.junit.Test; public class FromJsonFilterTest extends BaseInterpretingTest { + private FromJsonFilter filter; @Before diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/FromYamlFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/FromYamlFilterTest.java index dddb2d6ca..f7ff45bc5 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/FromYamlFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FromYamlFilterTest.java @@ -10,6 +10,7 @@ import org.junit.Test; public class FromYamlFilterTest extends BaseInterpretingTest { + private FromYamlFilter filter; @Before diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/GroupByFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/GroupByFilterTest.java index 524ea0a6e..6d65ebf4d 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/GroupByFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/GroupByFilterTest.java @@ -41,6 +41,7 @@ public void testGroupByAttr() throws Exception { } public static class Person { + private String gender; private String firstName; private String lastName; diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/IndentFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/IndentFilterTest.java index 24be2dc35..1c9bcac8d 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/IndentFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/IndentFilterTest.java @@ -9,6 +9,7 @@ import org.junit.Test; public class IndentFilterTest extends BaseJinjavaTest { + private final Map VARS = new HashMap<>(); @Before @@ -27,11 +28,11 @@ public void itDoesntIndentFirstlineByDefault() { @Test public void itIndentsFirstline() { assertThat( - jinjava.render( - "{% set d=multiLine | indent(indentfirst= True, width=1) %}{{ d }}", - VARS - ) + jinjava.render( + "{% set d=multiLine | indent(indentfirst= True, width=1) %}{{ d }}", + VARS ) + ) .isEqualTo(" 1\n" + " 2\n" + " 3"); } } 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..b12ad01e2 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/IntFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/IntFilterTest.java @@ -3,22 +3,25 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; 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; import org.junit.Test; public class IntFilterTest extends BaseInterpretingTest { + private static final Locale FRENCH_LOCALE = new Locale("fr", "FR"); - private static final JinjavaConfig FRENCH_LOCALE_CONFIG = new JinjavaConfig( - StandardCharsets.UTF_8, - FRENCH_LOCALE, - ZoneOffset.UTC, - 10 - ); + private static final JinjavaConfig FRENCH_LOCALE_CONFIG = BaseJinjavaTest + .newConfigBuilder() + .withCharset(StandardCharsets.UTF_8) + .withLocale(FRENCH_LOCALE) + .withTimeZone(ZoneOffset.UTC) + .build(); IntFilter filter; @@ -110,7 +113,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 @@ -124,6 +136,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] }}")) 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..a7cb02c5e 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/IntersectFilterTest.java @@ -17,12 +17,12 @@ public void setup() { @Test public void itComputesSetIntersections() { assertThat( - jinjava.render("{{ [1, 1, 2, 3]|intersect([1, 2, 5, 6]) }}", new HashMap<>()) - ) + jinjava.render("{{ [1, 1, 2, 3]|intersect([1, 2, 5, 6]) }}", new HashMap<>()) + ) .isEqualTo("[1, 2]"); assertThat( - jinjava.render("{{ ['do', 'ray']|intersect(['ray', 'me']) }}", new HashMap<>()) - ) + jinjava.render("{{ ['do', 'ray']|intersect(['ray', 'me']) }}", new HashMap<>()) + ) .isEqualTo("['ray']"); } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/IpAddrFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/IpAddrFilterTest.java index 6e1bd7a4a..639e113af 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/IpAddrFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/IpAddrFilterTest.java @@ -15,6 +15,7 @@ import org.junit.Test; public class IpAddrFilterTest extends BaseInterpretingTest { + private IpAddrFilter ipAddrFilter; private Ipv4Filter ipv4Filter; private Ipv6Filter ipv6Filter; @@ -46,8 +47,8 @@ public void itRejectsInvalidIpV4Address() { @Test public void itAcceptsValidIpV6Address() { assertThat( - ipAddrFilter.filter("1200:0000:AB00:1234:0000:2552:7777:1313", interpreter) - ) + ipAddrFilter.filter("1200:0000:AB00:1234:0000:2552:7777:1313", interpreter) + ) .isEqualTo(true); assertThat(ipAddrFilter.filter("21DA:D3:0:2F3B:2AA:FF:FE28:9C5A", interpreter)) .isEqualTo(true); @@ -59,8 +60,8 @@ public void itRejectsInvalidIpV6Address() { assertThat(ipAddrFilter.filter("1200::AB00:1234::2552:7777:1313", interpreter)) .isEqualTo(false); assertThat( - ipAddrFilter.filter("1200:0000:AB00:1234:O000:2552:7777:1313", interpreter) - ) + ipAddrFilter.filter("1200:0000:AB00:1234:O000:2552:7777:1313", interpreter) + ) .isEqualTo(false); assertThat(ipAddrFilter.filter("1200::AB00:1234::2552:7777:1313:1232", interpreter)) .isEqualTo(false); @@ -76,12 +77,12 @@ public void itReturnsIpv4AddressPrefix() { @Test public void itReturnsIpv6AddressPrefix() { assertThat( - ipAddrFilter.filter( - "1200:0000:AB00:1234:0000:2552:7777:1313/43", - interpreter, - "prefix" - ) + ipAddrFilter.filter( + "1200:0000:AB00:1234:0000:2552:7777:1313/43", + interpreter, + "prefix" ) + ) .isEqualTo(43); } @@ -100,12 +101,12 @@ public void itReturnsIpv4AddressNetMask() { @Test public void itReturnsIpv6AddressNetMask() { assertThat( - ipAddrFilter.filter( - "1200:0000:AB00:1234:0000:2552:7777:1313/43", - interpreter, - "netmask" - ) + ipAddrFilter.filter( + "1200:0000:AB00:1234:0000:2552:7777:1313/43", + interpreter, + "netmask" ) + ) .isEqualTo("ffff:ffff:ffe0::"); } @@ -118,12 +119,12 @@ public void itReturnsIpv4AddressBroadcast() { @Test public void itReturnsIpv6AddressBroadcast() { assertThat( - ipAddrFilter.filter( - "1200:0000:AB00:1234:0000:2552:7777:1313/43", - interpreter, - "broadcast" - ) + ipAddrFilter.filter( + "1200:0000:AB00:1234:0000:2552:7777:1313/43", + interpreter, + "broadcast" ) + ) .isEqualTo("1200:0:ab1f:ffff:ffff:ffff:ffff:ffff"); } @@ -138,20 +139,20 @@ public void itReturnsIpv4AddressAddress() { @Test public void itReturnsIpv6AddressAddress() { assertThat( - ipAddrFilter.filter( - "1200:0000:AB00:1234:0000:2552:7777:1313/43", - interpreter, - "address" - ) + ipAddrFilter.filter( + "1200:0000:AB00:1234:0000:2552:7777:1313/43", + interpreter, + "address" ) + ) .isEqualTo("1200:0000:AB00:1234:0000:2552:7777:1313"); assertThat( - ipAddrFilter.filter( - "1200:0000:AB00:1234:0000:2552:7777:1314", - interpreter, - "address" - ) + ipAddrFilter.filter( + "1200:0000:AB00:1234:0000:2552:7777:1314", + interpreter, + "address" ) + ) .isEqualTo("1200:0000:AB00:1234:0000:2552:7777:1314"); } @@ -164,12 +165,12 @@ public void itReturnsIpv4AddressNetwork() { @Test public void itReturnsIpv6AddressNetwork() { assertThat( - ipAddrFilter.filter( - "1200:0000:AB00:1234:0000:2552:7777:1313/43", - interpreter, - "network" - ) + ipAddrFilter.filter( + "1200:0000:AB00:1234:0000:2552:7777:1313/43", + interpreter, + "network" ) + ) .isEqualTo("1200:0:ab00::"); } @@ -182,27 +183,27 @@ public void itReturnsIpv4AddressGateway() { @Test public void itReturnsIpv6AddressGateway() { assertThat( - ipAddrFilter.filter( - "1200:0000:AB00:1234:0000:2552:7777:1313/43", - interpreter, - "gateway" - ) + ipAddrFilter.filter( + "1200:0000:AB00:1234:0000:2552:7777:1313/43", + interpreter, + "gateway" ) + ) .isEqualTo("1200:0:ab00::1"); } @Test public void itAddsErrorOnInvalidCidrAddress() { - assertThatThrownBy( - () -> ipAddrFilter.filter("192.168.0.1/200", interpreter, "broadcast") + assertThatThrownBy(() -> + ipAddrFilter.filter("192.168.0.1/200", interpreter, "broadcast") ) .hasMessageContaining("must be a valid CIDR address"); } @Test public void itAddsErrorOnInvalidFunctionName() { - assertThatThrownBy( - () -> ipAddrFilter.filter("192.168.0.1/20", interpreter, "notAFunction") + assertThatThrownBy(() -> + ipAddrFilter.filter("192.168.0.1/20", interpreter, "notAFunction") ) .hasMessageContaining("must be one of"); } @@ -402,19 +403,19 @@ public void itFiltersIpAddressesBroadcast() { @Test public void itWorksWithSafeString() throws Exception { assertThat( - ipAddrFilter.filter( - new SafeString("1200:0000:AB00:1234:0000:2552:7777:1313"), - interpreter - ) + ipAddrFilter.filter( + new SafeString("1200:0000:AB00:1234:0000:2552:7777:1313"), + interpreter ) + ) .isEqualTo(true); assertThat(ipAddrFilter.filter(new SafeString("255.182.100.abc"), interpreter)) .isEqualTo(false); assertThat(ipAddrFilter.filter(new SafeString(" 128.0.0.1 "), interpreter)) .isEqualTo(true); assertThat( - ipAddrFilter.filter(new SafeString("255.182.100.1/10"), interpreter, "netmask") - ) + ipAddrFilter.filter(new SafeString("255.182.100.1/10"), interpreter, "netmask") + ) .isEqualTo("255.192.0.0"); } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/JoinFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/JoinFilterTest.java index 57040ac00..3ef8f7db0 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/JoinFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/JoinFilterTest.java @@ -29,14 +29,15 @@ public void testJoinVals() { @Test public void testJoinAttrs() { assertThat( - jinjava.render("{{ users|join(', ', attribute='username') }}", new HashMap<>()) - ) + jinjava.render("{{ users|join(', ', attribute='username') }}", new HashMap<>()) + ) .isEqualTo("foo, bar"); } @Test public void itTruncatesStringToConfigLimit() { - jinjava = new Jinjava(JinjavaConfig.newBuilder().withMaxStringLength(5).build()); + jinjava = + new Jinjava(BaseJinjavaTest.newConfigBuilder().withMaxStringLength(5).build()); RenderResult result = jinjava.renderForResult( "{{ [1, 2, 3, 4, 5]|join('|') }}", @@ -49,6 +50,7 @@ public void itTruncatesStringToConfigLimit() { } public static class User { + private String username; public User(String username) { diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/LastFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/LastFilterTest.java index cace67a32..57600765c 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/LastFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/LastFilterTest.java @@ -8,6 +8,7 @@ import org.junit.Test; public class LastFilterTest { + LastFilter filter; @Before 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..c99ccca2d 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; @@ -27,6 +31,7 @@ import org.junit.Test; public class ListFilterTest { + ListFilter filter; @Before @@ -60,4 +65,110 @@ 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()); + } + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/LogFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/LogFilterTest.java index 53f4701f2..54625dc43 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/LogFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/LogFilterTest.java @@ -10,6 +10,7 @@ import org.junit.Test; public class LogFilterTest extends BaseInterpretingTest { + LogFilter filter; @Before diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/MapFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/MapFilterTest.java index 7a244b596..f4456aa1c 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/MapFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/MapFilterTest.java @@ -14,105 +14,99 @@ public class MapFilterTest extends BaseJinjavaTest { @Test public void mapAttr() { assertThat( - jinjava.render( - "{{ users|map(attribute='username')|join(', ') }}", - ImmutableMap.of( - "users", - (Object) Lists.newArrayList(new User("foo"), new User("bar")) - ) + jinjava.render( + "{{ users|map(attribute='username')|join(', ') }}", + ImmutableMap.of( + "users", + (Object) Lists.newArrayList(new User("foo"), new User("bar")) ) ) + ) .isEqualTo("foo, bar"); } @Test public void mapFilter() { assertThat( - jinjava.render( - "{{ titles|map('lower')|join(', ') }}", - ImmutableMap.of( - "titles", - (Object) Lists.newArrayList("Happy Day", "FOO", "bar") - ) - ) + jinjava.render( + "{{ titles|map('lower')|join(', ') }}", + ImmutableMap.of("titles", (Object) Lists.newArrayList("Happy Day", "FOO", "bar")) ) + ) .isEqualTo("happy day, foo, bar"); } @Test public void itPassesAdditionalArgumentsIntoFilter() { assertThat( - jinjava.render( - "{{ titles|map('truncate', 5, true, '')|join(', ') }}", - ImmutableMap.of( - "titles", - (Object) Lists.newArrayList("Happy Day", "FOOBAR", "barfoo") - ) + jinjava.render( + "{{ titles|map('truncate', 5, true, '')|join(', ') }}", + ImmutableMap.of( + "titles", + (Object) Lists.newArrayList("Happy Day", "FOOBAR", "barfoo") ) ) + ) .isEqualTo("Happy, FOOBA, barfo"); } @Test public void itUsesAttributeIfAttributeNameClashesWithFilter() { assertThat( - jinjava.render( - "{{ titles|map(attribute='date')|join(' ') }}", - ImmutableMap.of("titles", (Object) Lists.newArrayList(new TestClass(12345))) - ) + jinjava.render( + "{{ titles|map(attribute='date')|join(' ') }}", + ImmutableMap.of("titles", (Object) Lists.newArrayList(new TestClass(12345))) ) + ) .isEqualTo("12345"); } @Test public void itMapsFirstArgumentToFilterIfFilterExists() { - assertThatThrownBy( - () -> - jinjava.render( - "{{ titles|map('date')|join(' ') }}", - ImmutableMap.of("titles", (Object) Lists.newArrayList(new TestClass(12345))) - ) + assertThatThrownBy(() -> + jinjava.render( + "{{ titles|map('date')|join(' ') }}", + ImmutableMap.of("titles", (Object) Lists.newArrayList(new TestClass(12345))) + ) ) .hasMessageContaining("Input to function must be a date object"); } @Test public void itAddsErrorIfFirstArgumentIsNull() { - assertThatThrownBy( - () -> - jinjava.render( - "{{ titles|map(null)|join(' ') }}", - ImmutableMap.of("titles", (Object) Lists.newArrayList(new TestClass(12345))) - ) + assertThatThrownBy(() -> + jinjava.render( + "{{ titles|map(null)|join(' ') }}", + ImmutableMap.of("titles", (Object) Lists.newArrayList(new TestClass(12345))) + ) ) .hasMessageContaining("1st argument cannot be null"); } @Test public void itAddsErrorIfAttributeArgumentIsNull() { - assertThatThrownBy( - () -> - jinjava.render( - "{{ titles|map(attribute=null)|join(' ') }}", - ImmutableMap.of("titles", (Object) Lists.newArrayList(new TestClass(12345))) - ) + assertThatThrownBy(() -> + jinjava.render( + "{{ titles|map(attribute=null)|join(' ') }}", + ImmutableMap.of("titles", (Object) Lists.newArrayList(new TestClass(12345))) + ) ) .hasMessageContaining("'attribute' argument cannot be null"); } @Test public void itAddsErrorIfNoArgumentsAreProvided() { - assertThatThrownBy( - () -> - jinjava.render( - "{{ titles|map()|join(' ') }}", - ImmutableMap.of("titles", (Object) Lists.newArrayList(new TestClass(12345))) - ) + assertThatThrownBy(() -> + jinjava.render( + "{{ titles|map()|join(' ') }}", + ImmutableMap.of("titles", (Object) Lists.newArrayList(new TestClass(12345))) + ) ) .hasMessageContaining("requires 1 argument"); } public class TestClass { + private final long date; public TestClass(long date) { diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/MinusTimeFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/MinusTimeFilterTest.java index 4555d6e4c..66b614409 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/MinusTimeFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/MinusTimeFilterTest.java @@ -10,16 +10,10 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Map; -import org.junit.Before; import org.junit.Test; public class MinusTimeFilterTest extends BaseJinjavaTest { - @Before - public void setup() { - jinjava.getGlobalContext().registerClasses(EscapeJsFilter.class); - } - @Test public void itSubtractsTime() { long timestamp = 1543352736000L; diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/PlusTimeFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/PlusTimeFilterTest.java index 7990a0c5a..b5e92b801 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/PlusTimeFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/PlusTimeFilterTest.java @@ -10,16 +10,10 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Map; -import org.junit.Before; import org.junit.Test; public class PlusTimeFilterTest extends BaseJinjavaTest { - @Before - public void setup() { - jinjava.getGlobalContext().registerClasses(EscapeJsFilter.class); - } - @Test public void itAddsTime() { long timestamp = 1543352736000L; diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/PrettyPrintFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/PrettyPrintFilterTest.java index 2a4d028b5..36b56eede 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/PrettyPrintFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/PrettyPrintFilterTest.java @@ -2,8 +2,14 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import com.hubspot.jinjava.BaseJinjavaTest; +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.date.PyishDate; import java.time.ZoneOffset; import java.time.ZonedDateTime; @@ -11,17 +17,22 @@ import org.junit.Test; public class PrettyPrintFilterTest { + + JinjavaInterpreter i; PrettyPrintFilter f; @Before public void setup() { + JinjavaConfig config = BaseJinjavaTest.newConfigBuilder().build(); + Jinjava jinjava = new Jinjava(config); + Context context = jinjava.getGlobalContext(); + i = new JinjavaInterpreter(jinjava, context, config); f = new PrettyPrintFilter(); } @Test public void ppString() { - assertThat(f.filter("foobar", null)) - .isEqualTo("{% raw %}(String: foobar){% endraw %}"); + assertThat(f.filter("foobar", i)).isEqualTo("{% raw %}(String: foobar){% endraw %}"); } @Test @@ -32,18 +43,18 @@ public void ppInt() { @Test public void ppPyDate() { assertThat( - f.filter( - new PyishDate(ZonedDateTime.of(2014, 8, 4, 0, 0, 0, 0, ZoneOffset.UTC)), - null - ) + f.filter( + new PyishDate(ZonedDateTime.of(2014, 8, 4, 0, 0, 0, 0, ZoneOffset.UTC)), + null ) + ) .isEqualTo("{% raw %}(PyishDate: 2014-08-04 00:00:00){% endraw %}"); } @Test public void ppMap() { - assertThat(f.filter(ImmutableMap.of("a", "foo", "b", "bar"), null)) - .isEqualTo("{% raw %}(RegularImmutableMap: {a=foo, b=bar}){% endraw %}"); + assertThat(f.filter(ImmutableMap.of("b", "foo", "a", "bar"), null)) + .isEqualTo("{% raw %}(RegularImmutableMap: {a=bar, b=foo}){% endraw %}"); } @Test @@ -54,10 +65,27 @@ public void ppList() { @Test public void ppObject() { - assertThat(f.filter(new MyClass(), null)) - .isEqualTo("{% raw %}(MyClass: {bar=123, foo=foofoo}){% endraw %}"); + MyClass myClass = new MyClass(); + assertThat(f.filter(myClass, i)) + .isEqualTo( + String.format( + "{%% raw %%}(MyClass: {\n" + + " "foo" : "%s",\n" + + " "bar" : %d,\n" + + " "nested_class" : {\n" + + " "foo_field" : "%s",\n" + + " "bar_field" : %d\n" + + " }\n" + + "}){%% endraw %%}", + myClass.getFoo(), + myClass.getBar(), + myClass.getNestedClass().getFooField(), + myClass.getNestedClass().getBarField() + ) + ); } + @JsonPropertyOrder({ "foo", "bar", "nestedClass" }) public static class MyClass { public String getFoo() { @@ -67,5 +95,21 @@ public String getFoo() { public int getBar() { return 123; } + + public MyNestedClass getNestedClass() { + return new MyNestedClass(); + } + } + + @JsonPropertyOrder({ "fooField", "barField" }) + public static class MyNestedClass { + + public String getFooField() { + return "foofieldfoofield"; + } + + public int getBarField() { + return 123; + } } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/RandomFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/RandomFilterTest.java index 6be389632..1a579de12 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/RandomFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/RandomFilterTest.java @@ -16,6 +16,7 @@ import org.junit.Test; public class RandomFilterTest { + RandomFilter filter = new RandomFilter(); JinjavaInterpreter interpreter = mock(JinjavaInterpreter.class); diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/RegexReplaceFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/RegexReplaceFilterTest.java index 5c1e98cbc..202aa4883 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/RegexReplaceFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/RegexReplaceFilterTest.java @@ -4,12 +4,16 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.interpret.InvalidArgumentException; +import com.hubspot.jinjava.interpret.InvalidInputException; import com.hubspot.jinjava.objects.SafeString; import org.junit.Before; import org.junit.Test; public class RegexReplaceFilterTest extends BaseInterpretingTest { + RegexReplaceFilter filter; @Before @@ -25,8 +29,8 @@ public void expects2Args() { @Test public void expectsNotNullArgs() { - assertThatThrownBy( - () -> filter.filter("foo", interpreter, new String[] { null, null }) + assertThatThrownBy(() -> + filter.filter("foo", interpreter, new String[] { null, null }) ) .hasMessageContaining("both a valid regex"); } @@ -49,10 +53,31 @@ public void isThrowsExceptionOnInvalidRegex() { @Test public void itMatchesRegexAndReplacesStringForSafeString() { assertThat( - filter - .filter(new SafeString("It costs $300"), interpreter, "[^a-zA-Z]", "") - .toString() - ) + filter + .filter(new SafeString("It costs $300"), interpreter, "[^a-zA-Z]", "") + .toString() + ) .isEqualTo("Itcosts"); } + + @Test + public void itLimitsLongInput() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 101; i++) { + sb.append('a'); + } + assertThatThrownBy(() -> + filter.filter( + sb.toString(), + new Jinjava(BaseJinjavaTest.newConfigBuilder().withMaxStringLength(10).build()) + .newInterpreter(), + "O", + "0" + ) + ) + .isInstanceOf(InvalidInputException.class) + .hasMessageContaining( + "Invalid input for 'regex_replace': input with length '101' exceeds maximum allowed length of '10'" + ); + } } 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..9d7ed44e7 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; @@ -28,56 +29,54 @@ public void setup() { @Test public void rejectAttrWithNoExp() { assertThat( - jinjava.render( - "{{ users|rejectattr('is_active') }}", - new HashMap() - ) - ) + jinjava.render("{{ users|rejectattr('is_active') }}", new HashMap()) + ) .isEqualTo("[0, 2]"); } @Test public void rejectAttrWithExp() { assertThat( - jinjava.render( - "{{ users|rejectattr('email', 'none') }}", - new HashMap() - ) + jinjava.render( + "{{ users|rejectattr('email', 'none') }}", + new HashMap() ) + ) .isEqualTo("[0, 1]"); } @Test public void rejectAttrWithIsEqualToExp() { assertThat( - jinjava.render( - "{{ users|rejectattr('email', 'equalto', 'bar@bar.com') }}", - new HashMap() - ) + jinjava.render( + "{{ users|rejectattr('email', 'equalto', 'bar@bar.com') }}", + new HashMap() ) + ) .isEqualTo("[0, 2]"); } @Test public void rejectAttrWithNestedProperty() { assertThat( - jinjava.render( - "{{ users|rejectattr('option.id', 'equalto', 1) }}", - new HashMap() - ) + jinjava.render( + "{{ users|rejectattr('option.id', 'equalto', 1) }}", + new HashMap() ) + ) .isEqualTo("[0, 2]"); assertThat( - jinjava.render( - "{{ users|rejectattr('option.name', 'equalto', 'option0') }}", - new HashMap() - ) + jinjava.render( + "{{ users|rejectattr('option.name', 'equalto', 'option0') }}", + new HashMap() ) + ) .isEqualTo("[1, 2]"); } public static class User implements PyishSerializable { + private int num; private boolean isActive; private String email; @@ -112,12 +111,15 @@ public String toString() { } @Override - public String toPyishString() { - return toString(); + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(toString()); } } public static class Option { + private long id; private String name; diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/RenderFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/RenderFilterTest.java index 04e238b7a..13b0bbe82 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/RenderFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/RenderFilterTest.java @@ -7,6 +7,7 @@ import org.junit.Test; public class RenderFilterTest extends BaseInterpretingTest { + private RenderFilter filter; @Before @@ -20,4 +21,66 @@ public void itRendersObject() { assertThat(filter.filter(stringToRender, interpreter)).isEqualTo("world"); } + + @Test + public void itRendersObjectWithinLimit() { + String stringToRender = "{% if null %}Hello{% else %}world{% endif %}"; + + assertThat(filter.filter(stringToRender, interpreter, "5")).isEqualTo("world"); + } + + @Test + public void itDoesNotRenderObjectOverLimit() { + String stringToRender = "{% if null %}Hello{% else %}world{% endif %}"; + + assertThat(filter.filter(stringToRender, interpreter, "4")).isEqualTo(""); + } + + @Test + public void itRendersPartialObjectOverLimit() { + String stringToRender = "Hello{% if null %}Hello{% else %}world{% endif %}"; + + assertThat(filter.filter(stringToRender, interpreter, "7")).isEqualTo("Hello"); + } + + @Test + public void itCountsHtmlTags() { + String stringToRender = "

    Hello

    {% if null %}Hello{% else %}world{% endif %}"; + + assertThat(filter.filter(stringToRender, interpreter, "15")) + .isEqualTo("

    Hello

    "); + } + + @Test + public void itDoesNotAlwaysCompleteHtmlTags() { + String stringToRender = + "

    Hello, {% if null %}world{% else %}world!{% endif %}

    "; + + assertThat(filter.filter(stringToRender, interpreter, "17")) + .isEqualTo("

    Hello, world!"); + } + + @Test + public void itDoesNotProcessExtendsRoots() { + String stringToRender = + "{% extends 'filter/render/base.jinja' -%}\n" + + "{% block body %}\n" + + "I am the extension body\n" + + "{% endblock %}" + + "You should never see this text in the output!\n" + + "{%- set foo = '{{ 1 + 1}}'|render -%}\n" + + "{% block footer %}\n" + + "I am the extension footer\n" + + "{% endblock %}"; + + assertThat(interpreter.render(stringToRender)) + .isEqualTo( + "Body is: \n" + + "I am the extension body\n" + + "\n" + + "Footer is: \n" + + "I am the extension footer\n" + + "\n" + ); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/ReplaceFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/ReplaceFilterTest.java index 022a4344e..3c0e93de4 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/ReplaceFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/ReplaceFilterTest.java @@ -1,14 +1,19 @@ package com.hubspot.jinjava.lib.filter; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.interpret.InterpretException; +import com.hubspot.jinjava.interpret.InvalidInputException; import com.hubspot.jinjava.objects.SafeString; import org.junit.Before; import org.junit.Test; public class ReplaceFilterTest extends BaseInterpretingTest { + ReplaceFilter filter; @Before @@ -40,10 +45,10 @@ public void replaceWithCount() { @Test public void replaceSafeStringWithCount() { assertThat( - filter - .filter(new SafeString("aaaaargh"), interpreter, "a", "d'oh, ", "2") - .toString() - ) + filter + .filter(new SafeString("aaaaargh"), interpreter, "a", "d'oh, ", "2") + .toString() + ) .isEqualTo("d'oh, d'oh, aaargh"); } @@ -52,4 +57,25 @@ public void replaceBoolean() { assertThat(filter.filter(true, interpreter, "true", "TRUEEE").toString()) .isEqualTo("TRUEEE"); } + + @Test + public void itLimitsLongInput() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 101; i++) { + sb.append('a'); + } + assertThatThrownBy(() -> + filter.filter( + sb.toString(), + new Jinjava(BaseJinjavaTest.newConfigBuilder().withMaxStringLength(10).build()) + .newInterpreter(), + "O", + "0" + ) + ) + .isInstanceOf(InvalidInputException.class) + .hasMessageContaining( + "Invalid input for 'replace': input with length '101' exceeds maximum allowed length of '10'" + ); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/ReverseFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/ReverseFilterTest.java new file mode 100644 index 000000000..5d60a5d90 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/ReverseFilterTest.java @@ -0,0 +1,51 @@ +package com.hubspot.jinjava.lib.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.LegacyOverrides; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; + +public class ReverseFilterTest extends BaseJinjavaTest { + + @Test + public void itReversesPrimitiveIntArray() { + Map context = new HashMap<>(); + context.put("arr", new int[] { 1, 2, 3 }); + assertThat( + jinjava.render("{% for item in arr|reverse %}{{ item }}{% endfor %}", context) + ) + .isEqualTo("321"); + } + + @Test + public void itReversesObjectArray() { + Map context = new HashMap<>(); + context.put("arr", new String[] { "a", "b", "c" }); + assertThat( + jinjava.render("{% for item in arr|reverse %}{{ item }}{% endfor %}", context) + ) + .isEqualTo("cba"); + } + + @Test + public void itAllowsIndexingWhenLegacyOverrideIsDisabled() { + Jinjava legacyJinjava = new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withIteratorOnlyReverseFilter(false) + .build() + ) + .build() + ); + Map context = new HashMap<>(); + context.put("arr", new String[] { "a", "b", "c" }); + assertThat(legacyJinjava.render("{{ (arr|reverse)[0] }}", context)).isEqualTo("c"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/RootFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/RootFilterTest.java index 64fd310b5..ee2c04a72 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/RootFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/RootFilterTest.java @@ -10,6 +10,7 @@ import org.junit.Test; public class RootFilterTest extends BaseInterpretingTest { + RootFilter filter; @Before diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/SafeFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/SafeFilterTest.java index c2787fa05..4ddda5775 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/SafeFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/SafeFilterTest.java @@ -10,6 +10,7 @@ import org.junit.Test; public class SafeFilterTest extends BaseInterpretingTest { + private static final String HTML = "Link"; private static final List TEST_NUMBERS = ImmutableList.of(43, 1, 24); private static final List TEST_STRINGS = ImmutableList.of( @@ -88,8 +89,8 @@ public void itWorksForAllRelevantFilters() throws Exception { for (String testString : TEST_STRINGS) { interpreter.getContext().put("string_under_test", testString); assertThat( - interpreter.renderFlat("{{ string_under_test|safe|" + testFilter + "|safe }}") - ) + interpreter.renderFlat("{{ string_under_test|safe|" + testFilter + "|safe }}") + ) .as( "Testing behaviour of filter with and without safe filter: " + testFilter + @@ -105,8 +106,8 @@ public void itWorksForAllRelevantFilters() throws Exception { for (Integer testInt : TEST_NUMBERS) { interpreter.getContext().put("string_under_test", testInt); assertThat( - interpreter.renderFlat("{{ string_under_test|safe|" + testFilter + " }}") - ) + interpreter.renderFlat("{{ string_under_test|safe|" + testFilter + " }}") + ) .as( "Testing behaviour of filter with and without safe filter: " + testFilter + 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..b3b845f3e 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/SelectAttrFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/SelectAttrFilterTest.java @@ -3,9 +3,11 @@ import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.collect.ImmutableMap; 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; @@ -24,83 +26,136 @@ public void setup() { new User(2, false, null, new Option(2, "option2")) ) ); + jinjava + .getGlobalContext() + .put( + "numbers", + Lists.newArrayList( + ImmutableMap.of("number", 1), + ImmutableMap.of("number", 2), + ImmutableMap.of("number", 3), + ImmutableMap.of("number", 4) + ) + ); } @Test public void selectAttrWithNoExp() { assertThat( - jinjava.render( - "{{ users|selectattr('is_active') }}", - new HashMap() - ) - ) + jinjava.render("{{ users|selectattr('is_active') }}", new HashMap()) + ) .isEqualTo("[1]"); } @Test public void selectAttrWithExp() { assertThat( - jinjava.render( - "{{ users|selectattr('email', 'none') }}", - new HashMap() - ) + jinjava.render( + "{{ users|selectattr('email', 'none') }}", + new HashMap() ) + ) .isEqualTo("[2]"); } @Test public void selectAttrWithSymbolicExp() { assertThat( - jinjava.render( - "{{ users|selectattr('isActive', '==', 'true') }}", - new HashMap() - ) + jinjava.render( + "{{ users|selectattr('isActive', '==', 'true') }}", + new HashMap() ) + ) .isEqualTo("[1]"); } @Test public void selectAttrWithIsEqualToExp() { assertThat( - jinjava.render( - "{{ users|selectattr('email', 'equalto', 'bar@bar.com') }}", - new HashMap() - ) + jinjava.render( + "{{ users|selectattr('email', 'equalto', 'bar@bar.com') }}", + new HashMap() ) + ) .isEqualTo("[1]"); } @Test public void selectAttrWithNumericIsEqualToExp() { assertThat( - jinjava.render( - "{{ users|selectattr('num', 'equalto', 1) }}", - new HashMap() - ) + jinjava.render( + "{{ users|selectattr('num', 'equalto', 1) }}", + new HashMap() ) + ) .isEqualTo("[1]"); } @Test public void selectAttrWithNestedProperty() { assertThat( - jinjava.render( - "{{ users|selectattr('option.id', 'equalto', 1) }}", - new HashMap() - ) + jinjava.render( + "{{ users|selectattr('option.id', 'equalto', 1) }}", + new HashMap() ) + ) .isEqualTo("[1]"); assertThat( - jinjava.render( - "{{ users|selectattr('option.name', 'equalto', 'option2') }}", - new HashMap() - ) + jinjava.render( + "{{ users|selectattr('option.name', 'equalto', 'option2') }}", + new HashMap() ) + ) .isEqualTo("[2]"); } + @Test + public void selectAttrWithSymbolicLtExp() { + assertThat( + jinjava.render( + "{{ numbers|selectattr('number', '<', '3')|map('number') }}", + new HashMap() + ) + ) + .isEqualTo("[1, 2]"); + } + + @Test + public void selectAttrWithSymbolicLeExp() { + assertThat( + jinjava.render( + "{{ numbers|selectattr('number', '<=', '3')|map('number') }}", + new HashMap() + ) + ) + .isEqualTo("[1, 2, 3]"); + } + + @Test + public void selectAttrWithSymbolicGtExp() { + assertThat( + jinjava.render( + "{{ numbers|selectattr('number', '>', '3')|map('number') }}", + new HashMap() + ) + ) + .isEqualTo("[4]"); + } + + @Test + public void selectAttrWithSymbolicGeExp() { + assertThat( + jinjava.render( + "{{ numbers|selectattr('number', '>=', '3')|map('number') }}", + new HashMap() + ) + ) + .isEqualTo("[3, 4]"); + } + public static class User implements PyishSerializable { + private long num; private boolean isActive; private String email; @@ -135,12 +190,15 @@ public String toString() { } @Override - public String toPyishString() { - return toString(); + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(toString()); } } public static class Option implements PyishSerializable { + private long id; private String name; @@ -164,8 +222,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/ShuffleFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/ShuffleFilterTest.java index 31f111ea2..bd1646d49 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/ShuffleFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/ShuffleFilterTest.java @@ -14,6 +14,7 @@ import org.junit.Test; public class ShuffleFilterTest { + ShuffleFilter filter = new ShuffleFilter(); JinjavaInterpreter interpreter = mock(JinjavaInterpreter.class); diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/SliceFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/SliceFilterTest.java index 0f59385f9..571a7b0bd 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/SliceFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/SliceFilterTest.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.lib.filter; +import static com.hubspot.jinjava.lib.filter.SliceFilter.MAX_SLICES; import static org.assertj.core.api.Assertions.assertThat; import com.google.common.collect.ImmutableMap; @@ -8,6 +9,9 @@ import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.interpret.RenderResult; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.junit.Test; @@ -35,6 +39,20 @@ public void itSlicesLists() throws Exception { assertThat(dom.select(".columwrapper .column-3 li")).hasSize(1); } + @Test + public void itSlicesToTheMaxLimit() throws Exception { + String result = jinjava.render( + Resources.toString( + Resources.getResource("filter/slice-filter-big.jinja"), + StandardCharsets.UTF_8 + ), + ImmutableMap.of("items", Lists.newArrayList("a", "b", "c", "d", "e")) + ); + + assertThat(result).isNotEmpty(); + assertThat(result.split("\n")).hasSize(MAX_SLICES + 2); // 1 for each slice, 1 for the newline + } + @Test public void itSlicesListWithReplacement() throws Exception { String result = jinjava.render( 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..f58a979c5 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/SortFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/SortFilterTest.java @@ -5,7 +5,7 @@ import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.interpret.RenderResult; import com.hubspot.jinjava.interpret.TemplateError.ErrorType; -import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import com.hubspot.jinjava.testobjects.SortFilterTestObjects; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -32,13 +32,15 @@ public void sortStringsCaseSensitive() { public void sortWithNamedAttributes() throws Exception { // even if named attributes were never supported for this filter, ensure parameters are passed in order and it works assertThat( - render( - "(reverse=false, case_sensitive=false, attribute='foo.date')", - new MyBar(new MyFoo(new Date(250L))), - new MyBar(new MyFoo(new Date(0L))), - new MyBar(new MyFoo(new Date(100000000L))) + render( + "(reverse=false, case_sensitive=false, attribute='foo.date')", + new SortFilterTestObjects.MyBar(new SortFilterTestObjects.MyFoo(new Date(250L))), + new SortFilterTestObjects.MyBar(new SortFilterTestObjects.MyFoo(new Date(0L))), + new SortFilterTestObjects.MyBar( + new SortFilterTestObjects.MyFoo(new Date(100000000L)) ) ) + ) .isEqualTo("0250100000000"); } @@ -50,26 +52,28 @@ public void sortStringsCaseInsensitive() { @Test public void sortWithAttr() { assertThat( - render( - "(false, false, 'date')", - new MyFoo(new Date(250L)), - new MyFoo(new Date(0L)), - new MyFoo(new Date(100000000L)) - ) + render( + "(false, false, 'date')", + new SortFilterTestObjects.MyFoo(new Date(250L)), + new SortFilterTestObjects.MyFoo(new Date(0L)), + new SortFilterTestObjects.MyFoo(new Date(100000000L)) ) + ) .isEqualTo("0250100000000"); } @Test public void sortWithNestedAttr() { assertThat( - render( - "(false, false, 'foo.date')", - new MyBar(new MyFoo(new Date(250L))), - new MyBar(new MyFoo(new Date(0L))), - new MyBar(new MyFoo(new Date(100000000L))) + render( + "(false, false, 'foo.date')", + new SortFilterTestObjects.MyBar(new SortFilterTestObjects.MyFoo(new Date(250L))), + new SortFilterTestObjects.MyBar(new SortFilterTestObjects.MyFoo(new Date(0L))), + new SortFilterTestObjects.MyBar( + new SortFilterTestObjects.MyFoo(new Date(100000000L)) ) ) + ) .isEqualTo("0250100000000"); } @@ -77,9 +81,9 @@ public void sortWithNestedAttr() { public void itThrowsInvalidArgumentExceptionOnNullAttribute() { RenderResult result = renderForResult( "(false, false, null)", - new MyFoo(new Date(250L)), - new MyFoo(new Date(0L)), - new MyFoo(new Date(100000000L)) + new SortFilterTestObjects.MyFoo(new Date(250L)), + new SortFilterTestObjects.MyFoo(new Date(0L)), + new SortFilterTestObjects.MyFoo(new Date(100000000L)) ); assertThat(result.getOutput()).isEmpty(); assertThat(result.getErrors()).hasSize(1); @@ -91,9 +95,9 @@ public void itThrowsInvalidArgumentExceptionOnNullAttribute() { public void itThrowsInvalidArgumentWhenObjectAttributeIsNull() { RenderResult result = renderForResult( "(false, false, 'doesNotResolve')", - new MyFoo(new Date(250L)), - new MyFoo(new Date(0L)), - new MyFoo(new Date(100000000L)) + new SortFilterTestObjects.MyFoo(new Date(250L)), + new SortFilterTestObjects.MyFoo(new Date(0L)), + new SortFilterTestObjects.MyFoo(new Date(100000000L)) ); assertThat(result.getOutput()).isEmpty(); assertThat(result.getErrors()).hasSize(2); @@ -106,10 +110,10 @@ public void itThrowsInvalidArgumentWhenObjectAttributeIsNull() { public void itThrowsInvalidInputWhenListContainsNull() { RenderResult result = renderForResult( "(false, false)", - new MyFoo(new Date(250L)), - new MyFoo(new Date(0L)), + new SortFilterTestObjects.MyFoo(new Date(250L)), + new SortFilterTestObjects.MyFoo(new Date(0L)), null, - new MyFoo(new Date(100000000L)) + new SortFilterTestObjects.MyFoo(new Date(100000000L)) ); assertThat(result.getOutput()).isEmpty(); assertThat(result.getErrors()).hasSize(1); @@ -135,48 +139,4 @@ RenderResult renderForResult(String sortExtra, Object... items) { context ); } - - public static class MyFoo implements PyishSerializable { - private Date date; - - MyFoo(Date date) { - this.date = date; - } - - public Date getDate() { - return date; - } - - @Override - public String toString() { - return "" + date.getTime(); - } - - @Override - public String toPyishString() { - return toString(); - } - } - - public static class MyBar implements PyishSerializable { - private MyFoo foo; - - MyBar(MyFoo foo) { - this.foo = foo; - } - - public MyFoo getFoo() { - return foo; - } - - @Override - public String toString() { - return foo.toString(); - } - - @Override - public String toPyishString() { - return toString(); - } - } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/SplitFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/SplitFilterTest.java index 4ccf06dd3..b1c2bc424 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/SplitFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/SplitFilterTest.java @@ -9,6 +9,7 @@ @SuppressWarnings("unchecked") public class SplitFilterTest extends BaseInterpretingTest { + SplitFilter filter; @Before diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/StringToTimeFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/StringToTimeFilterTest.java index fd01ab4bd..483a9f296 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/StringToTimeFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/StringToTimeFilterTest.java @@ -34,8 +34,8 @@ public void itErrorsOnNonStringInput() { Map vars = ImmutableMap.of("datetime", datetime, "format", format); assertThat( - jinjava.renderForResult("{{ datetime|strtotime(format) }}", vars).getErrors() - ) + jinjava.renderForResult("{{ datetime|strtotime(format) }}", vars).getErrors() + ) .hasSize(1); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/StripTagsFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/StripTagsFilterTest.java index b28f58cd2..52084ce2b 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/StripTagsFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/StripTagsFilterTest.java @@ -2,37 +2,45 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.eager.DeferredToken; -import com.hubspot.jinjava.tree.parse.DefaultTokenScannerSymbols; import com.hubspot.jinjava.tree.parse.ExpressionToken; import java.util.Collections; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Answers; import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.internal.stubbing.answers.ReturnsArgumentAt; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class StripTagsFilterTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - JinjavaInterpreter interpreter; + + private JinjavaInterpreter interpreter; @InjectMocks - StripTagsFilter filter; + private StripTagsFilter filter; @Before public void setup() { - when(interpreter.getContext().getDeferredTokens()).thenReturn(Collections.emptySet()); - when(interpreter.renderFlat(anyString())).thenAnswer(new ReturnsArgumentAt(0)); + JinjavaConfig config = BaseJinjavaTest.newConfigBuilder().build(); + Jinjava jinjava = new Jinjava(config); + this.interpreter = new JinjavaInterpreter(jinjava.newInterpreter()); + JinjavaInterpreter.pushCurrent(interpreter); + } + + @After + public void teardown() { + JinjavaInterpreter.popCurrent(); } @Test @@ -96,27 +104,41 @@ public void itAddsWhitespaceBetweenParagraphTags() { .isEqualTo("Test Value"); } + @Test + public void itExecutesJinjavaInsideTag() { + assertThat( + filter.filter("{% for i in [1, 2, 3] %}

    {{i}}
    {% endfor %}", interpreter) + ) + .isEqualTo("1 2 3"); + } + + @Test + public void itIsolatesJinjavaScopeWhenExecutingCodeInsideTag() { + filter.filter("{% set test = 'hello' %}", interpreter); + assertThat(interpreter.getContext().get("test")).isNull(); + } + @Test public void itThrowsDeferredValueExceptionWhenDeferredTokensAreLeft() { AtomicInteger counter = new AtomicInteger(); - when(interpreter.getContext().getDeferredTokens()) - .thenAnswer( - i -> - counter.getAndIncrement() == 0 - ? Collections.emptySet() - : Collections.singleton( - new DeferredToken( - new ExpressionToken( - "{{ deferred && other }}", - 0, - 0, - new DefaultTokenScannerSymbols() - ), - Collections.emptySet() + JinjavaInterpreter mockedInterpreter = mock(JinjavaInterpreter.class); + Context mockedContext = mock(Context.class); + when(mockedInterpreter.getContext()).thenReturn(mockedContext); + when(mockedContext.getDeferredTokens()) + .thenAnswer(i -> + counter.getAndIncrement() == 0 + ? Collections.emptySet() + : Collections.singleton( + DeferredToken + .builderFromImage( + "{{ deferred && other }}", + ExpressionToken.class, + interpreter ) - ) + .build() + ) ); - assertThatThrownBy(() -> filter.filter("{{ deferred && other }}", interpreter)) + assertThatThrownBy(() -> filter.filter("{{ deferred && other }}", mockedInterpreter)) .isInstanceOf(DeferredValueException.class); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/SumFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/SumFilterTest.java index fc12cf8ca..5aea7bdd9 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/SumFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/SumFilterTest.java @@ -38,6 +38,7 @@ public void sumOfSeq() { } public static class Item { + private Number price; public Item(Number price) { diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/SymmetricDifferenceFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/SymmetricDifferenceFilterTest.java index a9535849a..29323ab53 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/SymmetricDifferenceFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/SymmetricDifferenceFilterTest.java @@ -17,26 +17,26 @@ public void setup() { @Test public void itComputesSetDifferences() { assertThat( - jinjava.render( - "{{ [1, 2, 3, 3, 4]|symmetric_difference([1, 2, 5, 6]) }}", - new HashMap<>() - ) + jinjava.render( + "{{ [1, 2, 3, 3, 4]|symmetric_difference([1, 2, 5, 6]) }}", + new HashMap<>() ) + ) .isEqualTo("[3, 4, 5, 6]"); assertThat( - jinjava.render( - "{{ ['do', 'ray']|symmetric_difference(['ray', 'me']) }}", - new HashMap<>() - ) + jinjava.render( + "{{ ['do', 'ray']|symmetric_difference(['ray', 'me']) }}", + new HashMap<>() ) + ) .isEqualTo("['do', 'me']"); } @Test public void itReturnsEmptyOnNullParameters() { assertThat( - jinjava.render("{{ [1, 2, 3]|symmetric_difference(null) }}", new HashMap<>()) - ) + jinjava.render("{{ [1, 2, 3]|symmetric_difference(null) }}", new HashMap<>()) + ) .isEqualTo("[1, 2, 3]"); } } 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..a0c89fb39 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,22 @@ 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"); + } + + @Test + public void itRespectsNewlinesAndTabs() { + assertThat(new TitleFilter().filter("test\t(company)\nname", null)) + .isEqualTo("Test\t(Company)\nName"); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/ToJsonFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/ToJsonFilterTest.java index deeced69e..5dd406f73 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/ToJsonFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/ToJsonFilterTest.java @@ -3,12 +3,18 @@ import static org.assertj.core.api.Java6Assertions.assertThat; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.interpret.OutputTooBigException; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.junit.Before; import org.junit.Test; public class ToJsonFilterTest extends BaseInterpretingTest { + private ToJsonFilter filter; @Before @@ -27,4 +33,29 @@ public void itWritesObjectAsString() { assertThat(filter.filter(testMap, interpreter)) .isEqualTo("{\"testArray\":[4,1,2],\"testString\":\"testString\"}"); } + + @Test + public void itLimitsLength() { + List> original = new ArrayList<>(); + List> temp = original; + for (int i = 0; i < 100; i++) { + List> nested = new ArrayList<>(); + temp.add(nested); + temp = nested; + } + interpreter = + new Jinjava(BaseJinjavaTest.newConfigBuilder().withMaxOutputSize(500).build()) + .newInterpreter(); + assertThat(filter.filter(original, interpreter)).asString().contains("[[]]]]"); + for (int i = 0; i < 400; i++) { + List> nested = new ArrayList<>(); + temp.add(nested); + temp = nested; + } + try { + filter.filter(original, interpreter); + } catch (Exception e) { + assertThat(e).isInstanceOf(OutputTooBigException.class); + } + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/ToYamlFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/ToYamlFilterTest.java index 0bce7f660..16b27de62 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/ToYamlFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/ToYamlFilterTest.java @@ -9,6 +9,7 @@ import org.junit.Test; public class ToYamlFilterTest extends BaseInterpretingTest { + private ToYamlFilter filter; @Before diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/TrimFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/TrimFilterTest.java index 245a33cb2..a45c7558c 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/TrimFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/TrimFilterTest.java @@ -8,6 +8,7 @@ import org.junit.Test; public class TrimFilterTest extends BaseInterpretingTest { + TrimFilter filter; @Before diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/TruncateFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/TruncateFilterTest.java index cdb8fc64c..944ec93cc 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/TruncateFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/TruncateFilterTest.java @@ -8,10 +8,11 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class TruncateFilterTest { + @Mock JinjavaInterpreter interpreter; @@ -27,10 +28,10 @@ public void itPassesThroughSmallEnoughText() throws Exception { @Test public void itTruncatesText() throws Exception { assertThat( - filter - .filter(StringUtils.rightPad("", 256, 'x') + "y", interpreter, "255", "True") - .toString() - ) + filter + .filter(StringUtils.rightPad("", 256, 'x') + "y", interpreter, "255", "True") + .toString() + ) .hasSize(258) .endsWith("x..."); } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilterTest.java index 3aafe1b19..963bc2e85 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilterTest.java @@ -11,6 +11,7 @@ import org.junit.Test; public class TruncateHtmlFilterTest extends BaseInterpretingTest { + TruncateHtmlFilter filter; @Before @@ -26,9 +27,7 @@ public void itPreservesEndTagsWhenTruncatingWithinTagContent() { "33" ); assertThat(result) - .isEqualTo( - "

    HTML Ipsum Presents

    \n

    Pellentesque...

    " - ); + .isEqualTo("

    HTML Ipsum Presents

    \n

    Pellentesque...

    "); } @Test @@ -39,9 +38,7 @@ public void itDoesntChopWordsWhenSpecified() { "35" ); assertThat(result) - .isEqualTo( - "

    HTML Ipsum Presents

    \n

    Pellentesque...

    " - ); + .isEqualTo("

    HTML Ipsum Presents

    \n

    Pellentesque...

    "); result = (String) filter.filter( @@ -53,7 +50,7 @@ public void itDoesntChopWordsWhenSpecified() { ); assertThat(result) .isEqualTo( - "

    HTML Ipsum Presents

    \n

    Pellentesque ha...

    " + "

    HTML Ipsum Presents

    \n

    Pellentesque ha...

    " ); } @@ -66,9 +63,7 @@ public void itTakesKwargs() { ImmutableMap.of("breakwords", false) ); assertThat(result) - .isEqualTo( - "

    HTML Ipsum Presents

    \n

    Pellentesque...

    " - ); + .isEqualTo("

    HTML Ipsum Presents

    \n

    Pellentesque...

    "); result = (String) filter.filter( @@ -79,7 +74,7 @@ public void itTakesKwargs() { ); assertThat(result) .isEqualTo( - "

    HTML Ipsum Presents

    \n

    PellentesqueTEST

    " + "

    HTML Ipsum Presents

    \n

    PellentesqueTEST

    " ); } 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..1d4b12a82 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/UnescapeHtmlFilterTest.java @@ -0,0 +1,26 @@ +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"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/UnionFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/UnionFilterTest.java index 134ac990f..5188b270d 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/UnionFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/UnionFilterTest.java @@ -19,8 +19,8 @@ public void itComputesSetUnions() { assertThat(jinjava.render("{{ [1, 2, 3, 3]|union([1, 2, 5, 6]) }}", new HashMap<>())) .isEqualTo("[1, 2, 3, 5, 6]"); assertThat( - jinjava.render("{{ ['do', 'ray']|union(['ray', 'me']) }}", new HashMap<>()) - ) + jinjava.render("{{ ['do', 'ray']|union(['ray', 'me']) }}", new HashMap<>()) + ) .isEqualTo("['do', 'ray', 'me']"); } 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..ef924bc3f 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/UniqueFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/UniqueFilterTest.java @@ -3,7 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hubspot.jinjava.BaseJinjavaTest; -import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import com.hubspot.jinjava.testobjects.UniqueFilterTestObjects; import java.util.HashMap; import java.util.Map; import org.apache.commons.lang3.StringUtils; @@ -24,14 +24,14 @@ public void itFiltersDuplicatesFromSeq() { @Test public void itFiltersDuplicatesFromSeqByAttr() { assertThat( - render( - "name", - new MyClass("a"), - new MyClass("b"), - new MyClass("a"), - new MyClass("c") - ) + render( + "name", + new UniqueFilterTestObjects.MyClass("a"), + new UniqueFilterTestObjects.MyClass("b"), + new UniqueFilterTestObjects.MyClass("a"), + new UniqueFilterTestObjects.MyClass("c") ) + ) .isEqualTo("[Name:a][Name:b][Name:c]"); } @@ -53,26 +53,4 @@ String render(String attr, Object... items) { context ); } - - public static class MyClass implements PyishSerializable { - private final String name; - - public MyClass(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public String toString() { - return "[Name:" + name + "]"; - } - - @Override - public String toPyishString() { - return toString(); - } - } } 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..7d4af1373 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilterTest.java @@ -1,14 +1,22 @@ package com.hubspot.jinjava.lib.filter; +import static com.hubspot.jinjava.lib.filter.time.DateTimeFormatHelper.FIXED_DATE_TIME_FILTER_NULL_ARG; import static org.assertj.core.api.Assertions.assertThat; +import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.features.DateTimeFeatureActivationStrategy; +import com.hubspot.jinjava.features.FeatureConfig; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.time.ZonedDateTime; import org.junit.After; import org.junit.Before; import org.junit.Test; public class UnixTimestampFilterTest extends BaseInterpretingTest { + private final ZonedDateTime d = ZonedDateTime.parse( "2013-11-06T14:22:00.000+00:00[UTC]" ); @@ -20,12 +28,62 @@ 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); } + + @Test + public void itDefaultsToCurrentDate() { + Jinjava jinjava = new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withDateTimeProvider(() -> d.toEpochSecond() * 1000) + .withFeatureConfig( + FeatureConfig + .newBuilder() + .add(FIXED_DATE_TIME_FILTER_NULL_ARG, DateTimeFeatureActivationStrategy.of(d)) + .build() + ) + .build() + ); + + JinjavaInterpreter.pushCurrent(jinjava.newInterpreter()); + + try { + assertThat(jinjava.render("{{ null | unixtimestamp }}", ImmutableMap.of())) + .isEqualTo(timestamp); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + + @Test + public void itDefaultsToDeprecationDate() { + Jinjava jinjava = new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withDateTimeProvider(() -> d.toEpochSecond() * 1000) + .withFeatureConfig( + FeatureConfig + .newBuilder() + .add(FIXED_DATE_TIME_FILTER_NULL_ARG, DateTimeFeatureActivationStrategy.of(d)) + .build() + ) + .build() + ); + + JinjavaInterpreter.pushCurrent(jinjava.newInterpreter()); + + try { + assertThat(jinjava.render("{{ null | unixtimestamp }}", ImmutableMap.of())) + .isEqualTo("1383747720000"); + } finally { + JinjavaInterpreter.popCurrent(); + } + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/UpperFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/UpperFilterTest.java index 475754f87..1c0461f44 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/UpperFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/UpperFilterTest.java @@ -7,6 +7,7 @@ import org.junit.Test; public class UpperFilterTest extends BaseInterpretingTest { + private UpperFilter filter; @Before diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/UrlDecodeFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/UrlDecodeFilterTest.java index b675b4376..96914cf91 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/UrlDecodeFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/UrlDecodeFilterTest.java @@ -9,6 +9,7 @@ import org.junit.Test; public class UrlDecodeFilterTest extends BaseInterpretingTest { + private UrlDecodeFilter filter; @Before diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/UrlEncodeFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/UrlEncodeFilterTest.java index 865fd4bc7..f2021c064 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/UrlEncodeFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/UrlEncodeFilterTest.java @@ -9,6 +9,7 @@ import org.junit.Test; public class UrlEncodeFilterTest extends BaseInterpretingTest { + UrlEncodeFilter filter; @Before diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/WordCountFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/WordCountFilterTest.java index 40f0c78a5..42c42073c 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/WordCountFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/WordCountFilterTest.java @@ -6,6 +6,7 @@ import org.junit.Test; public class WordCountFilterTest { + WordCountFilter filter; @Before diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/XmlAttrFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/XmlAttrFilterTest.java index 7f6f0eebc..13f9a6bc7 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/XmlAttrFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/XmlAttrFilterTest.java @@ -2,8 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.google.common.collect.ImmutableList; import com.hubspot.jinjava.BaseJinjavaTest; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -27,4 +30,24 @@ public void testXmlAttr() { assertThat(dom.select("ul").attr("id")).isEqualTo("list-42"); assertThat(dom.select("ul").attr("missing")).isEmpty(); } + + @Test + public void itDoesNotAllowInvalidKeys() { + List invalidStrings = ImmutableList.of("\t", "\n", "\f", " ", "/", ">", "="); + invalidStrings.forEach(invalidString -> + assertThat( + jinjava + .renderForResult( + String.format("{{ {'%s': 'foo'}|xmlattr }}", invalidString), + Collections.emptyMap() + ) + .getErrors() + ) + .matches(templateErrors -> + templateErrors.size() == 1 && + templateErrors.get(0).getException().getCause().getCause() instanceof + IllegalArgumentException + ) + ); + } } 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..65c0b321b --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/time/FormatDateFilterTest.java @@ -0,0 +1,297 @@ +package com.hubspot.jinjava.lib.filter.time; + +import static com.hubspot.jinjava.lib.filter.time.DateTimeFormatHelper.FIXED_DATE_TIME_FILTER_NULL_ARG; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.features.DateTimeFeatureActivationStrategy; +import com.hubspot.jinjava.features.FeatureConfig; +import com.hubspot.jinjava.interpret.RenderResult; +import com.hubspot.jinjava.interpret.TemplateError; +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(BaseJinjavaTest.newConfigBuilder().build()); + 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"); + } + + @Test + public void itWarnsOnMissingFilterArg() { + RenderResult renderResult = jinjava.renderForResult( + "{{ d | format_date('short', 'America/New_York', null) }}", + ImmutableMap.of("d", DATE_TIME) + ); + assertThat(renderResult.getOutput()).isEqualTo("11/10/22"); + + assertThat(renderResult.getErrors()).isEmpty(); + + renderResult = + jinjava.renderForResult( + "{{ d | format_date('short', 'America/New_York', null) }}", + ImmutableMap.of() + ); + assertThat(renderResult.getErrors().get(0)).isInstanceOf(TemplateError.class); + assertThat(renderResult.getErrors().get(0).getMessage()) + .isEqualTo("format_date filter called with null datetime"); + } + + @Test + public void itDefaultsToCurrentDateOnMissingFilterArg() { + jinjava = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withDateTimeProvider(() -> 1233333414223L) + .build() + ); + + assertThat( + jinjava.render( + "{{ d | format_date('short', 'America/New_York', null) }}", + ImmutableMap.of() + ) + ) + .isEqualTo("1/30/09"); + } + + @Test + public void itDefaultsToDeprecationDateOnMissingFilterArg() { + jinjava = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withDateTimeProvider(() -> 1233333414223L) + .withFeatureConfig( + FeatureConfig + .newBuilder() + .add( + FIXED_DATE_TIME_FILTER_NULL_ARG, + DateTimeFeatureActivationStrategy.of(DATE_TIME) + ) + .build() + ) + .build() + ); + + assertThat( + jinjava.render( + "{{ d | format_date('short', 'America/New_York', null) }}", + ImmutableMap.of() + ) + ) + .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..c6dbe0aa5 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/time/FormatDatetimeFilterTest.java @@ -0,0 +1,238 @@ +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)) + ) + .isIn("Nov 10, 2022, 10:49:07 PM", "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)) + ) + .isIn("Nov 10, 2022, 10:49:07 PM", "Nov 10, 2022, 10:49:07 PM"); + } + + @Test + public void itFormatsZonedDateTime() { + assertThat( + jinjava.render("{{ d | format_datetime }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isIn("Nov 10, 2022, 10:49:07 PM", "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) + ) + ) + .isIn("11/10/22, 10:49 PM", "11/10/22, 10:49 PM"); + } + + @Test + public void itUsesMediumFormat() { + assertThat( + jinjava.render( + "{{ d | format_datetime('medium') }}", + ImmutableMap.of("d", DATE_TIME) + ) + ) + .isIn("Nov 10, 2022, 10:49:07 PM", "Nov 10, 2022, 10:49:07 PM"); + } + + @Test + public void itUsesLongFormat() { + assertThat( + jinjava.render("{{ d | format_datetime('long') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isIn("November 10, 2022 at 10:49:07 PM Z", "November 10, 2022, 10:49:07 PM Z"); + } + + @Test + public void itUsesFullFormat() { + assertThat( + jinjava.render("{{ d | format_datetime('full') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isIn( + "Thursday, November 10, 2022 at 10:49:07 PM Z", + "Thursday, November 10, 2022, 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) + ) + ) + .isIn("November 10, 2022 at 5:49:07 PM EST", "November 10, 2022, 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) + ) + ) + .isIn("11/10/22, 5:49 PM", "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..300334684 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/time/FormatTimeFilterTest.java @@ -0,0 +1,222 @@ +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)) + ) + .isIn("10:49:07 PM", "10:49:07 PM"); + } + + @Test + public void itFormatsPyishDates() { + PyishDate pyishDate = new PyishDate(1668120547000L); + + assertThat(jinjava.render("{{ d | format_time }}", ImmutableMap.of("d", pyishDate))) + .isIn("10:49:07 PM", "10:49:07 PM"); + } + + @Test + public void itFormatsZonedDateTime() { + assertThat(jinjava.render("{{ d | format_time }}", ImmutableMap.of("d", DATE_TIME))) + .isIn("10:49:07 PM", "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)) + ) + .isIn("10:49 PM", "10:49 PM"); + } + + @Test + public void itUsesMediumFormat() { + assertThat( + jinjava.render("{{ d | format_time('medium') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isIn("10:49:07 PM", "10:49:07 PM"); + } + + @Test + public void itUsesLongFormat() { + assertThat( + jinjava.render("{{ d | format_time('long') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isIn("10:49:07 PM Z", "10:49:07 PM Z"); + } + + @Test + public void itUsesFullFormat() { + assertThat( + jinjava.render("{{ d | format_time('full') }}", ImmutableMap.of("d", DATE_TIME)) + ) + .isIn("10:49:07 PM Z", "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) + ) + ) + .isIn("5:49:07 PM EST", "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) + ) + ) + .isIn("17:49:07", "5:49 PM"); + } + + @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) + ) + ) + .isIn("5:49 PM", "5:49 PM"); + } +} 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..9e28eee10 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/fn/DateFormatFunctionsTest.java @@ -0,0 +1,53 @@ +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)) + ) + ) + .isIn("4:30:04 PM", "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)) + ) + ) + .isIn("Nov 28, 2022, 4:30:04 PM", "Nov 28, 2022, 4:30:04 PM"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/fn/InjectedContextFunctionProxyTest.java b/src/test/java/com/hubspot/jinjava/lib/fn/InjectedContextFunctionProxyTest.java index bf79a53ef..6b4c00a0e 100644 --- a/src/test/java/com/hubspot/jinjava/lib/fn/InjectedContextFunctionProxyTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/fn/InjectedContextFunctionProxyTest.java @@ -8,6 +8,7 @@ public class InjectedContextFunctionProxyTest { public static class MyClass { + private String state; public MyClass(String state) { @@ -19,6 +20,19 @@ public String concatState(String in) { } } + public static class OtherClass { + + private String state; + + public OtherClass(String state) { + this.state = state; + } + + public String prependState(String in) { + return state + in; + } + } + @Test public void testDefineProxy() throws Exception { Method m = MyClass.class.getDeclaredMethod("concatState", String.class); @@ -31,9 +45,56 @@ public void testDefineProxy() throws Exception { instance ); assertThat(proxy.getName()).isEqualTo("ns:fooproxy"); - assertThat(proxy.getMethod().getDeclaringClass().getSimpleName()) - .isEqualTo(InjectedContextFunctionProxy.class.getSimpleName() + "$$ns$$fooproxy"); + assertThat(proxy.getMethod().getDeclaringClass().getName()) + .isEqualTo( + MyClass.class.getName() + + "$$" + + InjectedContextFunctionProxy.class.getSimpleName() + + "$$ns$$fooproxy" + ); assertThat(proxy.getMethod().invoke(null, "foo")).isEqualTo("foobar"); } + + @Test + public void testDefineMultipleProxies() throws Exception { + Method concat = MyClass.class.getDeclaredMethod("concatState", String.class); + MyClass myClassInstance = new MyClass("bar"); + + ELFunctionDefinition myClassProxy = InjectedContextFunctionProxy.defineProxy( + "ns", + "fooproxy", + concat, + myClassInstance + ); + Method prepend = OtherClass.class.getDeclaredMethod("prependState", String.class); + OtherClass otherClassInstance = new OtherClass("bar"); + + ELFunctionDefinition otherClassProxy = InjectedContextFunctionProxy.defineProxy( + "ns", + "fooproxy", + prepend, + otherClassInstance + ); + assertThat(myClassProxy.getName()).isEqualTo("ns:fooproxy"); + assertThat(myClassProxy.getMethod().getDeclaringClass().getName()) + .isEqualTo( + MyClass.class.getName() + + "$$" + + InjectedContextFunctionProxy.class.getSimpleName() + + "$$ns$$fooproxy" + ); + + assertThat(myClassProxy.getMethod().invoke(null, "foo")).isEqualTo("foobar"); + assertThat(otherClassProxy.getName()).isEqualTo("ns:fooproxy"); + assertThat(otherClassProxy.getMethod().getDeclaringClass().getName()) + .isEqualTo( + OtherClass.class.getName() + + "$$" + + InjectedContextFunctionProxy.class.getSimpleName() + + "$$ns$$fooproxy" + ); + + assertThat(otherClassProxy.getMethod().invoke(null, "foo")).isEqualTo("barfoo"); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/fn/RangeFunctionTest.java b/src/test/java/com/hubspot/jinjava/lib/fn/RangeFunctionTest.java index cc6073406..207d04c3f 100644 --- a/src/test/java/com/hubspot/jinjava/lib/fn/RangeFunctionTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/fn/RangeFunctionTest.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.InvalidArgumentException; @@ -15,11 +16,12 @@ import org.junit.Test; public class RangeFunctionTest { + private JinjavaConfig config; @Before public void beforeEach() { - config = JinjavaConfig.newBuilder().build(); + config = BaseJinjavaTest.newConfigBuilder().build(); Jinjava jinjava = new Jinjava(config); pushCurrent(new JinjavaInterpreter(jinjava.newInterpreter())); } @@ -88,8 +90,8 @@ public void itTruncatesRangeToDefaultRangeLimit() { public void itTruncatesRangeToCustomRangeLimit() { JinjavaInterpreter.popCurrent(); int customRangeLimit = 10; - JinjavaConfig customConfig = JinjavaConfig - .newBuilder() + JinjavaConfig customConfig = BaseJinjavaTest + .newConfigBuilder() .withRangeLimit(customRangeLimit) .build(); pushCurrent(new JinjavaInterpreter(new Jinjava(customConfig).newInterpreter())); 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..04e8550f4 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,45 @@ 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/lib/fn/TodayFunctionTest.java b/src/test/java/com/hubspot/jinjava/lib/fn/TodayFunctionTest.java index abb3830c4..3a380e471 100644 --- a/src/test/java/com/hubspot/jinjava/lib/fn/TodayFunctionTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/fn/TodayFunctionTest.java @@ -3,13 +3,14 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; -import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl; import com.hubspot.jinjava.interpret.Context; -import com.hubspot.jinjava.interpret.DeferredValueException; 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; @@ -17,16 +18,42 @@ 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() { ZonedDateTime zonedDateTime = Functions.today(); assertThat(zonedDateTime.getZone()).isEqualTo(ZoneOffset.UTC); } + @Test + public void itUsesFixedDateTimeProvider() { + long ts = 1233333414223L; + + try ( + AutoCloseableImpl a = JinjavaInterpreter + .closeablePushCurrent( + new JinjavaInterpreter( + new Jinjava(), + new Context(), + BaseJinjavaTest + .newConfigBuilder() + .withDateTimeProvider(new FixedDateTimeProvider(ts)) + .build() + ) + ) + .get() + ) { + assertThat(Functions.today(ZONE_NAME)) + .isEqualTo(ZonedDateTime.of(2009, 1, 30, 0, 0, 0, 0, ZONE_ID)); + } + } + @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) @@ -39,22 +66,24 @@ public void itIgnoresNullTimezone() { assertThat(Functions.today((String) null).getZone()).isEqualTo(ZoneOffset.UTC); } - @Test(expected = DeferredValueException.class) + @Test public void itDefersWhenExecutingEagerly() { - JinjavaInterpreter.pushCurrent( - new JinjavaInterpreter( - new Jinjava(), - new Context(), - JinjavaConfig - .newBuilder() - .withExecutionMode(EagerExecutionMode.instance()) - .build() - ) - ); - try { - Functions.today("America/New_York"); - } finally { - JinjavaInterpreter.popCurrent(); + try ( + AutoCloseableImpl a = JinjavaInterpreter + .closeablePushCurrent( + new JinjavaInterpreter( + new Jinjava(), + new Context(), + BaseJinjavaTest + .newConfigBuilder() + .withExecutionMode(EagerExecutionMode.instance()) + .build() + ) + ) + .get() + ) { + ZonedDateTime today = Functions.today(ZONE_NAME); + assertThat(today.getYear()).isGreaterThan(2023); } } } 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..58aca72f9 100644 --- a/src/test/java/com/hubspot/jinjava/lib/fn/TypeFunctionTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/fn/TypeFunctionTest.java @@ -33,8 +33,8 @@ public void testDouble() { @Test public void testDate() { assertThat( - TypeFunction.type(ZonedDateTime.parse("2013-11-06T14:22:00.000+00:00[UTC]")) - ) + TypeFunction.type(ZonedDateTime.parse("2013-11-06T14:22:00.000+00:00[UTC]")) + ) .isEqualTo("datetime"); } @@ -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"); + } } 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..380c72a50 100644 --- a/src/test/java/com/hubspot/jinjava/lib/fn/UnixTimestampFunctionTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/fn/UnixTimestampFunctionTest.java @@ -2,36 +2,63 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; -import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.Context; 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.After; import org.junit.Test; public class UnixTimestampFunctionTest { + private final ZonedDateTime d = ZonedDateTime.parse( "2013-11-06T14:22:12.345+00:00[UTC]" ); private final long epochMilliseconds = d.toEpochSecond() * 1000 + 345; + @After + public void tearDown() { + JinjavaInterpreter.popCurrent(); + } + @Test public void itGetsUnixTimestamps() { + JinjavaInterpreter jinjavaInterpreter = new JinjavaInterpreter( + new Jinjava(), + new Context(), + BaseJinjavaTest.newConfigBuilder().build() + ); + JinjavaInterpreter.pushCurrent(jinjavaInterpreter); assertThat(Functions.unixtimestamp()) .isGreaterThan(0) .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)); + assertThat(jinjavaInterpreter.getErrors()).isEmpty(); + } + + @Test + public void itUsesFixedDateTimeProvider() { + long ts = 1233333414223L; + + JinjavaInterpreter.pushCurrent( + new JinjavaInterpreter( + new Jinjava(), + new Context(), + BaseJinjavaTest + .newConfigBuilder() + .withDateTimeProvider(new FixedDateTimeProvider(ts)) + .build() ) - .isLessThan(1000); + ); + assertThat(Functions.unixtimestamp((Object) null)).isEqualTo(ts); } @Test(expected = DeferredValueException.class) @@ -40,16 +67,12 @@ public void itDefersWhenExecutingEagerly() { new JinjavaInterpreter( new Jinjava(), new Context(), - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(EagerExecutionMode.instance()) .build() ) ); - try { - Functions.unixtimestamp((Object) null); - } finally { - JinjavaInterpreter.popCurrent(); - } + Functions.unixtimestamp((Object) null); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/fn/eager/EagerMacroFunctionTest.java b/src/test/java/com/hubspot/jinjava/lib/fn/eager/EagerMacroFunctionTest.java index 208c5e612..75d53e6d2 100644 --- a/src/test/java/com/hubspot/jinjava/lib/fn/eager/EagerMacroFunctionTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/fn/eager/EagerMacroFunctionTest.java @@ -4,7 +4,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.hubspot.jinjava.BaseInterpretingTest; -import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; @@ -26,8 +26,8 @@ public void eagerSetup() { new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(EagerExecutionMode.instance()) .withLegacyOverrides( LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() @@ -49,13 +49,8 @@ public void teardown() { public void itReconstructsImage() { String name = "foo"; String code = "{% macro foo(bar) %}It's: {{ bar }}{% endmacro %}"; - MacroFunction macroFunction = makeMacroFunction(name, code); - EagerMacroFunction eagerMacroFunction = new EagerMacroFunction( - name, - macroFunction, - interpreter - ); - assertThat(eagerMacroFunction.reconstructImage()).isEqualTo(code); + EagerMacroFunction eagerMacroFunction = makeMacroFunction(name, code); + assertThat(eagerMacroFunction.reconstructImage(name)).isEqualTo(code); } @Test @@ -63,59 +58,60 @@ public void itResolvesFromContext() { context.put("foobar", "resolved"); String name = "foo"; String code = "{% macro foo(bar) %}{{ foobar }} and {{ bar }}{% endmacro %}"; - MacroFunction macroFunction = makeMacroFunction(name, code); - EagerMacroFunction eagerMacroFunction = new EagerMacroFunction( - name, - macroFunction, - interpreter - ); - assertThat(eagerMacroFunction.reconstructImage()) + EagerMacroFunction eagerMacroFunction = makeMacroFunction(name, code); + assertThat(eagerMacroFunction.reconstructImage(name)) .isEqualTo("{% macro foo(bar) %}resolved and {{ bar }}{% endmacro %}"); } @Test public void itReconstructsForAliasedName() { + context.remove("deferred"); String name = "foo"; String fullName = "local." + name; String codeFormat = "{%% macro %s(bar) %%}It's: {{ bar }}{%% endmacro %%}"; - MacroFunction macroFunction = makeMacroFunction( + EagerMacroFunction eagerMacroFunction = makeMacroFunction( name, String.format(codeFormat, name) ); - EagerMacroFunction eagerMacroFunction = new EagerMacroFunction( - fullName, - macroFunction, - interpreter - ); - assertThat(eagerMacroFunction.reconstructImage()) + assertThat(eagerMacroFunction.reconstructImage(fullName)) .isEqualTo(String.format(codeFormat, fullName)); } + @Test + public void itResolvesFromSet() { + String template = + "{% macro foo(foobar, other) %}" + + " {% do foobar.update({'a': 'b'}) %} " + + " {{ foobar }} and {{ other }}" + + "{% endmacro %}" + + "{% set bar = {} %}" + + "{% call foo(bar, deferred) %} {% endcall %}" + + "{{ bar }}"; + String firstPass = interpreter.render(template); + assertThat(firstPass).isEqualTo(template); + } + @Test public void itReconstructsImageWithNamedParams() { String name = "foo"; String code = "{% macro foo(bar, baz=0) %}It's: {{ bar }}, {{ baz }}{% endmacro %}"; - MacroFunction macroFunction = makeMacroFunction(name, code); - EagerMacroFunction eagerMacroFunction = new EagerMacroFunction( - name, - macroFunction, - interpreter - ); - assertThat(eagerMacroFunction.reconstructImage()).isEqualTo(code); + EagerMacroFunction eagerMacroFunction = makeMacroFunction(name, code); + assertThat(eagerMacroFunction.reconstructImage(name)).isEqualTo(code); } @Test public void itPartiallyEvaluatesMacroFunction() { // Put this test here because it's only used in eager execution context.put("deferred", DeferredValue.instance()); - MacroFunction macroFunction = makeMacroFunction( + EagerMacroFunction eagerMacroFunction = makeMacroFunction( "foo", "{% macro foo(bar) %}It's: {{ bar }}, {{ deferred }}{% endmacro %}" ); - assertThatThrownBy(() -> macroFunction.evaluate("Bar")) + assertThatThrownBy(() -> eagerMacroFunction.evaluate("Bar")) .isInstanceOf(DeferredValueException.class); try (TemporaryValueClosable ignored = context.withPartialMacroEvaluation()) { - assertThat(macroFunction.evaluate("Bar")).isEqualTo("It's: Bar, {{ deferred }}"); + assertThat(eagerMacroFunction.evaluate("Bar")) + .isEqualTo("{% for __ignored__ in [0] %}It's: Bar, {{ deferred }}{% endfor %}"); } } @@ -124,14 +120,9 @@ public void itDoesNotAllowStackOverflow() { String name = "rec"; String code = "{% macro rec(num=0) %}{% if num > 0 %}{{ num }}-{{ rec(num - 1)}}{% endif %}{% endmacro %}"; - MacroFunction macroFunction = makeMacroFunction(name, code); + EagerMacroFunction eagerMacroFunction = makeMacroFunction(name, code); String output; try (InterpreterScopeClosable c = interpreter.enterScope()) { - EagerMacroFunction eagerMacroFunction = new EagerMacroFunction( - name, - macroFunction, - interpreter - ); output = eagerMacroFunction.reconstructImage(); } assertThat(interpreter.render(output + "{{ rec(5) }}")).isEqualTo("5-4-3-2-1-"); @@ -155,23 +146,18 @@ public void itDefersDifferentMacrosWithSameName() { interpreter.getContext().addGlobalMacro(foo1Macro); interpreter.getContext().addGlobalMacro(barMacro); - MacroFunction foo2Macro; + EagerMacroFunction foo2Macro; String output; try (InterpreterScopeClosable c = interpreter.enterScope()) { foo2Macro = makeMacroFunction("foo", foo2Code); - EagerMacroFunction eagerMacroFunction = new EagerMacroFunction( - "foo", - foo2Macro, - interpreter - ); - output = eagerMacroFunction.reconstructImage(); + output = foo2Macro.reconstructImage(); } assertThat(interpreter.render(output + "{{ foo('Foo') }}")) .isEqualTo("~^This is the Foo^~"); } - private MacroFunction makeMacroFunction(String name, String code) { + private EagerMacroFunction makeMacroFunction(String name, String code) { interpreter.render(code); - return interpreter.getContext().getGlobalMacro(name); + return (EagerMacroFunction) interpreter.getContext().getGlobalMacro(name); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/BreakTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/BreakTagTest.java new file mode 100644 index 000000000..1d9312f7e --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/tag/BreakTagTest.java @@ -0,0 +1,51 @@ +package com.hubspot.jinjava.lib.tag; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.interpret.RenderResult; +import com.hubspot.jinjava.interpret.TemplateError; +import org.junit.Test; + +public class BreakTagTest extends BaseInterpretingTest { + + @Test + public void testBreak() { + String template = + "{% for item in [1, 2, 3, 4] %}{% if item > 2 %}{% break %}{% endif %}{{ item }}{% endfor %}"; + + RenderResult rendered = jinjava.renderForResult(template, context); + assertThat(rendered.getOutput()).isEqualTo("12"); + } + + @Test + public void testNestedBreak() { + String template = + "{% for item in [1, 2, 3, 4] %}{% for item2 in [5, 6, 7] %}{% break %}{{ item2 }}{% endfor %}{{ item }}{% endfor %}"; + + RenderResult rendered = jinjava.renderForResult(template, context); + assertThat(rendered.getOutput()).isEqualTo("1234"); + } + + @Test + public void testBreakWithEarlierContent() { + String template = + "{% for item in [1, 2, 3, 4] %}{{ item }}{% if item > 2 %}{% break %}{% endif %}{{ item }}{% endfor %}"; + + RenderResult rendered = jinjava.renderForResult(template, context); + assertThat(rendered.getOutput()).isEqualTo("11223"); + } + + @Test + public void testBreakOutOfContext() { + String template = "{% break %}"; + + RenderResult rendered = jinjava.renderForResult(template, context); + assertThat(rendered.getOutput()).isEqualTo(""); + assertThat(rendered.getErrors()).hasSize(1); + assertThat(rendered.getErrors().get(0).getSeverity()) + .isEqualTo(TemplateError.ErrorType.FATAL); + assertThat(rendered.getErrors().get(0).getMessage()) + .contains("NotInLoopException: `break` called while not in a for loop"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/CallTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/CallTagTest.java index 76d419d8b..b3b152d02 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/CallTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/CallTagTest.java @@ -4,6 +4,9 @@ import com.google.common.io.Resources; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.io.IOException; import java.nio.charset.StandardCharsets; import org.jsoup.Jsoup; @@ -20,6 +23,28 @@ public void testSimpleFn() { .isEqualTo("This is a simple dialog rendered by using a macro and a call block."); } + @Test + public void itDoesNotDoubleCountCallTagTowardsDepth() throws IOException { + interpreter = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withEnableRecursiveMacroCalls(true) + .withMaxMacroRecursionDepth(6) // There are 3 call tags, but a total of 6 "macro" calls happening in this file as each call to `caller()` counts too + .build() + ) + .newInterpreter(); + JinjavaInterpreter.pushCurrent(interpreter); + + try { + String template = fixture("multiple"); + interpreter.render(template); + assertThat(interpreter.getErrorsCopy()).isEmpty(); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + private String fixture(String name) { try { return Resources.toString( diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/ContinueTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/ContinueTagTest.java new file mode 100644 index 000000000..3b0ac69bc --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/tag/ContinueTagTest.java @@ -0,0 +1,51 @@ +package com.hubspot.jinjava.lib.tag; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.interpret.RenderResult; +import com.hubspot.jinjava.interpret.TemplateError; +import org.junit.Test; + +public class ContinueTagTest extends BaseInterpretingTest { + + @Test + public void testContinue() { + String template = + "{% for item in [1, 2, 3, 4] %}{% if item % 2 == 0 %}{% continue %}{% endif %}{{ item }}{% endfor %}"; + + RenderResult rendered = jinjava.renderForResult(template, context); + assertThat(rendered.getOutput()).isEqualTo("13"); + } + + @Test + public void testNestedContinue() { + String template = + "{% for item in [1, 2, 3, 4] %}{% for item2 in [5, 6, 7] %}{% continue %}{{ item2 }}{% endfor %}{{ item }}{% endfor %}"; + + RenderResult rendered = jinjava.renderForResult(template, context); + assertThat(rendered.getOutput()).isEqualTo("1234"); + } + + @Test + public void testContinueWithEarlierContent() { + String template = + "{% for item in [1, 2, 3, 4] %}{{ item }}{% if item % 2 == 0 %}{% continue %}{% endif %}{{ item }}{% endfor %}"; + + RenderResult rendered = jinjava.renderForResult(template, context); + assertThat(rendered.getOutput()).isEqualTo("112334"); + } + + @Test + public void testContinueOutOfContext() { + String template = "{% continue %}"; + + RenderResult rendered = jinjava.renderForResult(template, context); + assertThat(rendered.getOutput()).isEqualTo(""); + assertThat(rendered.getErrors()).hasSize(1); + assertThat(rendered.getErrors().get(0).getSeverity()) + .isEqualTo(TemplateError.ErrorType.FATAL); + assertThat(rendered.getErrors().get(0).getMessage()) + .contains("NotInLoopException: `continue` called while not in a for loop"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/CycleTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/CycleTagTest.java index 58a2c2e49..28b062077 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/CycleTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/CycleTagTest.java @@ -32,4 +32,11 @@ public void itDefaultsMultipleNullToImageUsingAs() { "{% for item in [0,1] %}{% cycle {{foo}},{{bar}} as var %}{% cycle var %}{% endfor %}"; assertThat(interpreter.render(template)).isEqualTo("{{foo}}{{bar}}"); } + + @Test + public void itHandlesEscapedQuotes() { + String template = + "{% for item in [0,1] %}{% cycle 'a','class=\\'foo bar\\'' %}.{% endfor %}"; + assertThat(interpreter.render(template)).isEqualTo("a.class='foo bar'."); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/DoTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/DoTagTest.java index 427c42b88..a039e983d 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/DoTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/DoTagTest.java @@ -17,11 +17,18 @@ public void itResolvesExpressions() { } @Test - public void itAddsTemplateErrorOnEmptyExpression() { + public void itAddsTemplateErrorOnEmptyExpressionAndNoEndTag() { String template = "{% do %}"; RenderResult renderResult = jinjava.renderForResult(template, Maps.newHashMap()); assertThat(renderResult.getErrors()).hasSize(1); assertThat(renderResult.getErrors().get(0).getReason()) .isEqualTo(ErrorReason.SYNTAX_ERROR); } + + @Test + public void itEvaluatesDoBlockAndDiscardsResult() { + String template = + "{% do %}{% set foo = 1 %}{{ foo }}{% enddo %}{% if foo == 1 %}Yes{% endif %}"; + assertThat(jinjava.render(template, Maps.newHashMap())).isEqualTo("Yes"); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/ExtendsTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/ExtendsTagTest.java index a65765131..b5c091151 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/ExtendsTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/ExtendsTagTest.java @@ -25,6 +25,7 @@ import org.junit.Test; public class ExtendsTagTest extends BaseInterpretingTest { + private ExtendsTagTestResourceLocator locator; @Before @@ -258,6 +259,7 @@ public void itLimitsExtendsWithMultipleLevels() throws IOException { } private static class ExtendsTagTestResourceLocator implements ResourceLocator { + private RelativePathResolver relativePathResolver = new RelativePathResolver(); @Override @@ -265,8 +267,7 @@ public String getString( String fullName, Charset encoding, JinjavaInterpreter interpreter - ) - throws IOException { + ) throws IOException { return fixture(fullName); } 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..5333b5f7c 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java @@ -10,12 +10,13 @@ import com.google.common.collect.Maps; import com.google.common.io.Resources; import com.hubspot.jinjava.BaseInterpretingTest; -import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.InterpretException; 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; @@ -32,11 +33,28 @@ import org.junit.Test; public class ForTagTest extends BaseInterpretingTest { + public Tag tag; @Before - public void setup() { + @Override + public void baseSetup() { + super.baseSetup(); tag = new ForTag(); + + try { + jinjava + .getGlobalContext() + .registerFunction( + new ELFunctionDefinition( + "", + "in_for_loop", + this.getClass().getDeclaredMethod("inForLoop") + ) + ); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } } @Test @@ -60,12 +78,12 @@ public void forLoopUsingScalarValue() { public void forLoopNestedFor() { TagNode tagNode = (TagNode) fixture("nested-fors"); assertThat( - Splitter - .on("\n") - .trimResults() - .omitEmptyStrings() - .split(tag.interpret(tagNode, interpreter)) - ) + Splitter + .on("\n") + .trimResults() + .omitEmptyStrings() + .split(tag.interpret(tagNode, interpreter)) + ) .contains("02", "03", "12", "13"); } @@ -196,8 +214,8 @@ public void testForLoopWithDates() { new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withLegacyOverrides( LegacyOverrides.newBuilder().withUsePyishObjectMapper(false).build() ) @@ -349,4 +367,56 @@ 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() }}"; + + RenderResult rendered = jinjava.renderForResult(template, context); + assertThat(rendered.getOutput()).isEqualTo("false true true false"); + } + + @Test + public void forLoopWithNullValues() { + context.put("number", -1); + context.put("the_list", Lists.newArrayList(1L, 2L, null, null, null)); + String template = "{% for number in the_list %} {{ number }} {% endfor %}"; + TagNode tagNode = (TagNode) new TreeParser(interpreter, template) + .buildTree() + .getChildren() + .getFirst(); + String result = tag.interpret(tagNode, interpreter); + assertThat(result).isEqualTo(" 1 2 null null null "); + } + + @Test + public void forLoopTupleWithNullValues() { + context.put("number", -1); + context.put("the_list", Lists.newArrayList(1L, 2L, null, null, null)); + String template = "{% for number,name in the_list %} {{ number }} {% endfor %}"; + TagNode tagNode = (TagNode) new TreeParser(interpreter, template) + .buildTree() + .getChildren() + .getFirst(); + String result = tag.interpret(tagNode, interpreter); + // This is quite intuitive, if the value cannot be assigned to the loop var, + // the outer value of number is used as in the loop, number is not assigned if val is not null. + assertThat(result).isEqualTo(" -1 -1 null null null "); + } + + @Test + public void itUsesJinjavaRestrictedResolverOnReadingLoopVars() { + String template = + """ + {% for _, config, class in ____int3rpr3t3r____ %}{{ class }}{% endfor %}"""; + String result = interpreter.render(template); + assertThat(result).isEqualTo(""); + } + + public static boolean inForLoop() { + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); + return interpreter.getContext().isInForLoop(); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/FromTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/FromTagTest.java index e8a386381..36dd47886 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/FromTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/FromTagTest.java @@ -33,8 +33,7 @@ public String getString( String fullName, Charset encoding, JinjavaInterpreter interpreter - ) - throws IOException { + ) throws IOException { return Resources.toString( Resources.getResource(String.format("tags/macrotag/%s", fullName)), StandardCharsets.UTF_8 @@ -58,6 +57,14 @@ public void importedContextExposesVars() { .contains("wrap-padding: padding-left:42px;padding-right:42px"); } + @Test + public void itImportsAliasedMacroName() { + assertThat(fixture("from-alias-macro")) + .contains("wrap-spacer:") + .contains("") + .contains("wrap-padding: padding-left:42px;padding-right:42px"); + } + @Test public void importedCycleDected() { fixture("from-recursion"); diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/IfTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/IfTagTest.java index 6216973eb..62ae0e483 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/IfTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/IfTagTest.java @@ -5,16 +5,16 @@ import com.google.common.collect.Lists; import com.google.common.io.Resources; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.testobjects.IfTagTestObjects; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.TreeParser; -import com.hubspot.jinjava.util.HasObjectTruthValue; -import com.hubspot.jinjava.util.ObjectTruthValueTest; import java.io.IOException; import java.nio.charset.StandardCharsets; import org.junit.Before; import org.junit.Test; public class IfTagTest extends BaseInterpretingTest { + public Tag tag; @Before @@ -38,11 +38,11 @@ public void itDoesntEvalChildrenWhenExprIsFalse() { @Test public void itChecksObjectTruthValue() { - context.put("foo", new TestObject().setObjectTruthValue(true)); + context.put("foo", new IfTagTestObjects.Foo().setObjectTruthValue(true)); TagNode n = fixture("if-object"); assertThat(tag.interpret(n, interpreter).trim()).isEqualTo("ifblock"); - context.put("foo", new TestObject().setObjectTruthValue(false)); + context.put("foo", new IfTagTestObjects.Foo().setObjectTruthValue(false)); n = fixture("if-object"); assertThat(tag.interpret(n, interpreter).trim()).isEqualTo(""); } @@ -130,18 +130,4 @@ private TagNode fixture(String name) { throw new RuntimeException(e); } } - - private class TestObject implements HasObjectTruthValue { - private boolean objectTruthValue = false; - - public TestObject setObjectTruthValue(boolean objectTruthValue) { - this.objectTruthValue = objectTruthValue; - return this; - } - - @Override - public boolean getObjectTruthValue() { - return objectTruthValue; - } - } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/ImportTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/ImportTagTest.java index 0c9ae2bd1..afd9a7f18 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/ImportTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/ImportTagTest.java @@ -6,15 +6,14 @@ import com.google.common.io.Resources; import com.hubspot.jinjava.BaseInterpretingTest; -import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.RenderResult; import com.hubspot.jinjava.lib.fn.MacroFunction; -import com.hubspot.jinjava.lib.tag.eager.EagerImportTagTest.PrintPathFilter; import com.hubspot.jinjava.loader.LocationResolver; import com.hubspot.jinjava.loader.RelativePathResolver; import com.hubspot.jinjava.loader.ResourceLocator; +import com.hubspot.jinjava.testobjects.EagerImportTagTestObjects.PrintPathFilter; import com.hubspot.jinjava.tree.Node; import java.io.IOException; import java.nio.charset.Charset; @@ -31,23 +30,12 @@ public class ImportTagTest extends BaseInterpretingTest { @Before public void setup() { - jinjava.setResourceLocator( - (fullName, encoding, interpreter) -> - Resources.toString( - Resources.getResource(String.format("tags/macrotag/%s", fullName)), - StandardCharsets.UTF_8 - ) - ); - context.put("padding", 42); context.registerFilter(new PrintPathFilter()); } @Test public void itAvoidsSimpleImportCycle() throws IOException { - Jinjava jinjava = new Jinjava(); - interpreter = new JinjavaInterpreter(jinjava, context, jinjava.getGlobalConfig()); - interpreter.render( Resources.toString( Resources.getResource("tags/importtag/imports-self.jinja"), @@ -62,9 +50,6 @@ public void itAvoidsSimpleImportCycle() throws IOException { @Test public void itAvoidsNestedImportCycle() throws IOException { - Jinjava jinjava = new Jinjava(); - interpreter = new JinjavaInterpreter(jinjava, context, jinjava.getGlobalConfig()); - interpreter.render( Resources.toString( Resources.getResource("tags/importtag/a-imports-b.jinja"), @@ -80,9 +65,6 @@ public void itAvoidsNestedImportCycle() throws IOException { @Test public void itHandlesNullImportedValues() throws IOException { - Jinjava jinjava = new Jinjava(); - interpreter = new JinjavaInterpreter(jinjava, context, jinjava.getGlobalConfig()); - interpreter.render( Resources.toString( Resources.getResource("tags/importtag/imports-null.jinja"), @@ -95,6 +77,12 @@ public void itHandlesNullImportedValues() throws IOException { @Test public void importedContextExposesVars() { + jinjava.setResourceLocator((fullName, encoding, interpreter) -> + Resources.toString( + Resources.getResource(String.format("tags/macrotag/%s", fullName)), + StandardCharsets.UTF_8 + ) + ); assertThat(fixture("import")) .contains("wrap-padding: padding-left:42px;padding-right:42px"); } @@ -104,8 +92,6 @@ public void importedContextExposesVars() { // subsequent uses of any variables defined in the imported template are marked as deferred @Test public void itDefersImportedVariableKey() { - Jinjava jinjava = new Jinjava(); - interpreter = new JinjavaInterpreter(jinjava, context, jinjava.getGlobalConfig()); interpreter.getContext().put("primary_font_size_num", DeferredValue.instance()); fixture("import-property"); assertThat(interpreter.getContext().get("pegasus")).isInstanceOf(DeferredValue.class); @@ -120,8 +106,6 @@ public void itDefersImportedVariableKey() { @Test public void itDefersGloballyImportedVariables() { - Jinjava jinjava = new Jinjava(); - interpreter = new JinjavaInterpreter(jinjava, context, jinjava.getGlobalConfig()); interpreter.getContext().put("primary_font_size_num", DeferredValue.instance()); fixture("import-property-global"); assertThat(interpreter.getContext().get("primary_line_height")) @@ -130,8 +114,6 @@ public void itDefersGloballyImportedVariables() { @Test public void itReconstructsDeferredImportTag() { - Jinjava jinjava = new Jinjava(); - interpreter = new JinjavaInterpreter(jinjava, context, jinjava.getGlobalConfig()); interpreter.getContext().put("primary_font_size_num", DeferredValue.instance()); String renderedImport = fixture("import-property"); assertThat(renderedImport) @@ -141,8 +123,6 @@ public void itReconstructsDeferredImportTag() { @Test public void itDoesNotRenderTagsDependingOnDeferredImport() { try { - Jinjava jinjava = new Jinjava(); - interpreter = new JinjavaInterpreter(jinjava, context, jinjava.getGlobalConfig()); interpreter.getContext().put("primary_font_size_num", DeferredValue.instance()); String renderedImport = fixture("import-property-global"); assertThat(renderedImport) @@ -160,8 +140,6 @@ public void itDoesNotRenderTagsDependingOnDeferredImport() { @Test public void itDoesNotRenderTagsDependingOnDeferredGlobalImport() { try { - Jinjava jinjava = new Jinjava(); - interpreter = new JinjavaInterpreter(jinjava, context, jinjava.getGlobalConfig()); interpreter.getContext().put("primary_font_size_num", DeferredValue.instance()); String renderedImport = fixture("import-property"); assertThat(renderedImport) @@ -178,8 +156,6 @@ public void itDoesNotRenderTagsDependingOnDeferredGlobalImport() { @Test public void itAddsAllDeferredNodesOfImport() { - Jinjava jinjava = new Jinjava(); - interpreter = new JinjavaInterpreter(jinjava, context, jinjava.getGlobalConfig()); interpreter.getContext().put("primary_font_size_num", DeferredValue.instance()); fixture("import-property"); Set deferredImages = interpreter @@ -189,18 +165,16 @@ public void itAddsAllDeferredNodesOfImport() { .map(Node::reconstructImage) .collect(Collectors.toSet()); assertThat( - deferredImages - .stream() - .filter(image -> image.contains("{% set primary_line_height")) - .collect(Collectors.toSet()) - ) + deferredImages + .stream() + .filter(image -> image.contains("{% set primary_line_height")) + .collect(Collectors.toSet()) + ) .isNotEmpty(); } @Test public void itAddsAllDeferredNodesOfGlobalImport() { - Jinjava jinjava = new Jinjava(); - interpreter = new JinjavaInterpreter(jinjava, context, jinjava.getGlobalConfig()); interpreter.getContext().put("primary_font_size_num", DeferredValue.instance()); fixture("import-property-global"); Set deferredImages = interpreter @@ -210,16 +184,22 @@ public void itAddsAllDeferredNodesOfGlobalImport() { .map(Node::reconstructImage) .collect(Collectors.toSet()); assertThat( - deferredImages - .stream() - .filter(image -> image.contains("{% set primary_line_height")) - .collect(Collectors.toSet()) - ) + deferredImages + .stream() + .filter(image -> image.contains("{% set primary_line_height")) + .collect(Collectors.toSet()) + ) .isNotEmpty(); } @Test public void importedContextExposesMacros() { + jinjava.setResourceLocator((fullName, encoding, interpreter) -> + Resources.toString( + Resources.getResource(String.format("tags/macrotag/%s", fullName)), + StandardCharsets.UTF_8 + ) + ); assertThat(fixture("import")).contains(""); MacroFunction fn = (MacroFunction) interpreter.resolveObject( "pegasus.spacer", @@ -233,12 +213,24 @@ public void importedContextExposesMacros() { @Test public void importedContextDoesntExposePrivateMacros() { + jinjava.setResourceLocator((fullName, encoding, interpreter) -> + Resources.toString( + Resources.getResource(String.format("tags/macrotag/%s", fullName)), + StandardCharsets.UTF_8 + ) + ); fixture("import"); assertThat(context.get("_private")).isNull(); } @Test public void importedContextFnsProperlyResolveScopedVars() { + jinjava.setResourceLocator((fullName, encoding, interpreter) -> + Resources.toString( + Resources.getResource(String.format("tags/macrotag/%s", fullName)), + StandardCharsets.UTF_8 + ) + ); String result = fixture("imports-macro-referencing-macro"); assertThat(interpreter.getErrorsCopy()).isEmpty(); @@ -250,9 +242,6 @@ public void importedContextFnsProperlyResolveScopedVars() { @Test public void itImportsMacroWithCall() throws IOException { - Jinjava jinjava = new Jinjava(); - interpreter = new JinjavaInterpreter(jinjava, context, jinjava.getGlobalConfig()); - String renderResult = interpreter.render( Resources.toString( Resources.getResource("tags/importtag/imports-macro.jinja"), @@ -265,7 +254,6 @@ public void itImportsMacroWithCall() throws IOException { @Test public void itImportsMacroViaRelativePathWithCall() throws IOException { - Jinjava jinjava = new Jinjava(); jinjava.setResourceLocator( new ResourceLocator() { private RelativePathResolver relativePathResolver = new RelativePathResolver(); @@ -275,8 +263,7 @@ public String getString( String fullName, Charset encoding, JinjavaInterpreter interpreter - ) - throws IOException { + ) throws IOException { return Resources.toString( Resources.getResource(String.format("%s", fullName)), StandardCharsets.UTF_8 @@ -305,7 +292,6 @@ public Optional getLocationResolver() { @Test public void itSetsErrorLineNumbersCorrectly() throws IOException { - Jinjava jinjava = new Jinjava(); RenderResult result = jinjava.renderForResult( Resources.toString( Resources.getResource("tags/importtag/errors/base.jinja"), @@ -327,7 +313,6 @@ public void itSetsErrorLineNumbersCorrectly() throws IOException { @Test public void itSetsErrorLineNumbersCorrectlyThroughIncludeTag() throws IOException { - Jinjava jinjava = new Jinjava(); RenderResult result = jinjava.renderForResult( Resources.toString( Resources.getResource("tags/importtag/errors/include.jinja"), @@ -349,7 +334,6 @@ public void itSetsErrorLineNumbersCorrectlyThroughIncludeTag() throws IOExceptio @Test public void itSetsErrorLineNumbersCorrectlyForImportedMacros() throws IOException { - Jinjava jinjava = new Jinjava(); RenderResult result = jinjava.renderForResult( Resources.toString( Resources.getResource("tags/importtag/errors/import-macro.jinja"), @@ -371,12 +355,18 @@ public void itSetsErrorLineNumbersCorrectlyForImportedMacros() throws IOExceptio @Test public void itCorrectlySetsNestedPaths() { + jinjava.setResourceLocator((fullName, encoding, interpreter) -> + Resources.toString( + Resources.getResource(String.format("tags/macrotag/%s", fullName)), + StandardCharsets.UTF_8 + ) + ); context.put("foo", "foo"); assertThat( - interpreter.render( - "{% import 'double-import-macro.jinja' %}{{ print_path_macro2(foo) }}" - ) + interpreter.render( + "{% import 'double-import-macro.jinja' %}{{ print_path_macro2(foo) }}" ) + ) .isEqualTo("double-import-macro.jinja\n\nimport-macro.jinja\nfoo\n"); } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/IncludeTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/IncludeTagTest.java index c724a14a0..788c186f6 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/IncludeTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/IncludeTagTest.java @@ -31,7 +31,7 @@ public void itAvoidsSimpleIncludeCycles() throws IOException { ), new HashMap() ); - assertThat(result).containsSequence("hello world", "hello world"); + assertThat(result).containsSubsequence("hello world", "hello world"); } @Test @@ -43,7 +43,7 @@ public void itAvoidsNestedIncludeCycles() throws IOException { ), new HashMap() ); - assertThat(result).containsSequence("A", "B"); + assertThat(result).containsSubsequence("A", "B"); } @Test @@ -126,8 +126,7 @@ public String getString( String fullName, Charset encoding, JinjavaInterpreter interpreter - ) - throws IOException { + ) throws IOException { return Resources.toString( Resources.getResource(String.format("%s", fullName)), StandardCharsets.UTF_8 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..0d669f5ca 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/MacroTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/MacroTagTest.java @@ -7,8 +7,8 @@ import com.google.common.collect.Lists; import com.google.common.io.Resources; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; -import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; @@ -60,11 +60,11 @@ public void testFnWithArgs() { assertThat(fn.getArguments()).containsExactly("link", "text"); assertThat( - snippet("{{section_link('mylink', 'mytext')}}") - .render(interpreter) - .getValue() - .trim() - ) + snippet("{{section_link('mylink', 'mytext')}}") + .render(interpreter) + .getValue() + .trim() + ) .isEqualTo("link: mylink, text: mytext"); } @@ -83,11 +83,8 @@ public void testFnWithDeferredArgs() { interpreter.getContext().put("mylink", DeferredValue.instance()); assertThat( - snippet("{{section_link(mylink, 'mytext')}}") - .render(interpreter) - .getValue() - .trim() - ) + snippet("{{section_link(mylink, 'mytext')}}").render(interpreter).getValue().trim() + ) .isEqualTo("{{section_link(mylink, 'mytext')}}"); } @@ -120,20 +117,20 @@ public void testFnWithArgsWithDefVals() { assertThat(fn.getDefaults()).contains(entry("last", false)); assertThat( - snippet("{{ article('mytitle','mythumb','mylink','mysummary') }}") - .render(interpreter) - .getValue() - .trim() - ) + snippet("{{ article('mytitle','mythumb','mylink','mysummary') }}") + .render(interpreter) + .getValue() + .trim() + ) .isEqualTo( "title: mytitle, thumb: mythumb, link: mylink, summary: mysummary, last: false" ); assertThat( - snippet("{{ article('mytitle','mythumb','mylink','mysummary', last=true) }}") - .render(interpreter) - .getValue() - .trim() - ) + snippet("{{ article('mytitle','mythumb','mylink','mysummary', last=true) }}") + .render(interpreter) + .getValue() + .trim() + ) .isEqualTo( "title: mytitle, thumb: mythumb, link: mylink, summary: mysummary, last: true" ); @@ -212,8 +209,10 @@ public void itAllowsMacrosCallingMacrosUsingCall() throws IOException { public void itAllowsMacroRecursionWhenEnabledInConfiguration() throws IOException { // I need a different configuration here therefore interpreter = - new Jinjava(JinjavaConfig.newBuilder().withEnableRecursiveMacroCalls(true).build()) - .newInterpreter(); + new Jinjava( + BaseJinjavaTest.newConfigBuilder().withEnableRecursiveMacroCalls(true).build() + ) + .newInterpreter(); JinjavaInterpreter.pushCurrent(interpreter); try { @@ -231,13 +230,13 @@ public void itAllowsMacroRecursionWhenEnabledInConfiguration() throws IOExceptio public void itAllowsMacroRecursionWithMaxDepth() throws IOException { interpreter = new Jinjava( - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withEnableRecursiveMacroCalls(true) .withMaxMacroRecursionDepth(10) .build() ) - .newInterpreter(); + .newInterpreter(); JinjavaInterpreter.pushCurrent(interpreter); try { @@ -254,14 +253,14 @@ public void itAllowsMacroRecursionWithMaxDepth() throws IOException { public void itAllowsMacroRecursionWithMaxDepthInValidationMode() throws IOException { interpreter = new Jinjava( - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withEnableRecursiveMacroCalls(true) .withMaxMacroRecursionDepth(10) .withValidationMode(true) .build() ) - .newInterpreter(); + .newInterpreter(); JinjavaInterpreter.pushCurrent(interpreter); try { @@ -278,45 +277,48 @@ public void itAllowsMacroRecursionWithMaxDepthInValidationMode() throws IOExcept public void itEnforcesMacroRecursionWithMaxDepth() throws IOException { interpreter = new Jinjava( - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withEnableRecursiveMacroCalls(true) .withMaxMacroRecursionDepth(2) .build() ) - .newInterpreter(); - JinjavaInterpreter.pushCurrent(interpreter); - - try { + .newInterpreter(); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { String template = fixtureText("ending-recursion"); String out = interpreter.render(template); assertThat(interpreter.getErrorsCopy().get(0).getMessage()) .contains("Max recursion limit of 2 reached for macro 'hello'"); assertThat(out).contains("Hello Hello"); - } finally { - JinjavaInterpreter.popCurrent(); } } @Test public void itPreventsRecursionForMacroWithVar() { - String jinja = - "{%- macro func(var) %}" + - "{%- for f in var %}" + - "{{ f.val }}" + - "{%- endfor %}" + - "{%- endmacro %}" + - "{%- set var = {" + - " 'f' : {" + - " 'val': '{{ self }}'," + - " }" + - "} %}" + - "{% set self='{{var}}' %}" + - "{{ func(var) }}" + - ""; - Node node = new TreeParser(interpreter, jinja).buildTree(); - assertThat(interpreter.render(node)) - .isEqualTo("{'f': {'val': '{'f': {'val': '{{ self }}'}}'}}"); + interpreter = + new Jinjava( + BaseJinjavaTest.newConfigBuilder().withNestedInterpretationEnabled(true).build() + ) + .newInterpreter(); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + String jinja = + "{%- macro func(var) %}" + + "{%- for k,f in var.items() %}" + + "{{ f.val }}" + + "{%- endfor %}" + + "{%- endmacro %}" + + "{%- set var = {" + + " 'f' : {" + + " 'val': '{{ self }}'," + + " }" + + "} %}" + + "{% set self='{{var}}' %}" + + "{{ func(var) }}" + + ""; + Node node = new TreeParser(interpreter, jinja).buildTree(); + assertThat(interpreter.render(node)) + .isEqualTo("{'f': {'val': '{'f': {'val': '{{ self }}'} }'} }"); + } } @Test @@ -349,13 +351,13 @@ public void itReconstructsMacroDefinitionFromMacroFunctionWithNoTrim() { public void itCorrectlyScopesNestedMacroTags() { interpreter = new Jinjava( - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withEnableRecursiveMacroCalls(true) .withMaxMacroRecursionDepth(2) .build() ) - .newInterpreter(); + .newInterpreter(); JinjavaInterpreter.pushCurrent(interpreter); try { String result = interpreter.render(fixtureText("scoping")); @@ -371,6 +373,32 @@ public void itCorrectlyScopesNestedMacroTags() { } } + @Test + public void itCallsMacroInTernaryWithVariableCondition() { + String template = + "{% macro greet(name) %}Hello {{ name }}{% endmacro %}" + + "{{ greet('world') if myVar else greet('nobody') }}"; + + context.put("myVar", true); + assertThat(jinjava.render(template, context).trim()).isEqualTo("Hello world"); + + context.put("myVar", false); + assertThat(jinjava.render(template, context).trim()).isEqualTo("Hello nobody"); + } + + @Test + public void itCallsMacroInStandardTernaryWithVariableCondition() { + String template = + "{% macro greet(name) %}Hello {{ name }}{% endmacro %}" + + "{{ myVar ? greet('world') : greet('nobody') }}"; + + context.put("myVar", true); + assertThat(jinjava.render(template, context).trim()).isEqualTo("Hello world"); + + context.put("myVar", false); + assertThat(jinjava.render(template, context).trim()).isEqualTo("Hello nobody"); + } + private Node snippet(String jinja) { return new TreeParser(interpreter, jinja).buildTree().getChildren().getFirst(); } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/RawTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/RawTagTest.java index d2349f203..542d20e73 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/RawTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/RawTagTest.java @@ -4,7 +4,7 @@ import com.google.common.io.Resources; import com.hubspot.jinjava.BaseInterpretingTest; -import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; @@ -21,6 +21,7 @@ import org.junit.Test; public class RawTagTest extends BaseInterpretingTest { + Tag tag; @Before @@ -99,8 +100,8 @@ public void itPreservesRawTags() { JinjavaInterpreter preserveInterpreter = new JinjavaInterpreter( jinjava, jinjava.getGlobalContextCopy(), - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(PreserveRawExecutionMode.instance()) .build() ); @@ -124,8 +125,8 @@ public void itOverridesRawTagPreservation() { JinjavaInterpreter preserveInterpreter = new JinjavaInterpreter( jinjava, jinjava.getGlobalContextCopy(), - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(PreserveRawExecutionMode.instance()) .build() ); @@ -149,8 +150,8 @@ public void ifFixesSpacingWithRawTagPreservation() { JinjavaInterpreter preserveInterpreter = new JinjavaInterpreter( jinjava, jinjava.getGlobalContextCopy(), - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(PreserveRawExecutionMode.instance()) .build() ); @@ -165,8 +166,8 @@ public void itPreservesDeferredWhilePreservingRawTags() { JinjavaInterpreter preserveInterpreter = new JinjavaInterpreter( jinjava, jinjava.getGlobalContextCopy(), - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(PreserveRawExecutionMode.instance()) .build() ); diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/SetTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/SetTagTest.java index 3dc036cad..5d4650a63 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/SetTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/SetTagTest.java @@ -23,6 +23,7 @@ @SuppressWarnings("unchecked") public class SetTagTest extends BaseInterpretingTest { + public Tag tag; @Before @@ -299,6 +300,25 @@ public void itSetsBlockWithFilter() { assertThat(result).isEqualTo("BAR"); } + @Test + public void itRunsSetBlockInAChildScope() { + String template = + "{% set bar = 1 %}{% set foo %}{% set bar = 2 %}{% endset %}{{ bar }}"; + final String result = interpreter.render(template); + + assertThat(result).isEqualTo("1"); + } + + @Test + public void itDoesNotRunSetBlockInAChildScopeForIgnoredVariableName() { + // This is to preserve legacy behaviour used in Eager Execution + String template = + "{% set bar = 1 %}{% set __ignored__ %}{% set bar = 2 %}{% endset %}{{ bar }}"; + final String result = interpreter.render(template); + + assertThat(result).isEqualTo("2"); + } + private Node fixture(String name) { try { return new TreeParser( diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/TagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/TagTest.java index 3db11962c..fc76db597 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/TagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/TagTest.java @@ -28,6 +28,7 @@ import org.junit.Test; public class TagTest { + Jinjava jinjava; String script; diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/UnlessTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/UnlessTagTest.java index 7e314a799..877e9498a 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/UnlessTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/UnlessTagTest.java @@ -12,6 +12,7 @@ import org.junit.Test; public class UnlessTagTest extends BaseInterpretingTest { + public Tag tag; @Before 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..3e0b1438f 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/ValidationModeTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/ValidationModeTest.java @@ -3,13 +3,15 @@ import static org.assertj.core.api.Assertions.assertThat; import com.google.common.collect.ImmutableList; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; -import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.lib.filter.Filter; import com.hubspot.jinjava.lib.fn.ELFunctionDefinition; import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.testobjects.ValidationModeTestObjects; +import com.hubspot.jinjava.testobjects.ValidationModeTestObjects; import com.hubspot.jinjava.tree.Node; import com.hubspot.jinjava.tree.TextNode; import com.hubspot.jinjava.tree.parse.DefaultTokenScannerSymbols; @@ -17,40 +19,21 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class ValidationModeTest { + JinjavaInterpreter interpreter; JinjavaInterpreter validatingInterpreter; Jinjava jinjava; private Context context; - ValidationFilter validationFilter; - - class ValidationFilter implements Filter { - private int executionCount = 0; - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - executionCount++; - return var; - } - - public int getExecutionCount() { - return executionCount; - } - - @Override - public String getName() { - return "validation_filter"; - } - } + ValidationModeTestObjects.ValidationFilter validationFilter; private static int functionExecutionCount = 0; @@ -60,7 +43,7 @@ public static int validationTestFunction() { @Before public void setup() { - validationFilter = new ValidationFilter(); + validationFilter = new ValidationModeTestObjects.ValidationFilter(); ELFunctionDefinition validationFunction = new ELFunctionDefinition( "", @@ -69,7 +52,7 @@ public void setup() { "validationTestFunction" ); - jinjava = new Jinjava(); + jinjava = new Jinjava(BaseJinjavaTest.newConfigBuilder().build()); jinjava.getGlobalContext().registerFilter(validationFilter); jinjava.getGlobalContext().registerFunction(validationFunction); interpreter = jinjava.newInterpreter(); @@ -79,15 +62,12 @@ public void setup() { new JinjavaInterpreter( jinjava, context, - JinjavaConfig.newBuilder().withValidationMode(true).build() + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides(LegacyOverrides.NONE) + .withValidationMode(true) + .build() ); - - JinjavaInterpreter.pushCurrent(interpreter); - } - - @After - public void tearDown() { - JinjavaInterpreter.popCurrent(); } @Test @@ -114,7 +94,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 +110,7 @@ public void itResolvesNestedForExpressionsInValidationMode() { "done" ); - assertThat(validatingInterpreter.getErrors().size()).isEqualTo(4); + assertThat(validatingInterpreter.getErrors().size()).isEqualTo(2); assertThat(output.trim()).isEqualTo("done"); } @@ -235,6 +215,7 @@ public void itDoesNotPrintValuesInNestedValidatedBlocks() { } private class InstrumentedMacroFunction extends MacroFunction { + private int invocationCount = 0; InstrumentedMacroFunction( @@ -275,17 +256,19 @@ public void itDoesNotExecuteMacrosInValidatedBlocks() { interpreter.getContext() ); interpreter.getContext().addGlobalMacro(macro); - String template = "{{ hello() }}" + "{% if false %} " + " {{ hello() }}" + "{% endif %}"; - assertThat(interpreter.getErrors()).isEmpty(); - assertThat(interpreter.render(template).trim()).isEqualTo("hello"); - assertThat(macro.getInvocationCount()).isEqualTo(1); - - assertThat(validatingInterpreter.render(template).trim()).isEqualTo("hello"); - assertThat(macro.getInvocationCount()).isEqualTo(3); - assertThat(validatingInterpreter.getErrors()).isEmpty(); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + assertThat(interpreter.getErrors()).isEmpty(); + assertThat(interpreter.render(template).trim()).isEqualTo("hello"); + assertThat(macro.getInvocationCount()).isEqualTo(1); + } + try (var a = JinjavaInterpreter.closeablePushCurrent(validatingInterpreter).get()) { + assertThat(validatingInterpreter.render(template).trim()).isEqualTo("hello"); + assertThat(macro.getInvocationCount()).isEqualTo(3); + assertThat(validatingInterpreter.getErrors()).isEmpty(); + } } @Test @@ -330,13 +313,13 @@ public void itDoesNotExecuteFiltersInValidatedBlocks() { assertThat(result).isEqualTo("10"); assertThat(validationFilter.getExecutionCount()).isEqualTo(1); - JinjavaInterpreter.pushCurrent(validatingInterpreter); - result = validatingInterpreter.render(template).trim(); + try (var a = JinjavaInterpreter.closeablePushCurrent(validatingInterpreter).get()) { + result = validatingInterpreter.render(template).trim(); - assertThat(validatingInterpreter.getErrors().size()).isEqualTo(1); - assertThat(validatingInterpreter.getErrors().get(0).getMessage()).contains("hey("); - assertThat(result).isEqualTo("10"); - assertThat(validationFilter.getExecutionCount()).isEqualTo(2); - JinjavaInterpreter.popCurrent(); + assertThat(validatingInterpreter.getErrors().size()).isEqualTo(1); + assertThat(validatingInterpreter.getErrors().get(0).getMessage()).contains("hey("); + assertThat(result).isEqualTo("10"); + assertThat(validationFilter.getExecutionCount()).isEqualTo(2); + } } } 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..0f4f84ad0 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,15 +1,23 @@ package com.hubspot.jinjava.lib.tag.eager; -import com.hubspot.jinjava.JinjavaConfig; +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseJinjavaTest; 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 com.hubspot.jinjava.tree.parse.TagToken; +import java.util.List; +import java.util.Optional; 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; private Tag tag; @@ -19,8 +27,8 @@ public void eagerSetup() { new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withMaxOutputSize(MAX_OUTPUT_SIZE) .withExecutionMode(EagerExecutionMode.instance()) .withLegacyOverrides( @@ -31,6 +39,7 @@ public void eagerSetup() { tag = new EagerCycleTag(); context.registerTag(tag); + context.put("deferred", DeferredValue.instance()); context.registerTag(new EagerForTag()); JinjavaInterpreter.pushCurrent(interpreter); } @@ -39,4 +48,40 @@ public void eagerSetup() { public void teardown() { JinjavaInterpreter.popCurrent(); } + + @Test + public void itAddCycleTagAsADeferredToken() { + 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' %}"); + } + + @Test + public void itHandlesDeferredCycle() { + String template = + "{% set l = [] %}{% for item in deferred %}{% cycle l.append(deferred),5 %}{% endfor %}{{ l }}"; + assertThat(interpreter.render(template)).isEqualTo(template); + } + + @Test + public void iitHandlesEscapedQuotesInVariable() { + String template = + "{% set class = \"class='foo bar'\" %}{% for item in deferred %}{% cycle 'item-1',class %}.{% endfor %}"; + String firstPass = interpreter.render(template); + assertThat(firstPass) + .isEqualTo( + "{% for item in deferred %}{% cycle 'item-1','class=\\'foo bar\\'' %}.{% endfor %}" + ); + interpreter.getContext().put("deferred", List.of(0, 1)); + String secondPass = interpreter.render(firstPass); + assertThat(secondPass).isEqualTo("item-1.class='foo bar'."); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerDoTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerDoTagTest.java index 1bfb629ef..6ef77d431 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerDoTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerDoTagTest.java @@ -2,8 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.ExpectedNodeInterpreter; -import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; @@ -16,6 +16,7 @@ import org.junit.Test; public class EagerDoTagTest extends DoTagTest { + private static final long MAX_OUTPUT_SIZE = 500L; private Tag tag; private ExpectedNodeInterpreter expectedNodeInterpreter; @@ -26,8 +27,8 @@ public void eagerSetup() { new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withMaxOutputSize(MAX_OUTPUT_SIZE) .withExecutionMode(EagerExecutionMode.instance()) .withLegacyOverrides( diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerExtendsTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerExtendsTagTest.java index 12459d308..93d6c4916 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerExtendsTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerExtendsTagTest.java @@ -2,12 +2,13 @@ import static org.assertj.core.api.Java6Assertions.assertThat; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.ExpectedTemplateInterpreter; -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.ExtendsTagTest; +import com.hubspot.jinjava.loader.RelativePathResolver; import com.hubspot.jinjava.mode.EagerExecutionMode; import java.io.IOException; import org.junit.After; @@ -16,16 +17,23 @@ import org.junit.Test; public class EagerExtendsTagTest extends ExtendsTagTest { + private ExpectedTemplateInterpreter expectedTemplateInterpreter; @Before public void eagerSetup() { + eagerSetup(false); + } + + void eagerSetup(boolean nestedInterpretation) { + JinjavaInterpreter.popCurrent(); interpreter = new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() + .withNestedInterpretationEnabled(nestedInterpretation) .withExecutionMode(EagerExecutionMode.instance()) .withLegacyOverrides( LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() @@ -45,7 +53,9 @@ public void teardown() { @Test public void itDefersBlockInExtendsChild() { - expectedTemplateInterpreter.assertExpectedOutput("defers-block-in-extends-child"); + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "defers-block-in-extends-child" + ); } @Test @@ -54,24 +64,49 @@ public void itDefersBlockInExtendsChildSecondPass() { expectedTemplateInterpreter.assertExpectedOutput( "defers-block-in-extends-child.expected" ); + context.remove(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "defers-block-in-extends-child.expected" + ); } @Test public void itDefersSuperBlockWithDeferred() { - expectedTemplateInterpreter.assertExpectedOutput("defers-super-block-with-deferred"); + eagerSetup(true); + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "defers-super-block-with-deferred" + ); } @Test public void itDefersSuperBlockWithDeferredSecondPass() { + eagerSetup(true); context.put("deferred", "Resolved now"); - expectedTemplateInterpreter.assertExpectedOutput( + expectedTemplateInterpreter.assertExpectedNonEagerOutput( "defers-super-block-with-deferred.expected" ); } @Test - public void itReconstructsDeferredOutsideBlock() { + public void itDefersSuperBlockWithDeferredNestedInterp() { + eagerSetup(true); + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "defers-super-block-with-deferred-nested-interp" + ); + } + + @Test + public void itDefersSuperBlockWithDeferredNestedInterpSecondPass() { + eagerSetup(true); + context.put("deferred", "Resolved now"); expectedTemplateInterpreter.assertExpectedOutput( + "defers-super-block-with-deferred-nested-interp.expected" + ); + } + + @Test + public void itReconstructsDeferredOutsideBlock() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( "reconstructs-deferred-outside-block" ); } @@ -83,6 +118,10 @@ public void itReconstructsDeferredOutsideBlockSecondPass() { expectedTemplateInterpreter.assertExpectedOutput( "reconstructs-deferred-outside-block.expected" ); + context.remove(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY); + expectedTemplateInterpreter.assertExpectedNonEagerOutput( + "reconstructs-deferred-outside-block.expected" + ); } @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 9c44dcf35..ed256fda3 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,24 +1,24 @@ 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.BaseJinjavaTest; import com.hubspot.jinjava.ExpectedNodeInterpreter; -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.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.List; import java.util.Optional; import org.junit.After; import org.junit.Before; import org.junit.Test; public class EagerForTagTest extends ForTagTest { + private static final long MAX_OUTPUT_SIZE = 5000L; private ExpectedNodeInterpreter expectedNodeInterpreter; @@ -28,12 +28,16 @@ public void eagerSetup() { new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withMaxOutputSize(MAX_OUTPUT_SIZE) .withExecutionMode(EagerExecutionMode.instance()) .withLegacyOverrides( - LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() + LegacyOverrides + .newBuilder() + .withUsePyishObjectMapper(true) + .withKeepNullableLoopValues(true) + .build() ) .build() ); @@ -59,8 +63,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"); } @@ -74,8 +77,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"); } @@ -87,15 +89,37 @@ 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.getErrors()).hasSize(1); - assertThat(interpreter.getErrors().get(0).getReason()) - .isEqualTo(ErrorReason.OUTPUT_TOO_BIG); + assertThat(interpreter.getContext().getDeferredTokens()).hasSize(1); + } + + @Test + public void itUsesDeferredExecutionModeWhenChildrenAreLarge() { + assertThat( + interpreter.render( + String.format( + "{%% for item in range(%d) %%}1234567890{%% endfor %%}", + MAX_OUTPUT_SIZE / 10 - 1 + ) + ) + ) + .hasSize((int) MAX_OUTPUT_SIZE - 10); + 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 @@ -151,7 +175,7 @@ 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" + @@ -197,25 +221,41 @@ public void itDefersLoopVariable() { } @Test - public void itDoesNotSwallowDeferredValueException() { + public void itCanNowHandleModificationInPartiallyDeferredLoop() { interpreter.getContext().registerTag(new EagerDoTag()); interpreter.getContext().registerTag(new EagerIfTag()); interpreter.getContext().registerTag(new EagerSetTag()); String input = "{% set my_list = [] %}" + - "{% for i in range(30) %}" + - "{{ my_list.append(i) }}" + + "{% for i in range(401) %}" + + "{% do 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) %}" + + "{% do my_list.append(i) %}" + "{% endif %}" + "{% endfor %}" + "{% endfor %}" + "{{ my_list }}"; - interpreter.render(input); - assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); + String initialResult = interpreter.render(input); + assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); + interpreter.getContext().put("deferred", ImmutableList.of(1, 2)); + interpreter.render(initialResult); + assertThat(interpreter.getContext().get("my_list")).isInstanceOf(List.class); + assertThat((List) interpreter.getContext().get("my_list")) + .as( + "Appends 401 numbers and then appends '-1', running the 'i' loop twice," + + "which runs the 'j' loop, the first time appending the value of 'i', which will be '0', then '1'" + ) + .hasSize(404) + .containsSequence(400L, -1L, 0L, 1L); + assertThat(interpreter.getContext().getDeferredNodes()).isEmpty(); + } + + public static boolean inForLoop() { + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); + return interpreter.getContext().isInForLoop(); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTagTest.java index 0496b3ab3..dde89ecd7 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTagTest.java @@ -3,7 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.google.common.io.Resources; -import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.FromTag; @@ -35,8 +35,7 @@ public String getString( String fullName, Charset encoding, JinjavaInterpreter interpreter - ) - throws IOException { + ) throws IOException { return Resources.toString( Resources.getResource(String.format("tags/macrotag/%s", fullName)), StandardCharsets.UTF_8 @@ -54,8 +53,8 @@ public Optional getLocationResolver() { new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(EagerExecutionMode.instance()) .build() ); @@ -76,9 +75,37 @@ public void teardown() { public void itDefersWhenPathIsDeferred() { String input = "{% from deferred import foo %}"; String output = interpreter.render(input); - assertThat(output).isEqualTo("{% set current_path = null %}" + input); + assertThat(output).isEqualTo("{% set current_path = '' %}" + input); assertThat(interpreter.getContext().getGlobalMacro("foo")).isNotNull(); assertThat(interpreter.getContext().getGlobalMacro("foo").isDeferred()).isTrue(); + assertThat(interpreter.getContext().getDeferredTokens()) + .isNotEmpty() + .anySatisfy(deferredToken -> { + assertThat(deferredToken.getToken().getImage()) + .isEqualTo("{% from deferred import foo %}"); + assertThat(deferredToken.getSetDeferredWords()).containsExactly("foo"); + assertThat(deferredToken.getUsedDeferredWords()) + .containsExactlyInAnyOrder("deferred", "foo"); + }); + } + + @Test + public void itDefersWhenPathIsDeferredWithAlias() { + String input = "{% from deferred import foo as new_foo %}"; + String output = interpreter.render(input); + assertThat(output).isEqualTo("{% set current_path = '' %}" + input); + assertThat(interpreter.getContext().getGlobalMacro("new_foo")).isNotNull(); + assertThat(interpreter.getContext().getGlobalMacro("new_foo").isDeferred()).isTrue(); + assertThat(interpreter.getContext().getDeferredTokens()) + .isNotEmpty() + .anySatisfy(deferredToken -> { + assertThat(deferredToken.getToken().getImage()) + .isEqualTo("{% from deferred import foo as new_foo %}"); + assertThat(deferredToken.getSetDeferredWords()).containsExactly("new_foo"); + assertThat(deferredToken.getUsedDeferredWords()) + .containsExactlyInAnyOrder("deferred", "foo"); + }); + assertThat(interpreter.getContext().get("new_foo")).isInstanceOf(DeferredValue.class); } @Test 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..2c415f550 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 @@ -2,8 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.ExpectedNodeInterpreter; -import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.IfTagTest; @@ -15,6 +15,7 @@ import org.junit.Test; public class EagerIfTagTest extends IfTagTest { + private ExpectedNodeInterpreter expectedNodeInterpreter; @Before @@ -23,8 +24,8 @@ public void eagerSetup() { new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(EagerExecutionMode.instance()) .build() ); @@ -66,7 +67,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 +83,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 +99,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/EagerImportTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTagTest.java index 401e6c1c7..58de257ed 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTagTest.java @@ -2,24 +2,33 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.google.common.base.Strings; import com.google.common.io.Resources; -import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.lib.filter.Filter; +import com.hubspot.jinjava.interpret.RenderResult; import com.hubspot.jinjava.lib.tag.ImportTag; import com.hubspot.jinjava.lib.tag.ImportTagTest; import com.hubspot.jinjava.lib.tag.Tag; +import com.hubspot.jinjava.lib.tag.eager.importing.AliasedEagerImportingStrategy; +import com.hubspot.jinjava.lib.tag.eager.importing.EagerImportingStrategy; +import com.hubspot.jinjava.lib.tag.eager.importing.EagerImportingStrategyFactory; +import com.hubspot.jinjava.lib.tag.eager.importing.FlatEagerImportingStrategy; +import com.hubspot.jinjava.lib.tag.eager.importing.ImportingData; import com.hubspot.jinjava.loader.LocationResolver; import com.hubspot.jinjava.loader.RelativePathResolver; import com.hubspot.jinjava.loader.ResourceLocator; import com.hubspot.jinjava.mode.EagerExecutionMode; -import com.hubspot.jinjava.objects.collections.PyMap; +import com.hubspot.jinjava.testobjects.EagerImportTagTestObjects; +import com.hubspot.jinjava.tree.parse.DefaultTokenScannerSymbols; +import com.hubspot.jinjava.tree.parse.TagToken; import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; @@ -30,19 +39,22 @@ import org.junit.Test; public class EagerImportTagTest extends ImportTagTest { + private static final String CONTEXT_VAR = "context_var"; private static final String TEMPLATE_FILE = "template.jinja"; + private TagToken tagToken; + @Before public void eagerSetup() throws Exception { context.put("padding", 42); - context.registerFilter(new PrintPathFilter()); + context.registerFilter(new EagerImportTagTestObjects.PrintPathFilter()); interpreter = new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(EagerExecutionMode.instance()) .withLegacyOverrides( LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() @@ -57,6 +69,36 @@ public void eagerSetup() throws Exception { context.registerTag(tag); context.put("deferred", DeferredValue.instance()); JinjavaInterpreter.pushCurrent(interpreter); + tagToken = + new TagToken( + String.format("{%% import foo as %s %%}", CONTEXT_VAR), + 0, + 0, + new DefaultTokenScannerSymbols() + ); + } + + private AliasedEagerImportingStrategy getAliasedStrategy( + String alias, + JinjavaInterpreter parentInterpreter + ) { + ImportingData importingData = EagerImportingStrategyFactory.getImportingData( + tagToken, + parentInterpreter + ); + + return new AliasedEagerImportingStrategy(importingData, alias); + } + + private FlatEagerImportingStrategy getFlatStrategy( + JinjavaInterpreter parentInterpreter + ) { + ImportingData importingData = EagerImportingStrategyFactory.getImportingData( + tagToken, + parentInterpreter + ); + + return new FlatEagerImportingStrategy(importingData); } @After @@ -70,7 +112,7 @@ public void itRemovesKeysFromChildBindings() { Map childBindings = child.getContext().getSessionBindings(); assertThat(childBindings.get(Context.IMPORT_RESOURCE_ALIAS_KEY)) .isEqualTo(CONTEXT_VAR); - EagerImportTag.integrateChild(CONTEXT_VAR, childBindings, child, interpreter); + getAliasedStrategy(CONTEXT_VAR, interpreter).integrateChild(child); assertThat(interpreter.getContext().get(CONTEXT_VAR)).isInstanceOf(Map.class); assertThat(((Map) interpreter.getContext().get(CONTEXT_VAR)).keySet()) .doesNotContain(Context.IMPORT_RESOURCE_ALIAS_KEY); @@ -83,18 +125,8 @@ public void itHandlesMultiLayer() { JinjavaInterpreter child2 = getChildInterpreter(child, ""); child2.getContext().put("foo", "foo val"); child.getContext().put("bar", "bar val"); - EagerImportTag.integrateChild( - "", - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - "", - child.getContext().getSessionBindings(), - child, - interpreter - ); + getFlatStrategy(child).integrateChild(child2); + getFlatStrategy(interpreter).integrateChild(child); assertThat(interpreter.getContext().get("foo")).isEqualTo("foo val"); assertThat(interpreter.getContext().get("bar")).isEqualTo("bar val"); } @@ -109,129 +141,58 @@ public void itHandlesMultiLayerAliased() { child2.render("{% set foo = 'foo val' %}"); child.render("{% set bar = 'bar val' %}"); - EagerImportTag.integrateChild( - child2Alias, - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - CONTEXT_VAR, - child.getContext().getSessionBindings(), - child, - interpreter - ); + getAliasedStrategy(child2Alias, child).integrateChild(child2); + getAliasedStrategy(CONTEXT_VAR, interpreter).integrateChild(child); + assertThat(interpreter.getContext().get(CONTEXT_VAR)).isInstanceOf(Map.class); assertThat( - ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child2Alias) - ) + ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child2Alias) + ) .isInstanceOf(Map.class); assertThat( - ( - (Map) ( - (Map) interpreter.getContext().get(CONTEXT_VAR) - ).get(child2Alias) - ).get("foo") - ) + ((Map) ((Map) interpreter + .getContext() + .get(CONTEXT_VAR)).get(child2Alias)).get("foo") + ) .isEqualTo("foo val"); assertThat( - ((Map) interpreter.getContext().get(CONTEXT_VAR)).get("bar") - ) + ((Map) interpreter.getContext().get(CONTEXT_VAR)).get("bar") + ) .isEqualTo("bar val"); } @Test @SuppressWarnings("unchecked") public void itHandlesMultiLayerAliasedAndDeferred() { + setupResourceLocator(); String child2Alias = "double_child"; - JinjavaInterpreter child = getChildInterpreter(interpreter, CONTEXT_VAR); - JinjavaInterpreter child2 = getChildInterpreter(child, child2Alias); - - child2.render("{% set foo = 'foo val' %}"); - child.render("{% set bar = 'bar val' %}"); - child2.render("{% set foo_d = deferred %}"); - - EagerImportTag.integrateChild( - child2Alias, - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - CONTEXT_VAR, - child.getContext().getSessionBindings(), - child, - interpreter + RenderResult result = jinjava.renderForResult( + "{% import 'layer-one.jinja' as context_var %}", + new HashMap<>() ); - assertThat(interpreter.getContext().get(CONTEXT_VAR)).isInstanceOf(PyMap.class); - assertThat( - ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child2Alias) - ) - .isInstanceOf(DeferredValue.class); - assertThat( - ( - ( - (Map) ( - (DeferredValue) ( - (Map) (interpreter.getContext().get(CONTEXT_VAR)) - ).get(child2Alias) - ).getOriginalValue() - ).get("foo") - ) - ) - .isEqualTo("foo val"); - - assertThat( - (((Map) interpreter.getContext().get(CONTEXT_VAR)).get("bar")) - ) - .isEqualTo("bar val"); - } - - @Test - @SuppressWarnings("unchecked") - public void itHandlesMultiLayerAliasedAndNullDeferred() { - String child2Alias = "double_child"; - JinjavaInterpreter child = getChildInterpreter(interpreter, CONTEXT_VAR); - JinjavaInterpreter child2 = getChildInterpreter(child, child2Alias); - - child2.render("{% set foo = 'foo val' %}"); - child.render("{% set bar = 'bar val' %}"); - child2.render("{% set foo_d = deferred %}"); - EagerImportTag.integrateChild( - child2Alias, - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - CONTEXT_VAR, - child.getContext().getSessionBindings(), - child, - interpreter - ); - assertThat(interpreter.getContext().get(CONTEXT_VAR)).isInstanceOf(PyMap.class); + assertThat(result.getContext().get(CONTEXT_VAR)).isInstanceOf(DeferredValue.class); assertThat( - ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child2Alias) - ) + ((Map) ((DeferredValue) result + .getContext() + .get(CONTEXT_VAR)).getOriginalValue()).get(child2Alias) + ) .isInstanceOf(DeferredValue.class); assertThat( - ( - ( - (Map) ( - (DeferredValue) ( - (Map) interpreter.getContext().get(CONTEXT_VAR) - ).get(child2Alias) - ).getOriginalValue() - ).get("foo") - ) - ) + (((Map) ((DeferredValue) (((Map) ((DeferredValue) result + .getContext() + .get(CONTEXT_VAR)).getOriginalValue())).get( + child2Alias + )).getOriginalValue()).get("foo")) + ) .isEqualTo("foo val"); assertThat( - (((Map) interpreter.getContext().get(CONTEXT_VAR)).get("bar")) - ) + ((Map) ((DeferredValue) result + .getContext() + .get(CONTEXT_VAR)).getOriginalValue()).get("bar") + ) .isEqualTo("bar val"); } @@ -243,83 +204,56 @@ public void itHandlesMultiLayerDeferred() { child2.getContext().put("foo", DeferredValue.instance("foo val")); child.getContext().put("bar", DeferredValue.instance("bar val")); - EagerImportTag.integrateChild( - "", - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - "", - child.getContext().getSessionBindings(), - child, - interpreter - ); + getFlatStrategy(child).integrateChild(child2); + getFlatStrategy(interpreter).integrateChild(child); assertThat(interpreter.getContext().get("foo")).isInstanceOf(DeferredValue.class); assertThat( - (((DeferredValue) (interpreter.getContext().get("foo"))).getOriginalValue()) - ) + (((DeferredValue) (interpreter.getContext().get("foo"))).getOriginalValue()) + ) .isEqualTo("foo val"); assertThat(interpreter.getContext().get("bar")).isInstanceOf(DeferredValue.class); assertThat( - (((DeferredValue) (interpreter.getContext().get("bar"))).getOriginalValue()) - ) + (((DeferredValue) (interpreter.getContext().get("bar"))).getOriginalValue()) + ) .isEqualTo("bar val"); } @Test @SuppressWarnings("unchecked") public void itHandlesMultiLayerSomeAliased() { - String child2Alias = ""; String child3Alias = "triple_child"; JinjavaInterpreter child = getChildInterpreter(interpreter, CONTEXT_VAR); - JinjavaInterpreter child2 = getChildInterpreter(child, child2Alias); + JinjavaInterpreter child2 = getChildInterpreter(child, ""); JinjavaInterpreter child3 = getChildInterpreter(child2, child3Alias); child2.render("{% set foo = 'foo val' %}"); child.render("{% set bar = 'bar val' %}"); child3.render("{% set foobar = 'foobar val' %}"); - EagerImportTag.integrateChild( - child3Alias, - child3.getContext().getSessionBindings(), - child3, - child2 - ); - EagerImportTag.integrateChild( - child2Alias, - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - CONTEXT_VAR, - child.getContext().getSessionBindings(), - child, - interpreter - ); + getAliasedStrategy(child3Alias, child2).integrateChild(child3); + getFlatStrategy(child).integrateChild(child2); + getAliasedStrategy(CONTEXT_VAR, interpreter).integrateChild(child); + assertThat(interpreter.getContext().get(CONTEXT_VAR)).isInstanceOf(Map.class); assertThat( - ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child3Alias) - ) + ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child3Alias) + ) .isInstanceOf(Map.class); assertThat( - ( - (Map) ( - (Map) interpreter.getContext().get(CONTEXT_VAR) - ).get(child3Alias) - ).get("foobar") - ) + ((Map) ((Map) interpreter + .getContext() + .get(CONTEXT_VAR)).get(child3Alias)).get("foobar") + ) .isEqualTo("foobar val"); assertThat( - ((Map) interpreter.getContext().get(CONTEXT_VAR)).get("bar") - ) + ((Map) interpreter.getContext().get(CONTEXT_VAR)).get("bar") + ) .isEqualTo("bar val"); assertThat( - ((Map) interpreter.getContext().get(CONTEXT_VAR)).get("foo") - ) + ((Map) interpreter.getContext().get(CONTEXT_VAR)).get("foo") + ) .isEqualTo("foo val"); } @@ -337,55 +271,35 @@ public void itHandlesMultiLayerAliasedAndParallel() { child.render("{% set bar = 'bar val' %}"); child2B.render("{% set foo_b = 'foo_b val' %}"); - EagerImportTag.integrateChild( - child2Alias, - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - child2BAlias, - child2B.getContext().getSessionBindings(), - child2B, - child - ); - EagerImportTag.integrateChild( - CONTEXT_VAR, - child.getContext().getSessionBindings(), - child, - interpreter - ); + getAliasedStrategy(child2Alias, child).integrateChild(child2); + getAliasedStrategy(child2BAlias, child).integrateChild(child2B); + getAliasedStrategy(CONTEXT_VAR, interpreter).integrateChild(child); + assertThat(interpreter.getContext().get(CONTEXT_VAR)).isInstanceOf(Map.class); assertThat( - ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child2Alias) - ) + ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child2Alias) + ) .isInstanceOf(Map.class); assertThat( - ((Map) interpreter.getContext().get(CONTEXT_VAR)).get( - child2BAlias - ) - ) + ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child2BAlias) + ) .isInstanceOf(Map.class); assertThat( - ( - (Map) ( - (Map) interpreter.getContext().get(CONTEXT_VAR) - ).get(child2Alias) - ).get("foo") - ) + ((Map) ((Map) interpreter + .getContext() + .get(CONTEXT_VAR)).get(child2Alias)).get("foo") + ) .isEqualTo("foo val"); assertThat( - ( - (Map) ( - (Map) interpreter.getContext().get(CONTEXT_VAR) - ).get(child2BAlias) - ).get("foo_b") - ) + ((Map) ((Map) interpreter + .getContext() + .get(CONTEXT_VAR)).get(child2BAlias)).get("foo_b") + ) .isEqualTo("foo_b val"); assertThat( - ((Map) interpreter.getContext().get(CONTEXT_VAR)).get("bar") - ) + ((Map) interpreter.getContext().get(CONTEXT_VAR)).get("bar") + ) .isEqualTo("bar val"); } @@ -409,7 +323,7 @@ public void itDefersTripleLayer() { context.put("b_val", "b"); context.put("c_val", "c"); String result = interpreter.render( - "{% import 'import-tree-c.jinja' as c %}{{ c|dictsort(false, 'key') }}" + "{% import 'import-tree-c.jinja' as c %}{{ c.b|dictsort(false, 'key') }}{{ c.foo_b }}{{ c.import_resource_path }}" ); assertThat(interpreter.render("{{ c.b.a.foo_a }}")).isEqualTo("{{ c.b.a.foo_a }}"); assertThat(interpreter.render("{{ c.b.foo_b }}")).isEqualTo("{{ c.b.foo_b }}"); @@ -417,10 +331,11 @@ public void itDefersTripleLayer() { removeDeferredContextKeys(); context.put("a_val", "a"); // There are some extras due to deferred values copying up the context stack. + context.getDeferredTokens().clear(); assertThat(interpreter.render(result).trim()) .isEqualTo( interpreter.render( - "{% import 'import-tree-c.jinja' as c %}{{ c|dictsort(false, 'key') }}" + "{% import 'import-tree-c.jinja' as c %}{{ c.b|dictsort(false, 'key') }}{{ c.foo_b }}{{ c.import_resource_path }}" ) ); } @@ -449,6 +364,7 @@ public void itDefersQuadLayer() { removeDeferredContextKeys(); context.put("a_val", "a"); + context.getDeferredTokens().clear(); assertThat(interpreter.render(result).trim()).isEqualTo("12345 cbaabaaba"); } @@ -462,11 +378,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" + - "{% 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" + - "{% do b.update({'a': a,'foo_b': foo_b,'import_resource_path': 'import-tree-b.jinja'}) %}{% set current_path = '' %}{% endset %}{% endif %}" + "{% if deferred %}{% do %}{% set __temp_meta_current_path_930119534__,current_path = current_path,'import-tree-b.jinja' %}{% set __temp_meta_import_alias_98__ = {} %}{% for __ignored__ in [0] %}{% do %}{% set __temp_meta_current_path_58714367__,current_path = current_path,'import-tree-a.jinja' %}{% set __temp_meta_import_alias_95701__ = {} %}{% for __ignored__ in [0] %}{% set something = 'somn' %}{% do __temp_meta_import_alias_95701__.update({'something': something}) %}\n" + + "{% set foo_a = 'a' %}{% do __temp_meta_import_alias_95701__.update({'foo_a': foo_a}) %}\n" + + "{% do __temp_meta_import_alias_95701__.update({'foo_a': 'a','import_resource_path': 'import-tree-a.jinja','something': 'somn'}) %}{% endfor %}{% set a = __temp_meta_import_alias_95701__ %}{% set current_path,__temp_meta_current_path_58714367__ = __temp_meta_current_path_58714367__,null %}{% enddo %}\n" + + "{% set foo_b = 'b' + a.foo_a %}{% do __temp_meta_import_alias_98__.update({'foo_b': foo_b}) %}\n" + + "{% do __temp_meta_import_alias_98__.update({'a': a,'foo_b': foo_b,'import_resource_path': 'import-tree-b.jinja'}) %}{% endfor %}{% set b = __temp_meta_import_alias_98__ %}{% set current_path,__temp_meta_current_path_930119534__ = __temp_meta_current_path_930119534__,null %}{% enddo %}{% endif %}" ); removeDeferredContextKeys(); @@ -596,6 +512,18 @@ public void itKeepsDeferredImportAliasesInsideOwnScope() { assertThat(interpreter.render(result)).isEqualTo("A_resolved_A-B_resolved_B"); } + @Test + public void itKeepsDeferredImportAliasesInsideOwnScopeInSet() { + setupResourceLocator(); + String result = interpreter.render( + "{% import 'printer-a.jinja' as printer %}{% import 'intermediate-b.jinja' as inter %}" + + "{% set foo = printer.print(deferred) %}{% set bar = inter.print(deferred) %}" + + "{{ foo }}-{{ bar }}" + ); + context.put("deferred", "resolved"); + assertThat(interpreter.render(result)).isEqualTo("A_resolved_A-B_resolved_B"); + } + @Test public void itDefersWhenPathIsDeferred() { String input = "{% import deferred as foo %}"; @@ -636,10 +564,17 @@ public void itHandlesVarFromImportedMacro() { ); assertThat(result.trim()) .isEqualTo( - "{% set var = [] %}{% do var.append('a' ~ deferred) %}" + - "a\n" + + "{% set var = [] %}" + + "{% set __macro_adjust_108896029_temp_variable_0__ %}" + + "{% do var.append('a' ~ deferred) %}" + + "a" + + "{% endset %}" + + "{{ __macro_adjust_108896029_temp_variable_0__ }}\n" + + "{% set __macro_adjust_108896029_temp_variable_1__ %}" + "{% do var.append('b' ~ deferred) %}" + - "b\n" + + "b" + + "{% endset %}" + + "{{ __macro_adjust_108896029_temp_variable_1__ }}\n" + "c{{ var }}" ); context.put("deferred", "resolved"); @@ -675,7 +610,110 @@ public void itDoesNotSilentlyOverrideMacro() { assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); } - private static JinjavaInterpreter getChildInterpreter( + @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 %}" + + "{% do %}" + + "{% set __temp_meta_current_path_299252430__,current_path = current_path,'var-b.jinja' %}" + + "{% set __temp_meta_import_alias_3612204__ = {} %}" + + "{% for __ignored__ in [0] %}" + + "{% set foo = 'b' %}" + + "{% do __temp_meta_import_alias_3612204__.update({'foo': foo}) %}\n" + + "{% do __temp_meta_import_alias_3612204__.update({'foo': 'b','import_resource_path': 'var-b.jinja'}) %}" + + "{% endfor %}" + + "{% set vars = __temp_meta_import_alias_3612204__ %}" + + "{% set current_path,__temp_meta_current_path_299252430__ = __temp_meta_current_path_299252430__,null %}" + + "{% enddo %}" + + "{% endif %}" + + "{{ vars.foo }}" + ); + interpreter.getContext().put("deferred", "resolved"); + context.getDeferredTokens().clear(); + 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 %}" + + "{% do %}" + + "{% set __temp_meta_current_path_299252430__,current_path = current_path,'var-b.jinja' %}" + + "{% set foo = 'b' %}\n" + + "{% set current_path,__temp_meta_current_path_299252430__ = __temp_meta_current_path_299252430__,null %}" + + "{% enddo %}" + + "{% endif %}" + + "{{ foo }}" + ); + + interpreter.getContext().put("deferred", "resolved"); + context.getDeferredTokens().clear(); + 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( + "{% do %}" + + "{% set __temp_meta_current_path_549676938__,current_path = current_path,'set-two-variables.jinja' %}" + + "{% set foo = deferred %}\n" + + "\n" + + "{% set current_path,__temp_meta_current_path_549676938__ = __temp_meta_current_path_549676938__,null %}" + + "{% enddo %}" + + "{{ foo }} bar" + ); + } + + private JinjavaInterpreter getChildInterpreter( JinjavaInterpreter interpreter, String alias ) { @@ -684,7 +722,13 @@ private static JinjavaInterpreter getChildInterpreter( .getInterpreterFactory() .newInstance(interpreter); child.getContext().put(Context.IMPORT_RESOURCE_PATH_KEY, TEMPLATE_FILE); - EagerImportTag.setupImportAlias(alias, child, interpreter); + EagerImportingStrategy eagerImportingStrategy; + if (Strings.isNullOrEmpty(alias)) { + eagerImportingStrategy = getFlatStrategy(interpreter); + } else { + eagerImportingStrategy = getAliasedStrategy(alias, interpreter); + } + eagerImportingStrategy.setup(child); return child; } @@ -708,8 +752,7 @@ public String getString( String fullName, Charset encoding, JinjavaInterpreter interpreter - ) - throws IOException { + ) throws IOException { return Resources.toString( Resources.getResource(String.format("tags/eager/importtag/%s", fullName)), StandardCharsets.UTF_8 @@ -724,19 +767,6 @@ public Optional getLocationResolver() { ); } - public static class PrintPathFilter implements Filter { - - @Override - public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { - return interpreter.getContext().getCurrentPathStack().peek().orElse("/"); - } - - @Override - public String getName() { - return "print_path"; - } - } - @Test @Ignore @Override diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerIncludeTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerIncludeTagTest.java index ed360c99b..afd93de07 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerIncludeTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerIncludeTagTest.java @@ -3,8 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import com.google.common.io.Resources; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.ExpectedTemplateInterpreter; -import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.IncludeTagTest; @@ -23,6 +23,7 @@ import org.junit.Test; public class EagerIncludeTagTest extends IncludeTagTest { + private ExpectedTemplateInterpreter expectedTemplateInterpreter; @Before @@ -31,8 +32,8 @@ public void eagerSetup() { new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(EagerExecutionMode.instance()) .build() ); @@ -52,8 +53,7 @@ public String getString( String fullName, Charset encoding, JinjavaInterpreter interpreter - ) - throws IOException { + ) throws IOException { return Resources.toString( Resources.getResource(String.format("tags/eager/includetag/%s", fullName)), StandardCharsets.UTF_8 @@ -78,20 +78,20 @@ public void itIncludesDeferred() { setupResourceLocator(); expectedTemplateInterpreter.assertExpectedOutputNonIdempotent("includes-deferred"); assertThat( - context - .getDeferredTokens() - .stream() - .flatMap(deferredToken -> deferredToken.getUsedDeferredWords().stream()) - .collect(Collectors.toSet()) - ) + context + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getUsedDeferredWords().stream()) + .collect(Collectors.toSet()) + ) .containsExactlyInAnyOrder("foo", "deferred"); assertThat( - context - .getDeferredTokens() - .stream() - .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) - .collect(Collectors.toSet()) - ) + context + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) + .collect(Collectors.toSet()) + ) .containsExactlyInAnyOrder("foo"); } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagTest.java index 149062ac3..73f484d4d 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagTest.java @@ -2,8 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.ExpectedNodeInterpreter; -import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; @@ -18,6 +18,7 @@ import org.junit.Test; public class EagerSetTagTest extends SetTagTest { + private static final long MAX_OUTPUT_SIZE = 500L; private ExpectedNodeInterpreter expectedNodeInterpreter; @@ -27,8 +28,8 @@ public void eagerSetup() { new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withMaxOutputSize(MAX_OUTPUT_SIZE) .withExecutionMode(EagerExecutionMode.instance()) .build() @@ -117,20 +118,20 @@ public void itDefersBlock() { assertThat(result).isEqualTo("{% set foo %}i am{{ deferred }}{% endset %}{{ foo }}"); assertThat( - context - .getDeferredTokens() - .stream() - .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) - .collect(Collectors.toSet()) - ) + context + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) + .collect(Collectors.toSet()) + ) .containsExactly("foo"); assertThat( - context - .getDeferredTokens() - .stream() - .flatMap(deferredToken -> deferredToken.getUsedDeferredWords().stream()) - .collect(Collectors.toSet()) - ) + context + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getUsedDeferredWords().stream()) + .collect(Collectors.toSet()) + ) .containsExactlyInAnyOrder("foo", "deferred"); } @@ -145,21 +146,29 @@ public void itDefersBlockWithFilter() { "{% set foo = filter:add.filter(2, ____int3rpr3t3r____, deferred) %}{{ foo }}" ); assertThat( - context - .getDeferredTokens() - .stream() - .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) - .collect(Collectors.toSet()) - ) + context + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) + .collect(Collectors.toSet()) + ) .containsExactly("foo"); assertThat( - context - .getDeferredTokens() - .stream() - .flatMap(deferredToken -> deferredToken.getUsedDeferredWords().stream()) - .collect(Collectors.toSet()) - ) + context + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getUsedDeferredWords().stream()) + .collect(Collectors.toSet()) + ) .containsExactlyInAnyOrder("deferred", "foo", "add.filter"); + assertThat( + context + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getUsedDeferredBases().stream()) + .collect(Collectors.toSet()) + ) + .containsExactlyInAnyOrder("deferred", "foo", "add"); } @Test @@ -197,27 +206,60 @@ public void itDefersInDeferredExecutionModeWithFilter() { "{% set foo %}1{% endset %}{% set foo = filter:add.filter(1, ____int3rpr3t3r____, deferred) %}{{ foo }}" ); assertThat( - context - .getDeferredTokens() - .stream() - .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) - .collect(Collectors.toSet()) - ) + context + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) + .collect(Collectors.toSet()) + ) .containsExactly("foo"); assertThat( - context - .getDeferredTokens() - .stream() - .flatMap(deferredToken -> deferredToken.getUsedDeferredWords().stream()) - .collect(Collectors.toSet()) - ) + context + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getUsedDeferredWords().stream()) + .collect(Collectors.toSet()) + ) .containsExactlyInAnyOrder("deferred", "foo", "add.filter"); + assertThat( + context + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getUsedDeferredBases().stream()) + .collect(Collectors.toSet()) + ) + .containsExactlyInAnyOrder("deferred", "foo", "add"); context.remove("foo"); context.put("deferred", 2); context.setDeferredExecutionMode(false); assertThat(interpreter.render(result)).isEqualTo(interpreter.render(template)); // 1 + 2 + 2 = 5 } + @Test + public void itUnwrapsRawTags() { + String template = "{% set foo %}{% raw %}{%{% endraw %}{% endset %}"; + interpreter.render(template); + assertThat(interpreter.getContext().get("foo")).isEqualTo("{%"); + } + + @Test + public void itUnwrapsEmptyAdjacentRawTags() { + String template = + "{% set foo %}A{% raw %}{% endraw %}{% raw %}{% endraw %}B{% endset %}"; + interpreter.render(template); + assertThat(interpreter.getContext().get("foo")).isEqualTo("AB"); + } + + @Test + public void itDoesNotDoExtraNestedInterpretationWhenUnwrappingRaw() { + String template = + "{% set foo %}{% print '{{ 1 + 1 }}' %}{% raw %}{% endraw %}{{ deferred }}B{% endset %}"; + String result = interpreter.render(template); + interpreter.getContext().put("deferred", "resolved"); + interpreter.render(result); + assertThat(interpreter.getContext().get("foo")).isEqualTo("{{ 1 + 1 }}resolvedB"); + } + @Test @Override @Ignore 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..f6726ae9d 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,18 +2,20 @@ 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; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.interpret.DeferredValue; 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; +import com.hubspot.jinjava.testobjects.EagerTagDecoratorTestObjects; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.tree.parse.TagToken; import java.util.ArrayList; @@ -26,6 +28,7 @@ @RunWith(MockitoJUnitRunner.class) public class EagerTagDecoratorTest extends BaseInterpretingTest { + private static final long MAX_OUTPUT_SIZE = 50L; private Tag mockTag; private EagerGenericTag eagerTagDecorator; @@ -54,8 +57,8 @@ public void eagerSetup() throws Exception { new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withMaxOutputSize(MAX_OUTPUT_SIZE) .withExecutionMode(EagerExecutionMode.instance()) .build() @@ -88,12 +91,10 @@ public void itLimitsEagerInterpretLength() { for (int i = 0; i < MAX_OUTPUT_SIZE; i++) { tooLong.append(i); } - TagNode tagNode = (TagNode) ( - interpreter + TagNode tagNode = (TagNode) (interpreter .parse(String.format("{%% raw %%}%s{%% endraw %%}", tooLong.toString())) .getChildren() - .get(0) - ); + .get(0)); assertThatThrownBy(() -> eagerTagDecorator.eagerInterpret(tagNode, interpreter, null)) .isInstanceOf(OutputTooBigException.class); } @@ -105,27 +106,22 @@ public void itLimitsInterpretLength() { for (int i = 0; i < MAX_OUTPUT_SIZE; i++) { tooLong.append(i); } - TagNode tagNode = (TagNode) ( - interpreter + TagNode tagNode = (TagNode) (interpreter .parse(String.format("{%% raw %%}%s{%% endraw %%}", tooLong.toString())) .getChildren() - .get(0) - ); + .get(0)); 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 public void itLimitsTagLength() { - TagNode tagNode = (TagNode) ( - interpreter.parse("{% print range(0, 50) %}").getChildren().get(0) - ); - assertThatThrownBy( - () -> - eagerTagDecorator.getEagerTagImage((TagToken) tagNode.getMaster(), interpreter) + TagNode tagNode = (TagNode) (interpreter + .parse("{% print range(0, 50) %}") + .getChildren() + .get(0)); + assertThatThrownBy(() -> + eagerTagDecorator.getEagerTagImage((TagToken) tagNode.getMaster(), interpreter) ) .isInstanceOf(OutputTooBigException.class); } @@ -140,16 +136,16 @@ public void itPutsOnContextInChildContext() { public void itModifiesContextInChildContext() { context.put("foo", new ArrayList<>()); assertThat(interpreter.render("{{ modify_context('foo', 'bar') }}{{ foo }}")) - .isEqualTo("[bar]"); + .isEqualTo("['bar']"); } @Test public void itDoesntModifyContextWhenResultIsDeferred() { context.put("foo", new ArrayList<>()); assertThat( - interpreter.render("{{ modify_context('foo', 'bar') ~ deferred }}{{ foo }}") - ) - .isEqualTo("{{ null ~ deferred }}[bar]"); + interpreter.render("{{ modify_context('foo', 'bar') ~ deferred }}{{ foo }}") + ) + .isEqualTo("{{ null ~ deferred }}['bar']"); } public static void addToContext(String key, Object value) { @@ -159,4 +155,25 @@ public static void addToContext(String key, Object value) { public static void modifyContext(String key, Object value) { ((List) JinjavaInterpreter.getCurrent().getContext().get(key)).add(value); } + + @Test + public void itDefersNodeWhenOutputTooBigIsThrownWithinInnerInterpret() { + EagerTagDecoratorTestObjects.TooBig tooBig = new EagerTagDecoratorTestObjects.TooBig( + new ArrayList<>() + ); + interpreter = + new JinjavaInterpreter( + jinjava, + context, + BaseJinjavaTest + .newConfigBuilder() + .withExecutionMode(EagerExecutionMode.instance()) + .build() + ); + interpreter.getContext().put("too_big", tooBig); + interpreter.render( + "{% for i in range(2) %}{% do too_big.append(deferred) %}{% endfor %}" + ); + assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagFactoryTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagFactoryTest.java index c87f049b9..491c16415 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagFactoryTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerTagFactoryTest.java @@ -14,18 +14,16 @@ public class EagerTagFactoryTest { @Test public void itGetsEagerTagDecoratorForOverrides() { - Set> eagerTagDecoratorSet = EagerTagFactory - .EAGER_TAG_OVERRIDES.keySet() + Set> eagerTagDecoratorSet = EagerTagFactory.EAGER_TAG_OVERRIDES + .keySet() .stream() - .map( - clazz -> { - try { - return clazz.getDeclaredConstructor().newInstance(); - } catch (Exception ignored) { - return null; - } + .map(clazz -> { + try { + return clazz.getDeclaredConstructor().newInstance(); + } catch (Exception ignored) { + return null; } - ) + }) .filter(Objects::nonNull) .map(EagerTagFactory::getEagerTagDecorator) .filter(Optional::isPresent) @@ -34,19 +32,18 @@ public void itGetsEagerTagDecoratorForOverrides() { assertThat(eagerTagDecoratorSet.size()) .isEqualTo(EagerTagFactory.EAGER_TAG_OVERRIDES.keySet().size()); assertThat( - eagerTagDecoratorSet - .stream() - .map(e -> e.getTag().getClass()) - .collect(Collectors.toSet()) - ) + eagerTagDecoratorSet + .stream() + .map(e -> e.getTag().getClass()) + .collect(Collectors.toSet()) + ) .isEqualTo(EagerTagFactory.EAGER_TAG_OVERRIDES.keySet()); } @Test public void itGetsEagerTagDecoratorForNonOverride() { - Optional> maybeEagerGenericTag = EagerTagFactory.getEagerTagDecorator( - new IfchangedTag() - ); + Optional> maybeEagerGenericTag = + EagerTagFactory.getEagerTagDecorator(new IfchangedTag()); assertThat(maybeEagerGenericTag).isPresent(); assertThat(maybeEagerGenericTag.get()).isInstanceOf(EagerGenericTag.class); assertThat(maybeEagerGenericTag.get().getTag()).isInstanceOf(IfchangedTag.class); 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..598af35fb 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 @@ -2,8 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.ExpectedNodeInterpreter; -import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.UnlessTagTest; @@ -15,6 +15,7 @@ import org.junit.Test; public class EagerUnlessTagTest extends UnlessTagTest { + private ExpectedNodeInterpreter expectedNodeInterpreter; @Before @@ -23,8 +24,8 @@ public void eagerSetup() { new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(EagerExecutionMode.instance()) .build() ); @@ -66,6 +67,7 @@ public void itHandlesDeferredInEager() { .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/loader/CascadingResourceLocatorTest.java b/src/test/java/com/hubspot/jinjava/loader/CascadingResourceLocatorTest.java index 2d3750202..35a6f4112 100644 --- a/src/test/java/com/hubspot/jinjava/loader/CascadingResourceLocatorTest.java +++ b/src/test/java/com/hubspot/jinjava/loader/CascadingResourceLocatorTest.java @@ -9,11 +9,12 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) @SuppressWarnings("unchecked") public class CascadingResourceLocatorTest { + @Mock ResourceLocator first; diff --git a/src/test/java/com/hubspot/jinjava/loader/ClasspathResourceLocatorTest.java b/src/test/java/com/hubspot/jinjava/loader/ClasspathResourceLocatorTest.java index 7e6c0c574..3b63b2711 100644 --- a/src/test/java/com/hubspot/jinjava/loader/ClasspathResourceLocatorTest.java +++ b/src/test/java/com/hubspot/jinjava/loader/ClasspathResourceLocatorTest.java @@ -11,9 +11,9 @@ public class ClasspathResourceLocatorTest extends BaseInterpretingTest { @Test public void testLoadFromClasspath() throws Exception { assertThat( - new ClasspathResourceLocator() + new ClasspathResourceLocator() .getString("loader/cp/foo/bar.jinja", StandardCharsets.UTF_8, interpreter) - ) + ) .isEqualTo("hello world."); } diff --git a/src/test/java/com/hubspot/jinjava/loader/FileLocatorTest.java b/src/test/java/com/hubspot/jinjava/loader/FileLocatorTest.java index ff0e5854b..9e13e51d3 100644 --- a/src/test/java/com/hubspot/jinjava/loader/FileLocatorTest.java +++ b/src/test/java/com/hubspot/jinjava/loader/FileLocatorTest.java @@ -13,6 +13,7 @@ import org.junit.Test; public class FileLocatorTest extends BaseInterpretingTest { + FileLocator locatorWorkingDir; FileLocator locatorTmpDir; @@ -23,8 +24,8 @@ public class FileLocatorTest extends BaseInterpretingTest { public void setUp() throws Exception { locatorWorkingDir = new FileLocator(); - File tmpDir = java - .nio.file.Files.createTempDirectory(getClass().getSimpleName()) + File tmpDir = java.nio.file.Files + .createTempDirectory(getClass().getSimpleName()) .toFile(); locatorTmpDir = new FileLocator(tmpDir); @@ -41,44 +42,44 @@ public void setUp() throws Exception { @Test public void testWorkingDirRelative() throws Exception { assertThat( - locatorWorkingDir.getString( - "target/loader-test-data/second.jinja", - StandardCharsets.UTF_8, - interpreter - ) + locatorWorkingDir.getString( + "target/loader-test-data/second.jinja", + StandardCharsets.UTF_8, + interpreter ) + ) .isEqualTo("second"); } @Test public void testWorkingDirAbs() throws Exception { assertThat( - locatorWorkingDir.getString( - second.getAbsolutePath(), - StandardCharsets.UTF_8, - interpreter - ) + locatorWorkingDir.getString( + second.getAbsolutePath(), + StandardCharsets.UTF_8, + interpreter ) + ) .isEqualTo("second"); } @Test public void testTmpDirRel() throws Exception { assertThat( - locatorTmpDir.getString("foo/first.jinja", StandardCharsets.UTF_8, interpreter) - ) + locatorTmpDir.getString("foo/first.jinja", StandardCharsets.UTF_8, interpreter) + ) .isEqualTo("first"); } @Test public void testTmpDirAbs() throws Exception { assertThat( - locatorTmpDir.getString( - first.getAbsolutePath(), - StandardCharsets.UTF_8, - interpreter - ) + locatorTmpDir.getString( + first.getAbsolutePath(), + StandardCharsets.UTF_8, + interpreter ) + ) .isEqualTo("first"); } diff --git a/src/test/java/com/hubspot/jinjava/objects/NamespaceTest.java b/src/test/java/com/hubspot/jinjava/objects/NamespaceTest.java index 9b9d866b8..8f6e78732 100644 --- a/src/test/java/com/hubspot/jinjava/objects/NamespaceTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/NamespaceTest.java @@ -6,6 +6,7 @@ import org.junit.Test; public class NamespaceTest { + private Namespace namespace; @Before 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..d511a9e7f 100644 --- a/src/test/java/com/hubspot/jinjava/objects/collections/PyListTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/collections/PyListTest.java @@ -6,6 +6,7 @@ import com.hubspot.jinjava.interpret.IndexOutOfRangeException; import com.hubspot.jinjava.interpret.RenderResult; import com.hubspot.jinjava.interpret.TemplateError; +import java.util.ArrayList; import java.util.Collections; import org.junit.Test; @@ -14,66 +15,66 @@ public class PyListTest extends BaseJinjavaTest { @Test public void itSupportsAppendOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.append(4) %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.append(4) %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[1, 2, 3, 4]"); } @Test public void itSupportsExtendOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.extend([4, 5, 6]) %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.extend([4, 5, 6]) %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[1, 2, 3, 4, 5, 6]"); } @Test public void itSupportsExtendOperationWithNullList() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.extend(null) %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.extend(null) %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[1, 2, 3]"); } @Test public void itSupportsInsertOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.insert(1, 4) %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.insert(1, 4) %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[1, 4, 2, 3]"); } @Test public void itSupportsInsertOperationWithNegativeIndex() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.insert(-1, 4) %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.insert(-1, 4) %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[1, 2, 4, 3]"); } @Test public void itSupportsInsertOperationWithLargeNegativeIndex() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.insert(-99, 4) %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.insert(-99, 4) %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[4, 1, 2, 3]"); } @@ -93,33 +94,33 @@ public void itHandlesInsertOperationOutOfRange() { @Test public void itSupportsPopOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{{ test.pop() }}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{{ test.pop() }}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("3[1, 2]"); } @Test public void itSupportsPopAtIndexOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{{ test.pop(1) }}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{{ test.pop(1) }}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("2[1, 3]"); } @Test public void itSupportsPopAtNegativeIndexOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{{ test.pop(-1) }}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{{ test.pop(-1) }}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("3[1, 2]"); } @@ -152,91 +153,147 @@ public void itThrowsIndexOutOfRangeForPopOutOfRange() { @Test public void itSupportsClearOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.clear() %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.clear() %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[]"); } @Test public void itSupportsCountOperation() { assertThat( - jinjava.render( - "{% set test = [1, 1, 2, 2, 2, 3] %}" + "{{ test.count(2) }}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 1, 2, 2, 2, 3] %}" + "{{ test.count(2) }}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("3[1, 1, 2, 2, 2, 3]"); } @Test public void itSupportsReverseOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.reverse() %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.reverse() %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[3, 2, 1]"); } @Test public void itSupportsCopyOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + - "{% set test2 = test.copy() %}" + - "{% do test.append(4) %}" + - "{{ test }}{{test2}}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + + "{% set test2 = test.copy() %}" + + "{% do test.append(4) %}" + + "{{ test }}{{test2}}", + Collections.emptyMap() ) + ) .isEqualTo("[1, 2, 3, 4][1, 2, 3]"); } @Test public void itSupportsIndexOperation() { assertThat( - jinjava.render( - "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(20) }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(20) }}", + Collections.emptyMap() ) + ) .isEqualTo("1"); } @Test public void itSupportsIndexWithinBoundsOperation() { assertThat( - jinjava.render( - "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(20, 2, 6) }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(20, 2, 6) }}", + Collections.emptyMap() ) + ) .isEqualTo("4"); } @Test public void itReturnsNegativeOneForMissingObjectForIndex() { assertThat( - jinjava.render( - "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(999) }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(999) }}", + Collections.emptyMap() ) + ) .isEqualTo("-1"); } @Test public void itReturnsNegativeOneForMissingObjectForIndexWithinBounds() { assertThat( - jinjava.render( - "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(999, 1, 5) }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(999, 1, 5) }}", + Collections.emptyMap() ) + ) .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]"); + } + + @Test + public void itComputesHashCodeWhenListContainsItself() { + PyList list1 = new PyList(new ArrayList<>()); + PyList list2 = new PyList(new ArrayList<>()); + list1.add(list2); + int initialHashCode = list1.hashCode(); + list2.add(list1); + int hashCodeWithInfiniteRecursion = list1.hashCode(); + assertThat(initialHashCode).isNotEqualTo(hashCodeWithInfiniteRecursion); + assertThat(list1.hashCode()) + .isEqualTo(hashCodeWithInfiniteRecursion) + .describedAs("Hash code should be consistent on multiple calls"); + assertThat(list2.hashCode()) + .isEqualTo(list1.hashCode()) + .describedAs( + "The two lists are currently the same as they are both a list1 of a single infinitely recurring list" + ); + list1.add(123456); + assertThat(list2.hashCode()) + .isNotEqualTo(list1.hashCode()) + .describedAs( + "The two lists are no longer the same as list1 has 2 elements while list2 has one" + ); + PyList copy = list1.copy(); + assertThat(copy.hashCode()) + .isNotEqualTo(list1.hashCode()) + .describedAs( + "copy is not the same as list1 because it is a list of a list of recursion, whereas list1 is a list of recursion" + ); + assertThat(list1.copy().hashCode()) + .isEqualTo(copy.hashCode()) + .describedAs("All copies should have the same hash code"); + } } diff --git a/src/test/java/com/hubspot/jinjava/objects/collections/PyMapTest.java b/src/test/java/com/hubspot/jinjava/objects/collections/PyMapTest.java index 3ec10fbe8..0bb4bfa33 100644 --- a/src/test/java/com/hubspot/jinjava/objects/collections/PyMapTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/collections/PyMapTest.java @@ -3,13 +3,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.google.common.collect.ImmutableList; import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; -import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.IndexOutOfRangeException; import com.hubspot.jinjava.interpret.RenderResult; import com.hubspot.jinjava.interpret.TemplateError; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import org.junit.Test; @@ -19,55 +20,55 @@ public class PyMapTest extends BaseJinjavaTest { @Test public void itSupportsAppendOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.append(4) %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.append(4) %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[1, 2, 3, 4]"); } @Test public void itSupportsExtendOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.extend([4, 5, 6]) %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.extend([4, 5, 6]) %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[1, 2, 3, 4, 5, 6]"); } @Test public void itSupportsInsertOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.insert(1, 4) %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.insert(1, 4) %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[1, 4, 2, 3]"); } @Test public void itSupportsInsertOperationWithNegativeIndex() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.insert(-1, 4) %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.insert(-1, 4) %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[1, 2, 4, 3]"); } @Test public void itSupportsInsertOperationWithLargeNegativeIndex() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.insert(-99, 4) %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.insert(-99, 4) %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[4, 1, 2, 3]"); } @@ -87,33 +88,33 @@ public void itHandlesInsertOperationOutOfRange() { @Test public void itSupportsPopOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{{ test.pop() }}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{{ test.pop() }}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("3[1, 2]"); } @Test public void itSupportsPopAtIndexOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{{ test.pop(1) }}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{{ test.pop(1) }}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("2[1, 3]"); } @Test public void itSupportsPopAtNegativeIndexOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{{ test.pop(-1) }}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{{ test.pop(-1) }}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("3[1, 2]"); } @@ -146,91 +147,91 @@ public void itThrowsIndexOutOfRangeForPopOutOfRange() { @Test public void itSupportsClearOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.clear() %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.clear() %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[]"); } @Test public void itSupportsCountOperation() { assertThat( - jinjava.render( - "{% set test = [1, 1, 2, 2, 2, 3] %}" + "{{ test.count(2) }}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 1, 2, 2, 2, 3] %}" + "{{ test.count(2) }}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("3[1, 1, 2, 2, 2, 3]"); } @Test public void itSupportsReverseOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + "{% do test.reverse() %}" + "{{ test }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + "{% do test.reverse() %}" + "{{ test }}", + Collections.emptyMap() ) + ) .isEqualTo("[3, 2, 1]"); } @Test public void itSupportsCopyOperation() { assertThat( - jinjava.render( - "{% set test = [1, 2, 3] %}" + - "{% set test2 = test.copy() %}" + - "{% do test.append(4) %}" + - "{{ test }}{{test2}}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [1, 2, 3] %}" + + "{% set test2 = test.copy() %}" + + "{% do test.append(4) %}" + + "{{ test }}{{test2}}", + Collections.emptyMap() ) + ) .isEqualTo("[1, 2, 3, 4][1, 2, 3]"); } @Test public void itSupportsIndexOperation() { assertThat( - jinjava.render( - "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(20) }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(20) }}", + Collections.emptyMap() ) + ) .isEqualTo("1"); } @Test public void itSupportsIndexWithinBoundsOperation() { assertThat( - jinjava.render( - "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(20, 2, 6) }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(20, 2, 6) }}", + Collections.emptyMap() ) + ) .isEqualTo("4"); } @Test public void itReturnsNegativeOneForMissingObjectForIndex() { assertThat( - jinjava.render( - "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(999) }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(999) }}", + Collections.emptyMap() ) + ) .isEqualTo("-1"); } @Test public void itReturnsNegativeOneForMissingObjectForIndexWithinBounds() { assertThat( - jinjava.render( - "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(999, 1, 5) }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = [10, 20, 30, 10, 20, 30] %}" + "{{ test.index(999, 1, 5) }}", + Collections.emptyMap() ) + ) .isEqualTo("-1"); } @@ -261,26 +262,35 @@ public void itDisallowsSelfReferencingPutAll() { @Test public void itUpdatesKeysWithStaticName() { assertThat( - jinjava.render( - "{% set test = {\"key1\": \"value1\"} %}" + - "{% do test.update({\"key1\": \"value2\"}) %}" + - "{{ test[\"key1\"] }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = {\"key1\": \"value1\"} %}" + + "{% do test.update({\"key1\": \"value2\"}) %}" + + "{{ test[\"key1\"] }}", + Collections.emptyMap() ) + ) .isEqualTo("value2"); } @Test - public void itDoesntSetKeysWithVariableNameByDefault() { + public void itDoesntSetKeysWithVariableNameWhenLegacy() { + jinjava = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides.newBuilder().withEvaluateMapKeys(false).build() + ) + .build() + ); assertThat( - jinjava.render( - "{% set keyName = \"key1\" %}" + - "{% set test = {keyName: \"value1\"} %}" + - "{{ test['keyName'] }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set keyName = \"key1\" %}" + + "{% set test = {keyName: \"value1\"} %}" + + "{{ test['keyName'] }}", + Collections.emptyMap() ) + ) .isEqualTo("value1"); } @@ -288,8 +298,8 @@ public void itDoesntSetKeysWithVariableNameByDefault() { public void itSetsKeysWithVariableName() { jinjava = new Jinjava( - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withLegacyOverrides( LegacyOverrides.newBuilder().withEvaluateMapKeys(true).build() ) @@ -297,51 +307,75 @@ public void itSetsKeysWithVariableName() { ); assertThat( - jinjava.render( - "{% set keyName = \"key1\" %}" + - "{% set test = {keyName: \"value1\"} %}" + - "{{ test[keyName] }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set keyName = \"key1\" %}" + + "{% set test = {keyName: \"value1\"} %}" + + "{{ test[keyName] }}", + Collections.emptyMap() ) + ) .isEqualTo("value1"); } @Test public void itGetsKeysWithVariableName() { assertThat( - jinjava.render( - "{% set test = {\"key1\": \"value1\"} %}" + - "{% set keyName = \"key1\" %}" + - "{{ test[keyName] }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = {\"key1\": \"value1\"} %}" + + "{% set keyName = \"key1\" %}" + + "{{ test[keyName] }}", + Collections.emptyMap() ) + ) .isEqualTo("value1"); } + @Test + public void itSupportsGetWithOptionalDefault() { + assertThat( + jinjava.render( + "{% set test = {\"key1\": \"value1\"} %}" + + "{{ test.get(\"key2\", \"default\") }}", + Collections.emptyMap() + ) + ) + .isEqualTo("default"); + } + @Test public void itFallsBackUnknownVariableNameToString() { assertThat( - jinjava.render( - "{% set test = {keyName: \"value1\"} %}" + "{{ test[\"keyName\"] }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = {keyName: \"value1\"} %}" + "{{ test[\"keyName\"] }}", + Collections.emptyMap() ) + ) .isEqualTo("value1"); } @Test - public void itDoesntUpdateKeysWithVariableNameByDefault() { + public void itDoesntUpdateKeysWithVariableNameWhenLegacy() { + jinjava = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withEvaluateMapKeys(false) + .build() + ) + .build() + ); assertThat( - jinjava.render( - "{% set test = {\"key1\": \"value1\"} %}" + - "{% set keyName = \"key1\" %}" + - "{% do test.update({keyName: \"value2\"}) %}" + - "{{ test['key1'] }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = {\"key1\": \"value1\"} %}" + + "{% set keyName = \"key1\" %}" + + "{% do test.update({keyName: \"value2\"}) %}" + + "{{ test['key1'] }}", + Collections.emptyMap() ) + ) .isEqualTo("value1"); } @@ -349,22 +383,79 @@ public void itDoesntUpdateKeysWithVariableNameByDefault() { public void itUpdatesKeysWithVariableName() { jinjava = new Jinjava( - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withLegacyOverrides( LegacyOverrides.newBuilder().withEvaluateMapKeys(true).build() ) .build() ); assertThat( - jinjava.render( - "{% set test = {\"key1\": \"value1\"} %}" + - "{% set keyName = \"key1\" %}" + - "{% do test.update({keyName: \"value2\"}) %}" + - "{{ test[keyName] }}", - Collections.emptyMap() - ) + jinjava.render( + "{% set test = {\"key1\": \"value1\"} %}" + + "{% set keyName = \"key1\" %}" + + "{% do test.update({keyName: \"value2\"}) %}" + + "{{ test[keyName] }}", + Collections.emptyMap() ) + ) .isEqualTo("value2"); } + + @Test + public void itComputesHashCodeWhenContainedWithinItself() { + PyMap map = new PyMap(new HashMap<>()); + map.put("map1key1", "value1"); + + PyMap map2 = new PyMap(new HashMap<>()); + map2.put("map2key1", map); + + map.put("map1key2", map2); + + assertThat(map.hashCode()).isNotEqualTo(0); + } + + @Test + public void itComputesHashCodeWhenContainedWithinItselfWithFurtherEntries() { + PyMap map = new PyMap(new HashMap<>()); + map.put("map1key1", "value1"); + + PyMap map2 = new PyMap(new HashMap<>()); + map2.put("map2key1", map); + + map.put("map1key2", map2); + + int originalHashCode = map.hashCode(); + map2.put("newKey", "newValue"); + int newHashCode = map.hashCode(); + assertThat(originalHashCode).isNotEqualTo(newHashCode); + } + + @Test + public void itComputesHashCodeWhenContainedWithinItselfInsideList() { + PyMap map = new PyMap(new HashMap<>()); + map.put("map1key1", "value1"); + + PyMap map2 = new PyMap(new HashMap<>()); + map2.put("map2key1", map); + + map.put("map1key2", new PyList(ImmutableList.of((map2)))); + + assertThat(map.hashCode()).isNotEqualTo(0); + } + + @Test + public void itComputesHashCodeWithNullKeysAndValues() { + PyMap map = new PyMap(new HashMap<>()); + map.put(null, "value1"); + + PyMap map2 = new PyMap(new HashMap<>()); + map2.put("map2key1", map); + + PyList list = new PyList(new ArrayList<>()); + list.add(null); + map.put("map1key2", new PyList(list)); + + assertThat(map.hashCode()).isNotEqualTo(0); + } } 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..b732811b2 100644 --- a/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java @@ -2,9 +2,12 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; +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(), + BaseJinjavaTest + .newConfigBuilder() + .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")); @@ -67,6 +92,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")); @@ -93,4 +138,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"); + } } 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..8cc5d71cd 100644 --- a/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/date/StrftimeFormatterTest.java @@ -1,7 +1,11 @@ package com.hubspot.jinjava.objects.date; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Locale; @@ -9,6 +13,7 @@ import org.junit.Test; public class StrftimeFormatterTest { + ZonedDateTime d; @Before @@ -53,12 +58,13 @@ 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")) + .isIn("Nov 6, 2013, 2:22:00 PM", "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 @@ -68,12 +74,13 @@ public void testDayOfWeekNumber() { @Test public void testTime() { - assertThat(StrftimeFormatter.format(d, "%X")).isEqualTo("14:22:00"); + assertThat(StrftimeFormatter.format(d, "%X")).isIn("2:22:00 PM", "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")) + .isIn("2:22:00 PM 123000", "2:22:00 PM 123000"); } @Test @@ -92,13 +99,8 @@ public void testPaddedMinFmt() { @Test public void testFinnishMonths() { - assertThat( - StrftimeFormatter - .formatter("long") - .withLocale(Locale.forLanguageTag("fi")) - .format(d) - ) - .startsWith("6. marraskuuta 2013 klo 14.22.00"); + assertThat(StrftimeFormatter.format(d, "long", Locale.forLanguageTag("fi"))) + .isIn("6. marraskuuta 2013 klo 14.22.00 UTC", "6. marraskuuta 2013 14.22.00 UTC"); } @Test @@ -118,23 +120,49 @@ 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("Июнь", "июнь"); } @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'"); + + assertThatExceptionOfType(InvalidDateFormatException.class) + .isThrownBy(() -> StrftimeFormatter.format(d, "%d.%ğ.%Y")) + .withMessage("Invalid date format '%d.%ğ.%Y'"); + } + + @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("%"); + } + + @Test + public void itAllowsLiteralCharacters() { + assertThat(StrftimeFormatter.format(d, "1: day %d month %B")) + .isEqualTo("1: day 06 month November"); } @Test - public void testJavaFormatWithGT255Char() { - assertThat(StrftimeFormatter.toJavaDateTimeFormat("%d.%ğ.%Y")) - .isEqualTo("dd.null.yyyy"); + public void itUsesInterpreterLocaleAsDefault() { + try { + Jinjava jinjava = new Jinjava( + BaseJinjavaTest.newConfigBuilder().withLocale(Locale.FRENCH).build() + ); + JinjavaInterpreter.pushCurrent(jinjava.newInterpreter()); + assertThat(StrftimeFormatter.format(d, "%B %-d, %Y")).isEqualTo("novembre 6, 2013"); + } finally { + JinjavaInterpreter.popCurrent(); + } } } 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..483b8bd8a 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -1,13 +1,24 @@ 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.fasterxml.jackson.databind.JsonMappingException; import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.LegacyOverrides; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; +import com.hubspot.jinjava.testobjects.PyishObjectMapperTestObjects; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.Test; public class PyishObjectMapperTest { @@ -18,17 +29,36 @@ public void itSerializesMapWithNullKeysAsEmptyString() { map.put("foo", "bar"); map.put(null, "null"); assertThat(PyishObjectMapper.getAsPyishString(map)) - .isEqualTo("{'': 'null', 'foo': 'bar'}"); + .isEqualTo("{'': 'null', 'foo': 'bar'} "); } @Test - public void itSerializesMapEntrySet() throws JsonProcessingException { + public void itSerializesMapEntrySet() { SizeLimitingPyMap map = new SizeLimitingPyMap(new HashMap<>(), 10); map.put("foo", "bar"); 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 + public void itSerializesMapEntrySetWithLimit() { + SizeLimitingPyMap map = new SizeLimitingPyMap(new HashMap<>(), 10); + map.put("foo", "bar"); + map.put("bar", ImmutableMap.of("foobar", new ArrayList<>())); + + Jinjava jinjava = new Jinjava( + BaseJinjavaTest.newConfigBuilder().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 @@ -37,6 +67,116 @@ 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'} "); + } + + @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( + BaseJinjavaTest.newConfigBuilder().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(JsonMappingException.class) + .hasCauseInstanceOf(LengthLimitingJsonProcessingException.class) + .hasMessageContaining("Max length of 10000 chars reached"); + assertThatThrownBy(() -> PyishObjectMapper.getAsPyishString(original)) + .isInstanceOf(OutputTooBigException.class); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + + @Test + public void itLimitsOutputSize() { + String input = RandomStringUtils.random(10002); + try { + Jinjava jinjava = new Jinjava( + BaseJinjavaTest.newConfigBuilder().withMaxOutputSize(10000).build() + ); + JinjavaInterpreter.pushCurrent(jinjava.newInterpreter()); + assertThatThrownBy(() -> PyishObjectMapper.getAsPyishString(input)) + .isInstanceOf(OutputTooBigException.class) + .hasMessageContaining("over limit of 10000 bytes"); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + + @Test + public void itSerializesToSnakeCaseAccessibleMap() { + assertThat( + PyishObjectMapper.getAsPyishString(new PyishObjectMapperTestObjects.Foo("bar")) + ) + .isEqualTo("{'fooBar': 'bar'} |allow_snake_case"); + } + + @Test + public void itSerializesToSnakeCaseAccessibleMapWhenInMapEntry() { + assertThat( + PyishObjectMapper.getAsPyishString( + new AbstractMap.SimpleImmutableEntry<>( + "foo", + new PyishObjectMapperTestObjects.Foo("bar") + ) + ) + ) + .isEqualTo("fn:map_entry('foo', {'fooBar': 'bar'} |allow_snake_case)"); + } + + @Test + public void itDoesNotConvertToSnakeCaseMapWhenResultIsForOutput() { + Jinjava jinjava = new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() + ) + .build() + ); + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + interpreter.getContext().put("foo", new PyishObjectMapperTestObjects.Foo("bar")); + assertThat(interpreter.render("{{ foo }}")).isEqualTo("{'fooBar': 'bar'}"); + } + + @Test + public void itSerializesToSnakeCaseWhenLegacyOverrideIsSet() { + Jinjava jinjava = new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides + .newBuilder() + .withUsePyishObjectMapper(true) + .withUseSnakeCasePropertyNaming(true) + .build() + ) + .build() + ); + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + try { + JinjavaInterpreter.pushCurrent(interpreter); + interpreter.getContext().put("foo", new PyishObjectMapperTestObjects.Foo("bar")); + assertThat(interpreter.render("{{ foo }}")).isEqualTo("{'foo_bar': 'bar'}"); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + + @Test + public void itSerializesOptional() { + assertThat(PyishObjectMapper.getAsPyishString(Optional.of("foo"))).isEqualTo("'foo'"); } } diff --git a/src/test/java/com/hubspot/jinjava/testobjects/AstDictTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/AstDictTestObjects.java new file mode 100644 index 000000000..68c81e37e --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/AstDictTestObjects.java @@ -0,0 +1,36 @@ +package com.hubspot.jinjava.testobjects; + +import com.hubspot.jinjava.interpret.TemplateError; +import java.util.Map; + +public class AstDictTestObjects { + + public static class TestClass { + + private Map myMap; + + public TestClass(Map myMap) { + this.myMap = myMap; + } + + public Map getMyMap() { + return myMap; + } + } + + public static enum TestEnum { + FOO("fooName"), + BAR("barName"); + + private String name; + + TestEnum(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/EagerAstDotTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/EagerAstDotTestObjects.java new file mode 100644 index 000000000..68619a042 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/EagerAstDotTestObjects.java @@ -0,0 +1,23 @@ +package com.hubspot.jinjava.testobjects; + +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.PartiallyDeferredValue; + +public class EagerAstDotTestObjects { + + public static class Foo implements PartiallyDeferredValue { + + public String getDeferred() { + throw new DeferredValueException("foo.deferred is deferred"); + } + + public String getResolved() { + return "resolved"; + } + + @Override + public Object getOriginalValue() { + return null; + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/EagerExpressionResolverTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/EagerExpressionResolverTestObjects.java new file mode 100644 index 000000000..d51006361 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/EagerExpressionResolverTestObjects.java @@ -0,0 +1,58 @@ +package com.hubspot.jinjava.testobjects; + +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; + +public class EagerExpressionResolverTestObjects { + + public static class Foo { + + private final String bar; + + public Foo(String bar) { + this.bar = bar; + } + + public String bar() { + return bar; + } + + public String echo(String toEcho) { + return toEcho; + } + } + + public static class SomethingExceptionallyPyish implements PyishSerializable { + + private String name; + + public SomethingExceptionallyPyish(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + throw new DeferredValueException("Can't serialize"); + } + } + + public static class SomethingPyish implements PyishSerializable { + + private String name; + + public SomethingPyish(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/EagerImportTagTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/EagerImportTagTestObjects.java new file mode 100644 index 000000000..4610c493e --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/EagerImportTagTestObjects.java @@ -0,0 +1,20 @@ +package com.hubspot.jinjava.testobjects; + +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.filter.Filter; + +public class EagerImportTagTestObjects { + + public static class PrintPathFilter implements Filter { + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + return interpreter.getContext().getCurrentPathStack().peek().orElse("/"); + } + + @Override + public String getName() { + return "print_path"; + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/EagerTagDecoratorTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/EagerTagDecoratorTestObjects.java new file mode 100644 index 000000000..e6b675c78 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/EagerTagDecoratorTestObjects.java @@ -0,0 +1,23 @@ +package com.hubspot.jinjava.testobjects; + +import com.hubspot.jinjava.interpret.OutputTooBigException; +import com.hubspot.jinjava.objects.collections.PyList; +import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; +import java.util.List; + +public class EagerTagDecoratorTestObjects { + + public static class TooBig extends PyList implements PyishSerializable { + + public TooBig(List list) { + super(list); + } + + @Override + public T appendPyishString(T appendable) + throws IOException { + throw new OutputTooBigException(1, 1); + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/ExpressionResolverTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/ExpressionResolverTestObjects.java new file mode 100644 index 000000000..a9d030ee5 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/ExpressionResolverTestObjects.java @@ -0,0 +1,160 @@ +package com.hubspot.jinjava.testobjects; + +import com.google.common.collect.ForwardingList; +import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.objects.PyWrapper; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public class ExpressionResolverTestObjects { + + public static class MyCustomList extends ForwardingList implements PyWrapper { + + private final List list; + + public MyCustomList(List list) { + this.list = list; + } + + @Override + protected List delegate() { + return list; + } + + public int getTotalCount() { + return list.size(); + } + } + + public static final class MyCustomMap implements Map { + + Map data = ImmutableMap.of("foo", "bar", "two", "2", "size", "777"); + + @Override + public int size() { + return data.size(); + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return data.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return data.containsValue(value); + } + + @Override + public String get(Object key) { + return data.get(key); + } + + @Override + public String put(String key, String value) { + return null; + } + + @Override + public String remove(Object key) { + return null; + } + + @Override + public void putAll(Map m) {} + + @Override + public void clear() {} + + @Override + public Set keySet() { + return data.keySet(); + } + + @Override + public Collection values() { + return data.values(); + } + + @Override + public Set> entrySet() { + return data.entrySet(); + } + } + + public static class TestClass { + + private boolean touched = false; + private String name = "Amazing test class"; + + public boolean isTouched() { + return touched; + } + + public void touch() { + this.touched = true; + } + + public String getName() { + return name; + } + } + + public static final class MyClass { + + private Date date; + + public MyClass(Date date) { + this.date = date; + } + + public Class getClazz() { + return this.getClass(); + } + + public Date getDate() { + return date; + } + } + + public static final class OptionalProperty { + + private MyClass nested; + private String val; + + public OptionalProperty(MyClass nested, String val) { + this.nested = nested; + this.val = val; + } + + public Optional getNested() { + return Optional.ofNullable(nested); + } + + public Optional getVal() { + return Optional.ofNullable(val); + } + } + + public static final class NestedOptionalProperty { + + private OptionalProperty nested; + + public NestedOptionalProperty(OptionalProperty nested) { + this.nested = nested; + } + + public Optional getNested() { + return Optional.ofNullable(nested); + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/FilterOverrideTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/FilterOverrideTestObjects.java new file mode 100644 index 000000000..b235e2a95 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/FilterOverrideTestObjects.java @@ -0,0 +1,26 @@ +package com.hubspot.jinjava.testobjects; + +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.filter.Filter; + +public class FilterOverrideTestObjects { + + 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])) + ); + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/IfTagTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/IfTagTestObjects.java new file mode 100644 index 000000000..9bd2794db --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/IfTagTestObjects.java @@ -0,0 +1,21 @@ +package com.hubspot.jinjava.testobjects; + +import com.hubspot.jinjava.util.HasObjectTruthValue; + +public class IfTagTestObjects { + + public static class Foo implements HasObjectTruthValue { + + private boolean objectTruthValue = false; + + public Foo setObjectTruthValue(boolean objectTruthValue) { + this.objectTruthValue = objectTruthValue; + return this; + } + + @Override + public boolean getObjectTruthValue() { + return objectTruthValue; + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/JinjavaBeanELResolverTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/JinjavaBeanELResolverTestObjects.java new file mode 100644 index 000000000..9d10e6cd7 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/JinjavaBeanELResolverTestObjects.java @@ -0,0 +1,38 @@ +package com.hubspot.jinjava.testobjects; + +public class JinjavaBeanELResolverTestObjects { + + public static class TempItInvokesBestMethodWithSingleParam { + + 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"; + } + } + + public static class TempItPrefersPrimitives { + + 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"; + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/JinjavaInterpreterTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/JinjavaInterpreterTestObjects.java new file mode 100644 index 000000000..2bd11365a --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/JinjavaInterpreterTestObjects.java @@ -0,0 +1,32 @@ +package com.hubspot.jinjava.testobjects; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public class JinjavaInterpreterTestObjects { + + public static class Foo { + + private String bar; + + public Foo(String bar) { + this.bar = bar; + } + + public String getBar() { + return bar; + } + + public String getBarFoo() { + return bar; + } + + public String getBarFoo1() { + return bar; + } + + @JsonIgnore + public String getBarHidden() { + return bar; + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/PartiallyDeferredValueTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/PartiallyDeferredValueTestObjects.java new file mode 100644 index 000000000..c7c579412 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/PartiallyDeferredValueTestObjects.java @@ -0,0 +1,135 @@ +package com.hubspot.jinjava.testobjects; + +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.PartiallyDeferredValue; +import com.hubspot.jinjava.objects.collections.PyMap; +import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckForNull; + +public class PartiallyDeferredValueTestObjects { + + public static class BadSerialization implements PartiallyDeferredValue { + + public String getDeferred() { + throw new DeferredValueException("foo.deferred is deferred"); + } + + public String getResolved() { + return "resolved"; + } + + @Override + public Object getOriginalValue() { + return null; + } + } + + public static class BadEntrySet extends PyMap implements PartiallyDeferredValue { + + public BadEntrySet(Map map) { + super(map); + } + + @Override + public Set> entrySet() { + throw new DeferredValueException("entries are deferred"); + } + + @CheckForNull + @Override + public Object get(@CheckForNull Object key) { + if ("deferred".equals(key)) { + throw new DeferredValueException("deferred key"); + } + return super.get(key); + } + + @Override + public Object getOriginalValue() { + return null; + } + } + + public static class BadPyishSerializable + implements PartiallyDeferredValue, PyishSerializable { + + public String getDeferred() { + throw new DeferredValueException("foo.deferred is deferred"); + } + + public String getResolved() { + return "resolved"; + } + + @Override + public Object getOriginalValue() { + return null; + } + + @Override + public T appendPyishString(T appendable) + throws IOException { + throw new DeferredValueException("I'm bad"); + } + } + + public static class GoodPyishSerializable + implements PartiallyDeferredValue, PyishSerializable { + + public String getDeferred() { + throw new DeferredValueException("foo.deferred is deferred"); + } + + public String getResolved() { + return "resolved"; + } + + @Override + public Object getOriginalValue() { + return null; + } + + @Override + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append("good"); + } + } + + public static class BadEntrySetButPyishSerializable + extends PyMap + implements PartiallyDeferredValue, PyishSerializable { + + public BadEntrySetButPyishSerializable(Map map) { + super(map); + } + + @Override + public Set> entrySet() { + throw new DeferredValueException("entries are deferred"); + } + + @CheckForNull + @Override + public Object get(@CheckForNull Object key) { + if ("deferred".equals(key)) { + throw new DeferredValueException("deferred key"); + } + return super.get(key); + } + + @Override + public Object getOriginalValue() { + return null; + } + + @Override + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append("hello"); + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/PyishObjectMapperTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/PyishObjectMapperTestObjects.java new file mode 100644 index 000000000..c08c994d4 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/PyishObjectMapperTestObjects.java @@ -0,0 +1,17 @@ +package com.hubspot.jinjava.testobjects; + +public class PyishObjectMapperTestObjects { + + public static class Foo { + + private final String bar; + + public Foo(String bar) { + this.bar = bar; + } + + public String getFooBar() { + return bar; + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/SortFilterTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/SortFilterTestObjects.java new file mode 100644 index 000000000..cc985cda0 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/SortFilterTestObjects.java @@ -0,0 +1,58 @@ +package com.hubspot.jinjava.testobjects; + +import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; +import java.util.Date; + +public class SortFilterTestObjects { + + public static class MyFoo implements PyishSerializable { + + private Date date; + + public MyFoo(Date date) { + this.date = date; + } + + public Date getDate() { + return date; + } + + @Override + public String toString() { + return "" + date.getTime(); + } + + @Override + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(toString()); + } + } + + public static class MyBar implements PyishSerializable { + + private MyFoo foo; + + public MyBar(MyFoo foo) { + this.foo = foo; + } + + public MyFoo getFoo() { + return foo; + } + + @Override + public String toString() { + return foo.toString(); + } + + @Override + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(toString()); + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/UniqueFilterTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/UniqueFilterTestObjects.java new file mode 100644 index 000000000..034804fc3 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/UniqueFilterTestObjects.java @@ -0,0 +1,32 @@ +package com.hubspot.jinjava.testobjects; + +import com.hubspot.jinjava.objects.serialization.PyishSerializable; +import java.io.IOException; + +public class UniqueFilterTestObjects { + + public static class MyClass implements PyishSerializable { + + private final String name; + + public MyClass(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "[Name:" + name + "]"; + } + + @Override + @SuppressWarnings("unchecked") + public T appendPyishString(T appendable) + throws IOException { + return (T) appendable.append(toString()); + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/ValidationModeTestObjects.java b/src/test/java/com/hubspot/jinjava/testobjects/ValidationModeTestObjects.java new file mode 100644 index 000000000..3aaa9306b --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/ValidationModeTestObjects.java @@ -0,0 +1,27 @@ +package com.hubspot.jinjava.testobjects; + +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.filter.Filter; + +public class ValidationModeTestObjects { + + public static class ValidationFilter implements Filter { + + private int executionCount = 0; + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + executionCount++; + return var; + } + + public int getExecutionCount() { + return executionCount; + } + + @Override + public String getName() { + return "validation_filter"; + } + } +} diff --git a/src/test/java/com/hubspot/jinjava/tree/ExpressionNodeTest.java b/src/test/java/com/hubspot/jinjava/tree/ExpressionNodeTest.java index ca613ef99..ac5273b82 100644 --- a/src/test/java/com/hubspot/jinjava/tree/ExpressionNodeTest.java +++ b/src/test/java/com/hubspot/jinjava/tree/ExpressionNodeTest.java @@ -1,153 +1,232 @@ package com.hubspot.jinjava.tree; +import static com.hubspot.jinjava.lib.expression.DefaultExpressionStrategy.ECHO_UNDEFINED; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.google.common.io.Resources; -import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; -import com.hubspot.jinjava.interpret.Context; +import com.hubspot.jinjava.features.BuiltInFeatures; +import com.hubspot.jinjava.features.FeatureConfig; +import com.hubspot.jinjava.features.FeatureStrategies; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.UnknownTokenException; import java.nio.charset.StandardCharsets; +import org.junit.Before; import org.junit.Test; -public class ExpressionNodeTest extends BaseInterpretingTest { +public class ExpressionNodeTest { - @Test - public void itRendersResultAsTemplateWhenContainingVarBlocks() throws Exception { - context.put("myvar", "hello {{ place }}"); - context.put("place", "world"); + protected JinjavaInterpreter nestedInterpreter; + protected JinjavaInterpreter interpreter; - ExpressionNode node = fixture("simplevar"); - assertThat(node.render(interpreter).toString()).isEqualTo("hello world"); + @Before + public void setup() { + nestedInterpreter = + new Jinjava( + BaseJinjavaTest.newConfigBuilder().withNestedInterpretationEnabled(true).build() + ) + .newInterpreter(); + interpreter = + new Jinjava(BaseJinjavaTest.newConfigBuilder().build()).newInterpreter(); } @Test - public void itRendersResultWithoutNestedExpressionInterpretation() throws Exception { - final JinjavaConfig config = JinjavaConfig - .newBuilder() - .withNestedInterpretationEnabled(false) - .build(); - JinjavaInterpreter noNestedInterpreter = new Jinjava(config).newInterpreter(); - Context contextNoNestedInterpretation = noNestedInterpreter.getContext(); - contextNoNestedInterpretation.put("myvar", "hello {{ place }}"); - contextNoNestedInterpretation.put("place", "world"); + public void itRendersResultAsTemplateWhenContainingVarBlocks() throws Exception { + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter = a.value(); + nestedInterpreter.getContext().put("myvar", "hello {{ place }}"); + nestedInterpreter.getContext().put("place", "world"); - ExpressionNode node = fixture("simplevar"); - assertThat(node.render(noNestedInterpreter).toString()) - .isEqualTo("hello {{ place }}"); + ExpressionNode node = fixture("simplevar"); + assertThat(node.render(nestedInterpreter).toString()).isEqualTo("hello world"); + } } @Test - public void itRendersWithNestedExpressionInterpretationByDefault() throws Exception { - final JinjavaConfig config = JinjavaConfig.newBuilder().build(); - JinjavaInterpreter noNestedInterpreter = new Jinjava(config).newInterpreter(); - Context contextNoNestedInterpretation = noNestedInterpreter.getContext(); - contextNoNestedInterpretation.put("myvar", "hello {{ place }}"); - contextNoNestedInterpretation.put("place", "world"); + public void itRendersResultWithNestedExpressionInterpretation() throws Exception { + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter = a.value(); + nestedInterpreter.getContext().put("myvar", "hello {{ place }}"); + nestedInterpreter.getContext().put("place", "world"); + + ExpressionNode node = fixture("simplevar"); + assertThat(node.render(nestedInterpreter).toString()).isEqualTo("hello world"); + } + } - ExpressionNode node = fixture("simplevar"); - assertThat(node.render(noNestedInterpreter).toString()).isEqualTo("hello world"); + @Test + public void itRendersWithoutNestedExpressionInterpretationByDefault() throws Exception { + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter = a.value(); + interpreter.getContext().put("myvar", "hello {{ place }}"); + interpreter.getContext().put("place", "world"); + + ExpressionNode node = fixture("simplevar"); + assertThat(node.render(interpreter).toString()).isEqualTo("hello {{ place }}"); + } } @Test public void itRendersNestedTags() throws Exception { - final JinjavaConfig config = JinjavaConfig.newBuilder().build(); - JinjavaInterpreter jinjava = new Jinjava(config).newInterpreter(); - Context context = jinjava.getContext(); - context.put("myvar", "hello {% if (true) %}nasty{% endif %}"); + final JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() + .withNestedInterpretationEnabled(true) + .build(); + try (var a = JinjavaInterpreter.closeablePushCurrent(nestedInterpreter).get()) { + nestedInterpreter + .getContext() + .put("myvar", "hello {% if (true) %}nasty{% endif %}"); - ExpressionNode node = fixture("simplevar"); - assertThat(node.render(jinjava).toString()).isEqualTo("hello nasty"); + ExpressionNode node = fixture("simplevar"); + assertThat(node.render(nestedInterpreter).toString()).isEqualTo("hello nasty"); + } } @Test public void itAvoidsInfiniteRecursionWhenVarsContainBraceBlocks() throws Exception { - context.put("myvar", "hello {{ place }}"); - context.put("place", "{{ place }}"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter = a.value(); + interpreter.getContext().put("myvar", "hello {{ place }}"); + interpreter.getContext().put("place", "{{ place }}"); - ExpressionNode node = fixture("simplevar"); - assertThat(node.render(interpreter).toString()).isEqualTo("hello {{ place }}"); + ExpressionNode node = fixture("simplevar"); + assertThat(node.render(interpreter).toString()).isEqualTo("hello {{ place }}"); + } } @Test public void itAllowsNestedTagExpressions() throws Exception { - context.put("myvar", "{% if true %}{{ place }}{% endif %}"); - context.put("place", "{% if true %}Hello{% endif %}"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter = a.value(); + nestedInterpreter.getContext().put("myvar", "{% if true %}{{ place }}{% endif %}"); + nestedInterpreter.getContext().put("place", "{% if true %}Hello{% endif %}"); - ExpressionNode node = fixture("simplevar"); - assertThat(node.render(interpreter).toString()).isEqualTo("Hello"); + ExpressionNode node = fixture("simplevar"); + assertThat(node.render(nestedInterpreter).toString()).isEqualTo("Hello"); + } } @Test public void itAvoidsInfiniteRecursionWhenVarsAreInIfBlocks() throws Exception { - context.put("myvar", "{% if true %}{{ place }}{% endif %}"); - context.put("place", "{% if true %}{{ myvar }}{% endif %}"); - - ExpressionNode node = fixture("simplevar"); - assertThat(node.render(interpreter).toString()) - .isEqualTo("{% if true %}{{ myvar }}{% endif %}"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter = a.value(); + nestedInterpreter.getContext().put("myvar", "{% if true %}{{ place }}{% endif %}"); + nestedInterpreter.getContext().put("place", "{% if true %}{{ myvar }}{% endif %}"); + + ExpressionNode node = fixture("simplevar"); + assertThat(node.render(nestedInterpreter).toString()) + .isEqualTo("{% if true %}{{ myvar }}{% endif %}"); + } } @Test public void itDoesNotRescursivelyEvaluateExpressionsOfSelf() throws Exception { - context.put("myvar", "hello {{myvar}}"); - - ExpressionNode node = fixture("simplevar"); - // It renders once, and then stop further rendering after detecting recursion. - assertThat(node.render(interpreter).toString()).isEqualTo("hello hello {{myvar}}"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter = a.value(); + nestedInterpreter.getContext().put("myvar", "hello {{myvar}}"); + + ExpressionNode node = fixture("simplevar"); + // It renders once, and then stop further rendering after detecting recursion. + assertThat(node.render(nestedInterpreter).toString()) + .isEqualTo("hello hello {{myvar}}"); + } } @Test public void itDoesNotRescursivelyEvaluateExpressions() throws Exception { - context.put("myvar", "hello {{ place }}"); - context.put("place", "{{location}}"); - context.put("location", "this is a place."); - - ExpressionNode node = fixture("simplevar"); - assertThat(node.render(interpreter).toString()).isEqualTo("hello this is a place."); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter = a.value(); + nestedInterpreter.getContext().put("myvar", "hello {{ place }}"); + nestedInterpreter.getContext().put("place", "{{location}}"); + nestedInterpreter.getContext().put("location", "this is a place."); + + ExpressionNode node = fixture("simplevar"); + assertThat(node.render(nestedInterpreter).toString()) + .isEqualTo("hello this is a place."); + } } @Test public void itDoesNotRescursivelyEvaluateMoreExpressions() throws Exception { - context.put("myvar", "hello {{ place }}"); - context.put("place", "there, {{ location }}"); - context.put("location", "this is {{ place }}"); - - ExpressionNode node = fixture("simplevar"); - assertThat(node.render(interpreter).toString()) - .isEqualTo("hello there, this is {{ place }}"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter = a.value(); + nestedInterpreter.getContext().put("myvar", "hello {{ place }}"); + nestedInterpreter.getContext().put("place", "there, {{ location }}"); + nestedInterpreter.getContext().put("location", "this is {{ place }}"); + + ExpressionNode node = fixture("simplevar"); + assertThat(node.render(nestedInterpreter).toString()) + .isEqualTo("hello there, this is {{ place }}"); + } } @Test public void itRendersStringRange() throws Exception { - context.put("theString", "1234567890"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter = a.value(); + interpreter.getContext().put("theString", "1234567890"); - ExpressionNode node = fixture("string-range"); - assertThat(node.render(interpreter).toString()).isEqualTo("345"); + ExpressionNode node = fixture("string-range"); + assertThat(node.render(interpreter).toString()).isEqualTo("345"); + } + } + + @Test + public void itRenderEchoUndefined() { + final JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() + .withFeatureConfig( + FeatureConfig.newBuilder().add(ECHO_UNDEFINED, FeatureStrategies.ACTIVE).build() + ) + .build(); + try ( + var a = JinjavaInterpreter + .closeablePushCurrent(new Jinjava(config).newInterpreter()) + .get() + ) { + JinjavaInterpreter jinjavaInterpreter = a.value(); + jinjavaInterpreter.getContext().put("subject", "this"); + + String template = + "{{ subject | capitalize() }} expression {{ testing.template('hello_world') }} " + + "has a {{ unknown | lower() }} " + + "token but {{ unknown | default(\"replaced\") }} and empty {{ '' }}"; + Node node = new TreeParser(jinjavaInterpreter, template).buildTree(); + assertThat(jinjavaInterpreter.render(node)) + .isEqualTo( + "This expression {{ testing.template('hello_world') }} " + + "has a {{ unknown | lower() }} token but replaced and empty " + ); + } } @Test public void itFailsOnUnknownTokensVariables() throws Exception { - final JinjavaConfig config = JinjavaConfig - .newBuilder() + final JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withFailOnUnknownTokens(true) .build(); - JinjavaInterpreter jinjavaInterpreter = new Jinjava(config).newInterpreter(); - - String jinja = "{{ UnknownToken }}"; - Node node = new TreeParser(jinjavaInterpreter, jinja).buildTree(); - assertThatThrownBy(() -> jinjavaInterpreter.render(node)) - .isInstanceOf(UnknownTokenException.class) - .hasMessage("Unknown token found: UnknownToken"); + try ( + var a = JinjavaInterpreter + .closeablePushCurrent(new Jinjava(config).newInterpreter()) + .get() + ) { + JinjavaInterpreter jinjavaInterpreter = a.value(); + String jinja = "{{ UnknownToken }}"; + Node node = new TreeParser(jinjavaInterpreter, jinja).buildTree(); + assertThatThrownBy(() -> jinjavaInterpreter.render(node)) + .isInstanceOf(UnknownTokenException.class) + .hasMessage("Unknown token found: UnknownToken"); + } } @Test public void itFailsOnUnknownTokensOfLoops() throws Exception { - final JinjavaConfig config = JinjavaConfig - .newBuilder() + final JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withFailOnUnknownTokens(true) .build(); JinjavaInterpreter jinjavaInterpreter = new Jinjava(config).newInterpreter(); @@ -161,53 +240,95 @@ public void itFailsOnUnknownTokensOfLoops() throws Exception { @Test public void itFailsOnUnknownTokensOfIf() throws Exception { - final JinjavaConfig config = JinjavaConfig - .newBuilder() + final JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withFailOnUnknownTokens(true) .build(); - JinjavaInterpreter jinjavaInterpreter = new Jinjava(config).newInterpreter(); - - String jinja = "{% if bad %} BAD {% endif %}"; - Node node = new TreeParser(jinjavaInterpreter, jinja).buildTree(); - assertThatThrownBy(() -> jinjavaInterpreter.render(node)) - .isInstanceOf(UnknownTokenException.class) - .hasMessageContaining("Unknown token found: bad"); + try ( + var a = JinjavaInterpreter + .closeablePushCurrent(new Jinjava(config).newInterpreter()) + .get() + ) { + JinjavaInterpreter jinjavaInterpreter = a.value(); + String jinja = "{% if bad %} BAD {% endif %}"; + Node node = new TreeParser(jinjavaInterpreter, jinja).buildTree(); + assertThatThrownBy(() -> jinjavaInterpreter.render(node)) + .isInstanceOf(UnknownTokenException.class) + .hasMessageContaining("Unknown token found: bad"); + } } @Test public void itFailsOnUnknownTokensWithFilter() throws Exception { - final JinjavaConfig config = JinjavaConfig - .newBuilder() + final JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withFailOnUnknownTokens(true) .build(); - JinjavaInterpreter jinjavaInterpreter = new Jinjava(config).newInterpreter(); - - String jinja = "{{ UnknownToken }}"; - Node node = new TreeParser(jinjavaInterpreter, jinja).buildTree(); - assertThatThrownBy(() -> jinjavaInterpreter.render(node)) - .isInstanceOf(UnknownTokenException.class) - .hasMessage("Unknown token found: UnknownToken"); + try ( + var a = JinjavaInterpreter + .closeablePushCurrent(new Jinjava(config).newInterpreter()) + .get() + ) { + JinjavaInterpreter jinjavaInterpreter = a.value(); + String jinja = "{{ UnknownToken }}"; + Node node = new TreeParser(jinjavaInterpreter, jinja).buildTree(); + assertThatThrownBy(() -> jinjavaInterpreter.render(node)) + .isInstanceOf(UnknownTokenException.class) + .hasMessage("Unknown token found: UnknownToken"); + } } @Test public void valueExprWithOr() throws Exception { - context.put("a", "foo"); - context.put("b", "bar"); - context.put("c", ""); - context.put("d", 0); - - assertThat(val("{{ a or b }}")).isEqualTo("foo"); - assertThat(val("{{ c or a }}")).isEqualTo("foo"); - assertThat(val("{{ d or b }}")).isEqualTo("bar"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter = a.value(); + interpreter.getContext().put("a", "foo"); + interpreter.getContext().put("b", "bar"); + interpreter.getContext().put("c", ""); + interpreter.getContext().put("d", 0); + + assertThat(val("{{ a or b }}")).isEqualTo("foo"); + assertThat(val("{{ c or a }}")).isEqualTo("foo"); + assertThat(val("{{ d or b }}")).isEqualTo("bar"); + } } @Test public void itEscapesValueWhenContextSet() throws Exception { - context.put("a", "foo < bar"); - assertThat(val("{{ a }}")).isEqualTo("foo < bar"); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + interpreter = a.value(); + interpreter.getContext().put("a", "foo < bar"); + assertThat(val("{{ a }}")).isEqualTo("foo < bar"); - context.setAutoEscape(true); - assertThat(val("{{ a }}")).isEqualTo("foo < bar"); + interpreter.getContext().setAutoEscape(true); + assertThat(val("{{ a }}")).isEqualTo("foo < bar"); + } + } + + @Test + public void itIgnoresParseErrorsWhenFeatureIsEnabled() { + final JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() + .withFeatureConfig( + FeatureConfig + .newBuilder() + .add( + BuiltInFeatures.IGNORE_NESTED_INTERPRETATION_PARSE_ERRORS, + FeatureStrategies.ACTIVE + ) + .build() + ) + .build(); + try (var a = JinjavaInterpreter.closeablePushCurrent(interpreter).get()) { + nestedInterpreter = a.value(); + nestedInterpreter.getContext().put("myvar", "hello {% if"); + nestedInterpreter.getContext().put("place", "world"); + + ExpressionNode node = fixture("simplevar"); + + assertThat(node.render(nestedInterpreter).toString()).isEqualTo("hello {% if"); + assertThat(nestedInterpreter.getErrors()).isEmpty(); + } } private String val(String jinja) { diff --git a/src/test/java/com/hubspot/jinjava/tree/FailOnUnknownTokensTest.java b/src/test/java/com/hubspot/jinjava/tree/FailOnUnknownTokensTest.java index 9b6190b43..13042155f 100644 --- a/src/test/java/com/hubspot/jinjava/tree/FailOnUnknownTokensTest.java +++ b/src/test/java/com/hubspot/jinjava/tree/FailOnUnknownTokensTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.JinjavaInterpreter; @@ -13,11 +14,12 @@ import org.junit.Test; public class FailOnUnknownTokensTest { + private static Jinjava jinjava; @Before public void setUp() { - JinjavaConfig.Builder builder = JinjavaConfig.newBuilder(); + JinjavaConfig.Builder builder = BaseJinjavaTest.newConfigBuilder(); builder.withFailOnUnknownTokens(true); JinjavaConfig config = builder.build(); jinjava = new Jinjava(config); @@ -47,8 +49,8 @@ public void itReplacesTokensWithDefaultValues() { @Test public void itReplacesTokensInContextButThrowsExceptionForOthers() { - final JinjavaConfig config = JinjavaConfig - .newBuilder() + final JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() .withFailOnUnknownTokens(true) .build(); JinjavaInterpreter jinjavaInterpreter = new Jinjava(config).newInterpreter(); diff --git a/src/test/java/com/hubspot/jinjava/tree/TreeParserTest.java b/src/test/java/com/hubspot/jinjava/tree/TreeParserTest.java index ba5573ff9..7beba0733 100644 --- a/src/test/java/com/hubspot/jinjava/tree/TreeParserTest.java +++ b/src/test/java/com/hubspot/jinjava/tree/TreeParserTest.java @@ -4,8 +4,11 @@ import com.google.common.io.Resources; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; -import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.LegacyOverrides; +import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable; +import com.hubspot.jinjava.interpret.ErrorHandlingStrategy; import com.hubspot.jinjava.interpret.TemplateError.ErrorType; import java.nio.charset.StandardCharsets; import org.junit.Test; @@ -135,14 +138,34 @@ public void itStripsAllOuterWhiteSpaceWithComment() throws Exception { public void trimAndLstripBlocks() { interpreter = new Jinjava( - JinjavaConfig.newBuilder().withLstripBlocks(true).withTrimBlocks(true).build() + BaseJinjavaTest + .newConfigBuilder() + .withLstripBlocks(true) + .withTrimBlocks(true) + .build() ) - .newInterpreter(); + .newInterpreter(); assertThat(interpreter.render(parse("parse/tokenizer/whitespace-tags.jinja"))) .isEqualTo("
    \n" + " yay\n" + "
    \n"); } + @Test + public void trimAndLstripCommentBlocks() { + interpreter = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLstripBlocks(true) + .withTrimBlocks(true) + .build() + ) + .newInterpreter(); + + assertThat(interpreter.render(parse("parse/tokenizer/whitespace-comment-tags.jinja"))) + .isEqualTo("
    \n" + " yay\n" + " whoop\n" + "
    \n"); + } + @Test public void itWarnsAgainstMissingStartTags() { String expression = "{% if true %} foo {% endif %} {% endif %}"; @@ -164,6 +187,29 @@ public void itWarnsAgainstUnclosedComment() { assertThat(interpreter.getErrors().get(0).getFieldName()).isEqualTo("comment"); } + @Test + public void itWarnsAgainstUnclosedExpression() { + String expression = "foo {{ this is an unclosed expression %}"; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.getErrors()).hasSize(1); + assertThat(interpreter.getErrors().get(0).getSeverity()).isEqualTo(ErrorType.WARNING); + assertThat(interpreter.getErrors().get(0).getMessage()).isEqualTo("Unclosed token"); + assertThat(interpreter.getErrors().get(0).getFieldName()).isEqualTo("token"); + assertThat(interpreter.render(tree)) + .isEqualTo("foo {{ this is an unclosed expression %}"); + } + + @Test + public void itWarnsAgainstUnclosedTag() { + String expression = "foo {% this is an unclosed tag }}"; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.getErrors()).hasSize(1); + assertThat(interpreter.getErrors().get(0).getSeverity()).isEqualTo(ErrorType.WARNING); + assertThat(interpreter.getErrors().get(0).getMessage()).isEqualTo("Unclosed token"); + assertThat(interpreter.getErrors().get(0).getFieldName()).isEqualTo("token"); + assertThat(interpreter.render(tree)).isEqualTo("foo {% this is an unclosed tag }}"); + } + @Test public void itWarnsTwiceAgainstUnclosedForTag() { String expression = "{% for item in list %}\n{% for elem in items %}"; @@ -201,13 +247,168 @@ public void itWarnsTwiceAgainstUnclosedBlockTag() { assertThat(interpreter.getErrors().get(1).getLineno()).isEqualTo(1); } + @Test + public void itTrimsNotes() { + String expression = "A\n{#- note -#}\nB"; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo("AB"); + interpreter = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withUseTrimmingForNotesAndExpressions(false) + .build() + ) + .build() + ) + .newInterpreter(); + final Node newTree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(newTree)).isEqualTo("A\n\nB"); + } + + @Test + public void itAllowsTrailingNote() { + String expression = "A\n{# "; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo("A\n"); + interpreter = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides + .newBuilder() + .withUseTrimmingForNotesAndExpressions(true) + .build() + ) + .build() + ) + .newInterpreter(); + final Node newTree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(newTree)).isEqualTo("A\n"); + } + + @Test + public void itAllowsTrailingExpression() { + String expression = "A\n{{ "; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo("A\n{{ "); + interpreter = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides + .newBuilder() + .withUseTrimmingForNotesAndExpressions(true) + .build() + ) + .build() + ) + .newInterpreter(); + final Node newTree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(newTree)).isEqualTo("A\n{{ "); + } + + @Test + public void itAllowsTrailingTag() { + String expression = "A\n{% "; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo("A\n{% "); + interpreter = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides + .newBuilder() + .withUseTrimmingForNotesAndExpressions(true) + .build() + ) + .build() + ) + .newInterpreter(); + final Node newTree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(newTree)).isEqualTo("A\n{% "); + } + + @Test + public void itMergesTextNodesWhileRespectingTrim() { + String expression = "{% print 'A' -%}\n{#- note -#}\nB\n{%- print 'C' %}"; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo("ABC"); + } + + @Test + public void itTrimsExpressions() { + String expression = "A\n{{- 'B' -}}\nC"; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo("ABC"); + interpreter = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withUseTrimmingForNotesAndExpressions(false) + .build() + ) + .build() + ) + .newInterpreter(); + final Node newTree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(newTree)).isEqualTo("A\nB\nC"); + } + + @Test + public void itDoesNotMergeAdjacentTextNodesWhenLegacyOverrideIsApplied() { + String expression = "A\n{%- if true -%}\n{# comment #}\nB{% endif %}"; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo("A\nB"); + interpreter = + new Jinjava( + BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides.Builder + .from(LegacyOverrides.THREE_POINT_0) + .withAllowAdjacentTextNodes(false) + .build() + ) + .build() + ) + .newInterpreter(); + final Node overriddenTree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(overriddenTree)).isEqualTo("AB"); + } + + @Test + public void itDoesNotAddErrorWhenParseErrorsAreIgnored() { + try ( + TemporaryValueClosable c = interpreter + .getContext() + .withErrorHandlingStrategy(ErrorHandlingStrategy.ignoreAll()) + ) { + String expression = "{% if "; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(tree.getChildren()).hasSize(1); + assertThat(tree.getChildren().get(0).toTreeString()) + .isEqualToIgnoringWhitespace(" {~ {% if ~}"); + } + assertThat(interpreter.getErrors()).isEmpty(); + } + Node parse(String fixture) { try { return new TreeParser( interpreter, Resources.toString(Resources.getResource(fixture), StandardCharsets.UTF_8) ) - .buildTree(); + .buildTree(); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/test/java/com/hubspot/jinjava/tree/parse/CustomTokenScannerSymbolsTest.java b/src/test/java/com/hubspot/jinjava/tree/parse/CustomTokenScannerSymbolsTest.java index e3df96f09..7feb78f85 100644 --- a/src/test/java/com/hubspot/jinjava/tree/parse/CustomTokenScannerSymbolsTest.java +++ b/src/test/java/com/hubspot/jinjava/tree/parse/CustomTokenScannerSymbolsTest.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.lib.filter.JoinFilterTest.User; @@ -12,13 +13,17 @@ import org.junit.Test; public class CustomTokenScannerSymbolsTest { + private Jinjava jinjava; private JinjavaConfig config; @Before public void setup() { config = - JinjavaConfig.newBuilder().withTokenScannerSymbols(new CustomTokens()).build(); + BaseJinjavaTest + .newConfigBuilder() + .withTokenScannerSymbols(new CustomTokens()) + .build(); jinjava = new Jinjava(config); jinjava.getGlobalContext().put("numbers", Lists.newArrayList(1L, 2L, 3L, 4L, 5L)); } @@ -33,25 +38,25 @@ public void itRendersWithCustomTokens() { @Test public void itRendersFiltersWithCustomTokens() { assertThat( - jinjava.render( - "<% set d=d | default(\"some random value\") %><< d >>", - new HashMap<>() - ) + jinjava.render( + "<% set d=d | default(\"some random value\") %><< d >>", + new HashMap<>() ) + ) .isEqualTo("some random value"); assertThat(jinjava.render("<< [1, 2, 3, 3]|union(null) >>", new HashMap<>())) .isEqualTo("[1, 2, 3]"); assertThat(jinjava.render("<< numbers|select('equalto', 3) >>", new HashMap<>())) .isEqualTo("[3]"); assertThat( - jinjava.render( - "<< users|map(attribute='username')|join(', ') >>", - ImmutableMap.of( - "users", - (Object) Lists.newArrayList(new User("foo"), new User("bar")) - ) + jinjava.render( + "<< users|map(attribute='username')|join(', ') >>", + ImmutableMap.of( + "users", + (Object) Lists.newArrayList(new User("foo"), new User("bar")) ) ) + ) .isEqualTo("foo, bar"); } diff --git a/src/test/java/com/hubspot/jinjava/tree/parse/TagTokenTest.java b/src/test/java/com/hubspot/jinjava/tree/parse/TagTokenTest.java index 83bd6b96d..0eba63d63 100644 --- a/src/test/java/com/hubspot/jinjava/tree/parse/TagTokenTest.java +++ b/src/test/java/com/hubspot/jinjava/tree/parse/TagTokenTest.java @@ -7,6 +7,7 @@ import org.junit.Test; public class TagTokenTest { + private static final TokenScannerSymbols SYMBOLS = new DefaultTokenScannerSymbols(); @Test diff --git a/src/test/java/com/hubspot/jinjava/tree/parse/TokenScannerTest.java b/src/test/java/com/hubspot/jinjava/tree/parse/TokenScannerTest.java index 030e87fc9..4261b4fc2 100644 --- a/src/test/java/com/hubspot/jinjava/tree/parse/TokenScannerTest.java +++ b/src/test/java/com/hubspot/jinjava/tree/parse/TokenScannerTest.java @@ -6,8 +6,11 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.io.Resources; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.JinjavaConfig; -import com.hubspot.jinjava.LegacyOverrides; +import com.hubspot.jinjava.features.BuiltInFeatures; +import com.hubspot.jinjava.features.FeatureConfig; +import com.hubspot.jinjava.features.FeatureStrategies; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; @@ -16,6 +19,7 @@ import org.junit.Test; public class TokenScannerTest { + private JinjavaConfig config; private String script; @@ -24,7 +28,7 @@ public class TokenScannerTest { @Before public void setup() { - config = JinjavaConfig.newBuilder().build(); + config = BaseJinjavaTest.newConfigBuilder().build(); symbols = config.getTokenScannerSymbols(); } @@ -208,10 +212,10 @@ public void itProperlyTokenizesCommentWithTrailingTokens() { assertThat(tokens).hasSize(2); assertThat(tokens.get(tokens.size() - 1)).isInstanceOf(TextToken.class); assertThat( - StringUtils - .substringBetween(tokens.get(tokens.size() - 1).toString(), "{~", "~}") - .trim() - ) + StringUtils + .substringBetween(tokens.get(tokens.size() - 1).toString(), "{~", "~}") + .trim() + ) .isEqualTo("and here's some extra."); } @@ -265,13 +269,17 @@ public void testCommentWithWhitespaceChar() { List tokens = tokens("comment-without-whitespace"); assertThat(tokens.get(0).content.trim()).isEqualTo("$"); - LegacyOverrides legacyOverrides = LegacyOverrides - .newBuilder() - .withWhitespaceRequiredWithinTokens(true) - .build(); - JinjavaConfig config = JinjavaConfig - .newBuilder() - .withLegacyOverrides(legacyOverrides) + JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() + .withFeatureConfig( + FeatureConfig + .newBuilder() + .add( + BuiltInFeatures.WHITESPACE_REQUIRED_WITHIN_TOKENS, + FeatureStrategies.ACTIVE + ) + .build() + ) .build(); TokenScanner scanner = fixture("comment-without-whitespace", config); tokens = Lists.newArrayList(scanner); @@ -299,12 +307,33 @@ public void testEscapedBackslashWithinAttrValue() { @Test public void testLstripBlocks() { config = - JinjavaConfig.newBuilder().withLstripBlocks(true).withTrimBlocks(true).build(); + BaseJinjavaTest + .newConfigBuilder() + .withLstripBlocks(true) + .withTrimBlocks(true) + .build(); List tokens = tokens("tag-with-trim-chars"); assertThat(tokens).isNotEmpty(); } + @Test + public void itTreatsEscapedQuotesSameWhenNotInQuotes() { + List tokens = tokens("tag-with-all-escaped-quotes"); + assertThat(tokens).hasSize(8); + assertThat(tokens.stream().map(Token::getType).collect(Collectors.toList())) + .containsExactly( + symbols.getNote(), + symbols.getFixed(), + symbols.getTag(), + symbols.getFixed(), + symbols.getTag(), + symbols.getFixed(), + symbols.getTag(), + symbols.getFixed() + ); + } + private List tokens(String fixture) { TokenScanner t = fixture(fixture); return Lists.newArrayList(t); diff --git a/src/test/java/com/hubspot/jinjava/tree/parse/TokenWhitespaceTest.java b/src/test/java/com/hubspot/jinjava/tree/parse/TokenWhitespaceTest.java index ee79b1606..09c0f643a 100644 --- a/src/test/java/com/hubspot/jinjava/tree/parse/TokenWhitespaceTest.java +++ b/src/test/java/com/hubspot/jinjava/tree/parse/TokenWhitespaceTest.java @@ -4,6 +4,7 @@ import com.google.common.collect.Lists; import com.google.common.io.Resources; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.JinjavaConfig; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -21,6 +22,16 @@ public void trimBlocksTrimsAfterTag() { assertThat(tokens.get(2).getImage()).isEqualTo(" yay\n "); } + @Test + public void trimBlocksTrimsAfterCommentTag() { + List tokens = scanTokens( + "parse/tokenizer/whitespace-comment-tags.jinja", + trimBlocksConfig() + ); + assertThat(tokens.get(2).getImage()).isEqualTo(" yay\n "); + assertThat(tokens.get(4).getImage()).isEqualTo(" whoop\n\n"); + } + private List scanTokens(String srcPath, JinjavaConfig config) { try { return Lists.newArrayList( @@ -35,6 +46,6 @@ private List scanTokens(String srcPath, JinjavaConfig config) { } private JinjavaConfig trimBlocksConfig() { - return JinjavaConfig.newBuilder().withTrimBlocks(true).build(); + return BaseJinjavaTest.newConfigBuilder().withTrimBlocks(true).build(); } } diff --git a/src/test/java/com/hubspot/jinjava/util/DeferredValueUtilsTest.java b/src/test/java/com/hubspot/jinjava/util/DeferredValueUtilsTest.java index 6d8b9d72f..735d84361 100644 --- a/src/test/java/com/hubspot/jinjava/util/DeferredValueUtilsTest.java +++ b/src/test/java/com/hubspot/jinjava/util/DeferredValueUtilsTest.java @@ -2,7 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -23,6 +24,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @@ -41,9 +43,14 @@ public void itFindsGlobalProperties() { Optional.of(context) ); - Set deferredProperties = DeferredValueUtils.findAndMarkDeferredProperties( - context - ); + Context finalContext = context; + Set deferredProperties = context + .getDeferredNodes() + .stream() + .flatMap(node -> + DeferredValueUtils.findAndMarkDeferredProperties(finalContext, node).stream() + ) + .collect(Collectors.toSet()); assertThat(deferredProperties).contains("java_bean"); } @@ -55,9 +62,13 @@ public void itDefersWholePropertyOnArrayAccess() { ); context.put("array", Lists.newArrayList("a", "b", "c")); - Set deferredProperties = DeferredValueUtils.findAndMarkDeferredProperties( - context - ); + Set deferredProperties = context + .getDeferredNodes() + .stream() + .flatMap(node -> + DeferredValueUtils.findAndMarkDeferredProperties(context, node).stream() + ) + .collect(Collectors.toSet()); assertThat(deferredProperties).contains("array"); } @@ -68,9 +79,13 @@ public void itDefersWholePropertyOnDictAccess() { ); context.put("dict", Collections.singletonMap("a", "x")); - Set deferredProperties = DeferredValueUtils.findAndMarkDeferredProperties( - context - ); + Set deferredProperties = context + .getDeferredNodes() + .stream() + .flatMap(node -> + DeferredValueUtils.findAndMarkDeferredProperties(context, node).stream() + ) + .collect(Collectors.toSet()); assertThat(deferredProperties).contains("dict"); } @@ -92,7 +107,12 @@ public void itDefersTheCompleteObjectWhenAtLeastOnePropertyIsUsed() { Optional.of(context) ); - DeferredValueUtils.findAndMarkDeferredProperties(context); + Context finalContext = context; + context + .getDeferredNodes() + .forEach(node -> + DeferredValueUtils.findAndMarkDeferredProperties(finalContext, node) + ); assertThat(context.containsKey("java_bean")).isTrue(); assertThat(context.get("java_bean")).isInstanceOf(DeferredValue.class); DeferredValue deferredValue = (DeferredValue) context.get("java_bean"); @@ -114,8 +134,9 @@ public void itHandlesCaseWhereValueIsNull() { ) ); context.put("property", null); - DeferredValueUtils.findAndMarkDeferredProperties(context); - + context + .getDeferredNodes() + .forEach(node -> DeferredValueUtils.findAndMarkDeferredProperties(context, node)); assertThat(context.get("property")).isNull(); } @@ -134,7 +155,9 @@ public void itPreservesNonDeferredProperties() { context.put("deferred", "deferred"); context.put("not_deferred", "test_value"); - DeferredValueUtils.findAndMarkDeferredProperties(context); + context + .getDeferredNodes() + .forEach(node -> DeferredValueUtils.findAndMarkDeferredProperties(context, node)); assertThat(context.get("not_deferred")).isEqualTo("test_value"); } @@ -156,9 +179,8 @@ public void itRestoresContextSuccessfully() { context.put("java_bean_undeferred", javaBean); context.put("nested_map_undeferred", nestedMap); - HashMap result = DeferredValueUtils.getDeferredContextWithOriginalValues( - context - ); + HashMap result = + DeferredValueUtils.getDeferredContextWithOriginalValues(context); assertThat(result).contains(entry("simple_var", "SimpleVar")); assertThat(result).contains(entry("java_bean", javaBean)); assertThat(result).contains(entry("simple_bool", true)); @@ -178,9 +200,8 @@ public void itIgnoresUnrestorableValuesFromDeferredContext() { context.put("simple_var", DeferredValue.instance()); context.put("java_bean", DeferredValue.instance()); - HashMap result = DeferredValueUtils.getDeferredContextWithOriginalValues( - context - ); + HashMap result = + DeferredValueUtils.getDeferredContextWithOriginalValues(context); assertThat(result).isEmpty(); } @@ -188,16 +209,17 @@ public void itIgnoresUnrestorableValuesFromDeferredContext() { public void itDefersSetWordsInDeferredTokens() { Context context = new Context(); context.put("var_a", "a"); - DeferredToken deferredToken = new DeferredToken( - new TagToken( - "{% set var_a, var_b = deferred, deferred %}", - 1, - 1, - new DefaultTokenScannerSymbols() - ), - ImmutableSet.of(), - ImmutableSet.of("var_a", "var_b") - ); + DeferredToken deferredToken = DeferredToken + .builderFromToken( + new TagToken( + "{% set var_a, var_b = deferred, deferred %}", + 1, + 1, + new DefaultTokenScannerSymbols() + ) + ) + .addSetDeferredWords(ImmutableSet.of("var_a", "var_b")) + .build(); context.handleDeferredToken(deferredToken); assertThat(context.get("var_a")).isInstanceOf(DeferredValue.class); assertThat(context.get("var_b")).isInstanceOf(DeferredValue.class); @@ -207,20 +229,55 @@ public void itDefersSetWordsInDeferredTokens() { public void itDefersUsedWordsInDeferredTokens() { Context context = new Context(); context.put("var_a", "a"); - DeferredToken deferredToken = new DeferredToken( - new ExpressionToken( - "{{ var_a.append(deferred|int)}}", - 1, - 1, - new DefaultTokenScannerSymbols() - ), - ImmutableSet.of("var_a", "int") - ); + DeferredToken deferredToken = DeferredToken + .builderFromToken( + new ExpressionToken( + "{{ var_a.append(deferred|int)}}", + 1, + 1, + new DefaultTokenScannerSymbols() + ) + ) + .addUsedDeferredWords(ImmutableSet.of("var_a", "int")) + .build(); + context.handleDeferredToken(deferredToken); assertThat(context.get("var_a")).isInstanceOf(DeferredValue.class); assertThat(context.containsKey("int")).isFalse(); } + @Test + public void itFindsFirstValidDeferredWords() { + DeferredToken deferredToken = DeferredToken + .builderFromToken( + new ExpressionToken("{{ blah }}", 1, 1, new DefaultTokenScannerSymbols()) + ) + .addUsedDeferredWords(ImmutableSet.of("deferred", ".attribute1")) + .addSetDeferredWords(ImmutableSet.of("deferred", ".attribute2")) + .build(); + + assertThat(deferredToken.getUsedDeferredBases()) + .isEqualTo(ImmutableSet.of("deferred", "attribute1")); + assertThat(deferredToken.getSetDeferredBases()) + .isEqualTo(ImmutableSet.of("deferred", "attribute2")); + } + + @Test + public void itFindsFirstValidDeferredWordsWithNestedAttributes() { + DeferredToken deferredToken = DeferredToken + .builderFromToken( + new ExpressionToken("{{ blah }}", 1, 1, new DefaultTokenScannerSymbols()) + ) + .addUsedDeferredWords(ImmutableSet.of("deferred", ".attribute1.ignore")) + .addSetDeferredWords(ImmutableSet.of("deferred", ".attribute2.ignoreme")) + .build(); + + assertThat(deferredToken.getUsedDeferredBases()) + .isEqualTo(ImmutableSet.of("deferred", "attribute1")); + assertThat(deferredToken.getSetDeferredBases()) + .isEqualTo(ImmutableSet.of("deferred", "attribute2")); + } + private Context getContext(List nodes) { return getContext(nodes, Optional.empty()); } @@ -278,6 +335,7 @@ private JavaBean getPopulatedJavaBean() { } private class JavaBean { + String propertyOne; String propertyTwo; diff --git a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java index 104ae7d64..1065349cb 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerExpressionResolverTest.java @@ -4,8 +4,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; -import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.el.ext.AbstractCallableMethod; import com.hubspot.jinjava.interpret.Context; @@ -18,12 +18,14 @@ import com.hubspot.jinjava.objects.collections.PyMap; import com.hubspot.jinjava.objects.date.PyishDate; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; -import com.hubspot.jinjava.objects.serialization.PyishSerializable; import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy; +import com.hubspot.jinjava.testobjects.EagerExpressionResolverTestObjects; import com.hubspot.jinjava.tree.parse.DefaultTokenScannerSymbols; 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.math.BigDecimal; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -33,6 +35,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicLong; import org.junit.After; @@ -40,6 +43,7 @@ import org.junit.Test; public class EagerExpressionResolverTest { + private static final TokenScannerSymbols SYMBOLS = new DefaultTokenScannerSymbols(); private JinjavaInterpreter interpreter; @@ -53,13 +57,14 @@ public void setUp() throws Exception { private JinjavaInterpreter getInterpreter(boolean evaluateMapKeys) throws Exception { Jinjava jinjava = new Jinjava( - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(EagerExecutionMode.instance()) .withRandomNumberGeneratorStrategy(RandomNumberGeneratorStrategy.DEFERRED) .withLegacyOverrides( LegacyOverrides.newBuilder().withEvaluateMapKeys(evaluateMapKeys).build() ) + .withNestedInterpretationEnabled(true) .build() ); jinjava @@ -89,6 +94,15 @@ private JinjavaInterpreter getInterpreter(boolean evaluateMapKeys) throws Except this.getClass().getDeclaredMethod("sleeper") ) ); + jinjava + .getGlobalContext() + .registerFunction( + new ELFunctionDefinition( + "", + "optionally", + this.getClass().getDeclaredMethod("optionally", boolean.class) + ) + ); interpreter = new JinjavaInterpreter(jinjava.newInterpreter()); context = interpreter.getContext(); context.put("deferred", DeferredValue.instance()); @@ -120,6 +134,14 @@ public void itResolvesDeferredBoolean() { assertThat(interpreter.resolveELExpression(partiallyResolved, 1)).isEqualTo(true); } + @Test + public void itSerializesNestedOptional() { + assertThat(eagerResolveExpression("[optionally(true)]").toString()) + .isEqualTo("['1']"); + assertThat(eagerResolveExpression("[optionally(false)]").toString()) + .isEqualTo("[null]"); + } + @Test public void itResolvesDeferredList() { context.put("foo", "foo_val"); @@ -251,6 +273,7 @@ public void itSplitsAndIndexesOnNonWords() { @Test public void itSupportsOrderOfOperations() { + eagerResolveExpression("['a','b'][1]"); EagerExpressionResult eagerExpressionResult = eagerResolveExpression( "[0,1]|reverse + deferred" ); @@ -302,7 +325,7 @@ public void itEvaluatesDict() { } @Test - public void itSerializesDateProperly() { + public void itSerializesDateProperly() throws IOException { PyishDate date = new PyishDate( ZonedDateTime.ofInstant(Instant.ofEpochMilli(1234567890L), ZoneId.systemDefault()) ); @@ -310,7 +333,18 @@ public void itSerializesDateProperly() { EagerExpressionResult eagerExpressionResult = eagerResolveExpression("date"); assertThat(WhitespaceUtils.unquoteAndUnescape(eagerExpressionResult.toString())) - .isEqualTo(date.toPyishString().replace("'", "\\'").replace('"', '\'')); + .isEqualTo(date.appendPyishString(new StringBuilder()).toString()); + interpreter.render( + "{% set foo = " + + PyishObjectMapper.getAsPyishString(ImmutableMap.of("a", date)) + + " %}" + ); + assertThat( + ((PyishDate) ((Map) interpreter.getContext().get("foo")).get( + "a" + )).toDateTime() + ) + .isEqualTo(date.toDateTime()); } @Test @@ -324,6 +358,15 @@ public void itHandlesSingleQuotes() { .isEqualTo("' & ' & '\""); } + @Test + public void itHandlesEscapedSlashBeforeQuoteProperly() { + EagerExpressionResult eagerExpressionResult = eagerResolveExpression( + "deferred|replace('\\\\', '.')" + ); + assertThat(eagerExpressionResult.getDeferredWords()) + .containsExactlyInAnyOrder("deferred", "replace.filter"); + } + @Test public void itHandlesNewlines() { context.put("foo", "\n"); @@ -357,14 +400,14 @@ public void itHandlesCancellingSlashes() { @Test public void itOutputsEmptyForVoidFunctions() throws Exception { assertThat( - WhitespaceUtils.unquoteAndUnescape(interpreter.render("{{ void_function(2) }}")) - ) + WhitespaceUtils.unquoteAndUnescape(interpreter.render("{{ void_function(2) }}")) + ) .isEmpty(); assertThat( - WhitespaceUtils.unquoteAndUnescape( - eagerResolveExpression("void_function(2)").toString() - ) + WhitespaceUtils.unquoteAndUnescape( + eagerResolveExpression("void_function(2)").toString() ) + ) .isEmpty(); } @@ -407,15 +450,17 @@ public void itDoesntSplitOnBar() { public void itDoesntResolveNonPyishSerializable() { PyMap dict = new PyMap(new HashMap<>()); context.put("dict", dict); - context.put("foo", new Foo("bar")); + context.put("foo", new EagerExpressionResolverTestObjects.Foo("bar")); context.put("mark", "!"); EagerExpressionResult eagerExpressionResult = eagerResolveExpression( "dict.update({'foo': foo})" ); assertThat(WhitespaceUtils.unquoteAndUnescape(eagerExpressionResult.toString())) .isEqualTo(""); - assertThat(dict.get("foo")).isInstanceOf(Foo.class); - assertThat(((Foo) dict.get("foo")).bar()).isEqualTo("bar"); + assertThat(dict.get("foo")) + .isInstanceOf(EagerExpressionResolverTestObjects.Foo.class); + assertThat(((EagerExpressionResolverTestObjects.Foo) dict.get("foo")).bar()) + .isEqualTo("bar"); } @Test @@ -555,19 +600,22 @@ public void itHandlesNegativeZero() { @Test public void itHandlesPyishSerializable() { - context.put("foo", new SomethingPyish("yes")); + context.put("foo", new EagerExpressionResolverTestObjects.SomethingPyish("yes")); assertThat( - interpreter.render( - String.format("{{ %s.name }}", eagerResolveExpression("foo").toString()) - ) + interpreter.render( + String.format("{{ %s.name }}", eagerResolveExpression("foo").toString()) ) + ) .isEqualTo("yes"); } @Test public void itHandlesPyishSerializableWithProcessingException() { - context.put("foo", new SomethingExceptionallyPyish("yes")); - context.getMetaContextVariables().add("foo"); + context.put( + "foo", + new EagerExpressionResolverTestObjects.SomethingExceptionallyPyish("yes") + ); + context.addMetaContextVariables(Collections.singleton("foo")); assertThat(interpreter.render("{{ deferred && (1 == 2 || foo) }}")) .isEqualTo("{{ deferred && (false || foo) }}"); } @@ -616,6 +664,67 @@ public void itHandlesDeferredChoice() { .isEqualTo("deferred"); } + @Test + public void itGetsDeferredWordsFromNestedExpression() { + 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 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 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(expected = DeferredValueException.class) + 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"); @@ -661,7 +770,6 @@ public void itHandlesDeferredBracketMethod() throws NoSuchMethodException { new PyList( Collections.singletonList( new AbstractCallableMethod("echo", map) { - @Override public Object doEvaluate( Map argMap, @@ -683,10 +791,10 @@ public Object doEvaluate( @Test public void itHandlesOrOperator() { assertThat( - WhitespaceUtils.unquoteAndUnescape( - eagerResolveExpression("false == true || (true) ? 'yes' : 'no'").toString() - ) + WhitespaceUtils.unquoteAndUnescape( + eagerResolveExpression("false == true || (true) ? 'yes' : 'no'").toString() ) + ) .isEqualTo("yes"); } @@ -698,16 +806,24 @@ public void itSplitsResolvedExpression() { } @Test - public void itHandlesToday() { - context.put("foo", "bar"); - assertThat(eagerResolveExpression("foo ~ today()").toString()) - .isEqualTo("'bar' ~ today()"); + public void itDoesNotSplitJsonResolvedExpression() { + eagerResolveExpression("{'a': 1, 'b': 2}"); + assertThat(context.getResolvedExpressions()) + .containsExactlyInAnyOrder("{'a': 1, 'b': 2}"); + } + + @Test + public void itDoesNotSplitJsonInArrayResolvedExpression() { + // It should not add the broke JSON `{'a': 1` to resolved expressions. + eagerResolveExpression("[{'a': 1, 'b': 2}]"); + assertThat(context.getResolvedExpressions()) + .containsExactlyInAnyOrder("[{'a': 1, 'b': 2}]"); } @Test public void itHandlesRandom() { assertThat(eagerResolveExpression("range(1)|random").toString()) - .isEqualTo("filter:random.filter([0], ____int3rpr3t3r____)"); + .isEqualTo("filter:random.filter(range(1), ____int3rpr3t3r____)"); } @Test @@ -794,48 +910,23 @@ public static long sleeper() throws InterruptedException { return sleepTime; } - private static class Foo { - private final String bar; - - Foo(String bar) { - this.bar = bar; - } - - String bar() { - return bar; - } - - String echo(String toEcho) { - return toEcho; - } + public static Optional optionally(boolean hasValue) { + return Optional.of(hasValue).filter(Boolean::booleanValue).map(ignored -> "1"); } - public class SomethingPyish implements PyishSerializable { - private String name; - - public SomethingPyish(String name) { - this.name = name; - } - - public String getName() { - return name; - } + @Test + public void itCountsBigDecimalAsPrimitive() { + assertThat(EagerExpressionResolver.isResolvableObject(new BigDecimal("2.1E7"))) + .isTrue(); } - public class SomethingExceptionallyPyish implements PyishSerializable { - private String name; - - public SomethingExceptionallyPyish(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public String toPyishString() { - throw new DeferredValueException("Can't serialize"); - } + @Test + public void itCountsOptionalAsResolvable() { + assertThat( + EagerExpressionResolver.isResolvableObject( + ImmutableList.of(Optional.of(123), Optional.empty()) + ) + ) + .isTrue(); } } diff --git a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java index 7df72f1ca..a2e6b0b81 100644 --- a/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java +++ b/src/test/java/com/hubspot/jinjava/util/EagerReconstructionUtilsTest.java @@ -2,17 +2,22 @@ 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.mock; +import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.JinjavaConfig; 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.LazyExpression; import com.hubspot.jinjava.interpret.OutputTooBigException; import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.lib.fn.eager.EagerMacroFunction; import com.hubspot.jinjava.lib.tag.eager.DeferredToken; import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult; import com.hubspot.jinjava.mode.DefaultExecutionMode; @@ -23,7 +28,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; @@ -38,6 +42,7 @@ @RunWith(MockitoJUnitRunner.class) public class EagerReconstructionUtilsTest extends BaseInterpretingTest { + private static final long MAX_OUTPUT_SIZE = 50L; @Before @@ -46,8 +51,8 @@ public void eagerSetup() throws Exception { new JinjavaInterpreter( jinjava, context, - JinjavaConfig - .newBuilder() + BaseJinjavaTest + .newConfigBuilder() .withMaxOutputSize(MAX_OUTPUT_SIZE) .withExecutionMode(EagerExecutionMode.instance()) .build() @@ -64,15 +69,13 @@ public void teardown() { @Test public void itExecutesInChildContextAndTakesNewValue() { context.put("foo", new PyList(new ArrayList<>())); - EagerExecutionResult result = EagerReconstructionUtils.executeInChildContext( - ( - interpreter1 -> { + EagerExecutionResult result = EagerContextWatcher.executeInChildContext( + (interpreter1 -> { ((List) interpreter1.getContext().get("foo")).add(1); return EagerExpressionResult.fromString("function return"); - } - ), + }), interpreter, - EagerChildContextConfig + EagerContextWatcher.EagerChildContextConfig .newBuilder() .withTakeNewValue(true) .withForceDeferredExecutionMode(true) @@ -85,31 +88,31 @@ public void itExecutesInChildContextAndTakesNewValue() { assertThat(result.getResult().toString()).isEqualTo("function return"); // This will add an eager token because we normally don't call this method // unless we're in deferred execution mode. - assertThat(result.getPrefixToPreserveState()).isEqualTo("{% set foo = [1] %}"); + assertThat(result.getPrefixToPreserveState().toString()) + .isEqualTo("{% set foo = [1] %}"); } @Test public void itExecutesInChildContextAndDefersNewValue() { context.put("foo", new ArrayList()); - EagerExecutionResult result = EagerReconstructionUtils.executeInChildContext( - ( - interpreter1 -> { + EagerExecutionResult result = EagerContextWatcher.executeInChildContext( + (interpreter1 -> { context.put( "foo", DeferredValue.instance(interpreter1.getContext().get("foo")) ); return EagerExpressionResult.fromString("function return"); - } - ), + }), interpreter, - EagerChildContextConfig + EagerContextWatcher.EagerChildContextConfig .newBuilder() .withForceDeferredExecutionMode(true) .withCheckForContextChanges(true) .build() ); assertThat(result.getResult().toString()).isEqualTo("function return"); - assertThat(result.getPrefixToPreserveState()).isEqualTo("{% set foo = [] %}"); + assertThat(result.getPrefixToPreserveState().toString()) + .isEqualTo("{% set foo = [] %}"); assertThat(context.get("foo")).isInstanceOf(DeferredValue.class); } @@ -161,12 +164,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 @@ -188,12 +193,12 @@ public void itReconstructsVariablesAndMacroFunctions() { @Test public void itBuildsSetTagForDeferredAndRegisters() { - Map deferredValuesToSet = ImmutableMap.of("foo", "'bar'"); - String result = EagerReconstructionUtils.buildSetTag( - deferredValuesToSet, - interpreter, - true - ); + String result = + EagerReconstructionUtils.buildBlockOrInlineSetTagAndRegisterDeferredToken( + "foo", + "bar", + interpreter + ); assertThat(result).isEqualTo("{% set foo = 'bar' %}"); assertThat(context.getDeferredTokens()).hasSize(1); DeferredToken deferredToken = context @@ -207,11 +212,10 @@ public void itBuildsSetTagForDeferredAndRegisters() { @Test public void itBuildsSetTagForDeferredAndDoesntRegister() { - Map deferredValuesToSet = ImmutableMap.of("foo", "'bar'"); - String result = EagerReconstructionUtils.buildSetTag( - deferredValuesToSet, - interpreter, - false + String result = EagerReconstructionUtils.buildBlockOrInlineSetTag( + "foo", + "bar", + interpreter ); assertThat(result).isEqualTo("{% set foo = 'bar' %}"); assertThat(context.getDeferredTokens()).isEmpty(); @@ -243,9 +247,12 @@ public void itLimitsSetTagConstruction() { for (int i = 0; i < MAX_OUTPUT_SIZE; i++) { tooLong.append(i); } - Map deferredValuesToSet = ImmutableMap.of("foo", tooLong.toString()); - assertThatThrownBy( - () -> EagerReconstructionUtils.buildSetTag(deferredValuesToSet, interpreter, true) + assertThatThrownBy(() -> + EagerReconstructionUtils.buildBlockOrInlineSetTagAndRegisterDeferredToken( + "foo", + tooLong.toString(), + interpreter + ) ) .isInstanceOf(OutputTooBigException.class); } @@ -253,48 +260,48 @@ public void itLimitsSetTagConstruction() { @Test public void itWrapsInRawTag() { String toWrap = "{{ foo }}"; - JinjavaConfig preserveRawConfig = JinjavaConfig - .newBuilder() + JinjavaConfig preserveRawConfig = BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(PreserveRawExecutionMode.instance()) .build(); assertThat( - EagerReconstructionUtils.wrapInRawIfNeeded( - toWrap, - new JinjavaInterpreter(jinjava, context, preserveRawConfig) - ) + EagerReconstructionUtils.wrapInRawIfNeeded( + toWrap, + new JinjavaInterpreter(jinjava, context, preserveRawConfig) ) + ) .isEqualTo(String.format("{%% raw %%}%s{%% endraw %%}", toWrap)); } @Test public void itDoesntWrapInRawTagUnnecessarily() { String toWrap = "foo"; - JinjavaConfig preserveRawConfig = JinjavaConfig - .newBuilder() + JinjavaConfig preserveRawConfig = BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(PreserveRawExecutionMode.instance()) .build(); assertThat( - EagerReconstructionUtils.wrapInRawIfNeeded( - toWrap, - new JinjavaInterpreter(jinjava, context, preserveRawConfig) - ) + EagerReconstructionUtils.wrapInRawIfNeeded( + toWrap, + new JinjavaInterpreter(jinjava, context, preserveRawConfig) ) + ) .isEqualTo(toWrap); } @Test public void itDoesntWrapInRawTagForDefaultConfig() { - JinjavaConfig defaultConfig = JinjavaConfig - .newBuilder() + JinjavaConfig defaultConfig = BaseJinjavaTest + .newConfigBuilder() .withExecutionMode(DefaultExecutionMode.instance()) .build(); String toWrap = "{{ foo }}"; assertThat( - EagerReconstructionUtils.wrapInRawIfNeeded( - toWrap, - new JinjavaInterpreter(jinjava, context, defaultConfig) - ) + EagerReconstructionUtils.wrapInRawIfNeeded( + toWrap, + new JinjavaInterpreter(jinjava, context, defaultConfig) ) + ) .isEqualTo(toWrap); } @@ -329,21 +336,66 @@ public void itIgnoresMetaContextVariables() { .getContext() .put(Context.IMPORT_RESOURCE_ALIAS_KEY, DeferredValue.instance()); assertThat( - EagerReconstructionUtils.reconstructFromContextBeforeDeferring( - Collections.singleton(Context.IMPORT_RESOURCE_ALIAS_KEY), - interpreter - ) + EagerReconstructionUtils.reconstructFromContextBeforeDeferring( + Collections.singleton(Context.IMPORT_RESOURCE_ALIAS_KEY), + interpreter ) + ) .isEmpty(); } - private static MacroFunction getMockMacroFunction(String image) { - MacroFunction mockMacroFunction = mock(MacroFunction.class); - when(mockMacroFunction.getName()).thenReturn("foo"); - when(mockMacroFunction.getArguments()).thenReturn(ImmutableList.of("bar")); - when(mockMacroFunction.getEvaluationResult(anyMap(), anyMap(), anyList(), any())) - .thenReturn(image.substring(image.indexOf("%}") + 2, image.lastIndexOf("{%"))); - return mockMacroFunction; + @Test + public void itDiscardsSessionBindings() { + interpreter.getContext().put("foo", "bar"); + EagerExecutionResult withSessionBindings = EagerContextWatcher.executeInChildContext( + eagerInterpreter -> { + interpreter.getContext().put("foo", "foobar"); + return EagerExpressionResult.fromString(""); + }, + interpreter, + EagerContextWatcher.EagerChildContextConfig + .newBuilder() + .withDiscardSessionBindings(false) + .withCheckForContextChanges(true) + .build() + ); + EagerExecutionResult withoutSessionBindings = + EagerContextWatcher.executeInChildContext( + eagerInterpreter -> { + interpreter.getContext().put("foo", "foobar"); + return EagerExpressionResult.fromString(""); + }, + interpreter, + EagerContextWatcher.EagerChildContextConfig + .newBuilder() + .withDiscardSessionBindings(true) + .withCheckForContextChanges(true) + .build() + ); + assertThat(withSessionBindings.getSpeculativeBindings()) + .containsEntry("foo", "foobar"); + assertThat(withoutSessionBindings.getSpeculativeBindings()).doesNotContainKey("foo"); + } + + @Test + public void itDoesNotBreakOnNullLazyExpressions() { + interpreter.getContext().put("foo", LazyExpression.of(() -> null, "")); + EagerContextWatcher.executeInChildContext( + eagerInterpreter -> + EagerExpressionResult.fromString(interpreter.render("{% set foo = 'bar' %}")), + interpreter, + EagerContextWatcher.EagerChildContextConfig + .newBuilder() + .withDiscardSessionBindings(false) + .withCheckForContextChanges(true) + .withTakeNewValue(true) + .build() + ); + } + + private EagerMacroFunction getMockMacroFunction(String image) { + interpreter.render(image); + return (EagerMacroFunction) interpreter.getContext().getGlobalMacro("foo"); } private static TagNode getMockTagNode(String endName) { diff --git a/src/test/java/com/hubspot/jinjava/util/ForLoopTest.java b/src/test/java/com/hubspot/jinjava/util/ForLoopTest.java index 826312fca..262485438 100644 --- a/src/test/java/com/hubspot/jinjava/util/ForLoopTest.java +++ b/src/test/java/com/hubspot/jinjava/util/ForLoopTest.java @@ -23,9 +23,11 @@ import org.junit.Test; public class ForLoopTest { + private static final int NULL_VAL = Integer.MIN_VALUE; public static class AIterator implements Iterator { + int i = 0; @Override diff --git a/src/test/java/com/hubspot/jinjava/util/HelperStringTokenizerTest.java b/src/test/java/com/hubspot/jinjava/util/HelperStringTokenizerTest.java index 80964a74f..275074d29 100644 --- a/src/test/java/com/hubspot/jinjava/util/HelperStringTokenizerTest.java +++ b/src/test/java/com/hubspot/jinjava/util/HelperStringTokenizerTest.java @@ -21,6 +21,7 @@ import org.junit.Test; public class HelperStringTokenizerTest { + private HelperStringTokenizer tk; @Test @@ -104,11 +105,37 @@ public void test8() { @Test public void itDoesntReturnTrailingNull() { assertThat( - new HelperStringTokenizer("product in collections.frontpage.products ") - .splitComma(true) - .allTokens() - ) + new HelperStringTokenizer("product in collections.frontpage.products ") + .splitComma(true) + .allTokens() + ) .containsExactly("product", "in", "collections.frontpage.products") .doesNotContainNull(); } + + @Test + public void itHandlesEscapedQuotesWithinQuotedStrings() { + assertThat( + new HelperStringTokenizer("'hi','y\\'all don\\'t'").splitComma(true).allTokens() + ) + .containsExactly("'hi'", "'y\\'all don\\'t'"); + } + + @Test + public void itHandlesEscapedDoubleQuotesWithinQuotedStrings() { + assertThat( + new HelperStringTokenizer("\"hi\",\"say \\\"hello\\\"\"") + .splitComma(true) + .allTokens() + ) + .containsExactly("\"hi\"", "\"say \\\"hello\\\"\""); + } + + @Test + public void itHandlesEscapedBackslashes() { + assertThat( + new HelperStringTokenizer("'path\\\\to\\file'").splitComma(true).allTokens() + ) + .containsExactly("'path\\\\to\\file'"); + } } 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); } } diff --git a/src/test/java/com/hubspot/jinjava/util/ObjectIteratorTest.java b/src/test/java/com/hubspot/jinjava/util/ObjectIteratorTest.java index e5ea61104..f77786b50 100644 --- a/src/test/java/com/hubspot/jinjava/util/ObjectIteratorTest.java +++ b/src/test/java/com/hubspot/jinjava/util/ObjectIteratorTest.java @@ -18,8 +18,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import com.hubspot.jinjava.BaseJinjavaTest; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.util.ArrayList; @@ -29,6 +31,7 @@ import org.junit.Test; public class ObjectIteratorTest { + private Object items = null; private ForLoop loop = null; @@ -98,9 +101,11 @@ public void testItIteratesOverValues() { @Test public void testItIteratesOverKeys() throws Exception { - JinjavaConfig config = JinjavaConfig - .newBuilder() - .withIterateOverMapKeys(true) + JinjavaConfig config = BaseJinjavaTest + .newConfigBuilder() + .withLegacyOverrides( + LegacyOverrides.newBuilder().withIterateOverMapKeys(true).build() + ) .build(); JinjavaInterpreter.pushCurrent( new JinjavaInterpreter(new Jinjava(), new Context(), config) diff --git a/src/test/java/com/hubspot/jinjava/util/ObjectTruthValueTest.java b/src/test/java/com/hubspot/jinjava/util/ObjectTruthValueTest.java index bcb4c7750..f02f53cfa 100644 --- a/src/test/java/com/hubspot/jinjava/util/ObjectTruthValueTest.java +++ b/src/test/java/com/hubspot/jinjava/util/ObjectTruthValueTest.java @@ -45,6 +45,7 @@ private void checkNumberTruthiness(Object a, Object b) { } private class TestObject implements HasObjectTruthValue { + private boolean objectTruthValue = false; public TestObject setObjectTruthValue(boolean objectTruthValue) { diff --git a/src/test/java/com/hubspot/jinjava/util/RenderLimitUtilsTest.java b/src/test/java/com/hubspot/jinjava/util/RenderLimitUtilsTest.java new file mode 100644 index 000000000..1e0bca06d --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/util/RenderLimitUtilsTest.java @@ -0,0 +1,47 @@ +package com.hubspot.jinjava.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.JinjavaConfig; +import org.junit.Test; + +public class RenderLimitUtilsTest { + + @Test + public void itPicksLowerLimitWhenConfigIsSet() { + assertThat( + RenderLimitUtils.clampProvidedRenderLimitToConfig(100, configWithOutputSize(10)) + ) + .isEqualTo(10); + } + + @Test + public void itKeepsConfigLimitWhenConfigSetAndUnlimitedProvided() { + assertThat( + RenderLimitUtils.clampProvidedRenderLimitToConfig(0, configWithOutputSize(10)) + ) + .isEqualTo(10); + assertThat( + RenderLimitUtils.clampProvidedRenderLimitToConfig(-10, configWithOutputSize(10)) + ) + .isEqualTo(10); + } + + @Test + public void itUsesProvidedLimitWhenConfigIsUnlimited() { + assertThat( + RenderLimitUtils.clampProvidedRenderLimitToConfig(10, configWithOutputSize(0)) + ) + .isEqualTo(10); + + assertThat( + RenderLimitUtils.clampProvidedRenderLimitToConfig(10, configWithOutputSize(-10)) + ) + .isEqualTo(10); + } + + private JinjavaConfig configWithOutputSize(long size) { + return BaseJinjavaTest.newConfigBuilder().withMaxOutputSize(size).build(); + } +} diff --git a/src/test/java/com/hubspot/jinjava/util/ScopeMapTest.java b/src/test/java/com/hubspot/jinjava/util/ScopeMapTest.java index f33ed7b26..66f064568 100644 --- a/src/test/java/com/hubspot/jinjava/util/ScopeMapTest.java +++ b/src/test/java/com/hubspot/jinjava/util/ScopeMapTest.java @@ -10,6 +10,7 @@ import org.junit.Test; public class ScopeMapTest { + Map a, b, c; @Before diff --git a/src/test/resources/eager/allows-deferred-lazy-reference-to-get-overridden/test.expected.expected.jinja b/src/test/resources/eager/allows-deferred-lazy-reference-to-get-overridden/test.expected.expected.jinja new file mode 100644 index 000000000..830e009cf --- /dev/null +++ b/src/test/resources/eager/allows-deferred-lazy-reference-to-get-overridden/test.expected.expected.jinja @@ -0,0 +1,2 @@ +B: ['resolved', 'B']. +A: ['a', 'b', 'A']. diff --git a/src/test/resources/eager/allows-deferred-lazy-reference-to-get-overridden/test.expected.jinja b/src/test/resources/eager/allows-deferred-lazy-reference-to-get-overridden/test.expected.jinja new file mode 100644 index 000000000..07854f3c5 --- /dev/null +++ b/src/test/resources/eager/allows-deferred-lazy-reference-to-get-overridden/test.expected.jinja @@ -0,0 +1,12 @@ +{% set a_list = ['a', 'b'] %}\ +{% for __ignored__ in [0] %} +{% set b_list = a_list %}\ +{% if deferred %} +{% set b_list = [deferred] %} +{% endif %}\ +{% do b_list.append(deferred ? 'B' : '') %} +B: {{ b_list }}\ +.{% endfor %}\ +{% do a_list.append(deferred ? 'A' : '') %} +A: {{ a_list }}\ +. diff --git a/src/test/resources/eager/allows-deferred-lazy-reference-to-get-overridden/test.jinja b/src/test/resources/eager/allows-deferred-lazy-reference-to-get-overridden/test.jinja new file mode 100644 index 000000000..7d810e58a --- /dev/null +++ b/src/test/resources/eager/allows-deferred-lazy-reference-to-get-overridden/test.jinja @@ -0,0 +1,13 @@ +{% set a_list = [] %} +{%- do a_list.append('a') %} +{%- for i in range(1) %} +{%- set b_list = a_list %} +{%- do b_list.append('b') %} +{% if deferred %} +{% set b_list = [deferred] %} +{% endif %} +{%- do b_list.append(deferred ? 'B' : '') %} +B: {{ b_list }}. +{%- endfor %} +{%- do a_list.append(deferred ? 'A' : '') %} +A: {{ a_list }}. 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 deleted file mode 100644 index 4fa876537..000000000 --- a/src/test/resources/eager/allows-meta-context-var-overriding.expected.jinja +++ /dev/null @@ -1,9 +0,0 @@ -0 -META -{% for meta in deferred %} -{{ meta }}{% endfor %} -{{ meta }} -{% set meta = [] %}{% if deferred %} -{% set meta = [1] %} -{% endif %} -{{ meta }} \ No newline at end of file diff --git a/src/test/resources/eager/allows-meta-context-var-overriding/test.expected.jinja b/src/test/resources/eager/allows-meta-context-var-overriding/test.expected.jinja new file mode 100644 index 000000000..061a088d4 --- /dev/null +++ b/src/test/resources/eager/allows-meta-context-var-overriding/test.expected.jinja @@ -0,0 +1,11 @@ +0 +META +{% for meta in deferred %} +{{ meta }}\ +{% endfor %} +META +{% set meta = [] %}\ +{% if deferred %} +{% do meta.append(1) %} +{% endif %} +{{ meta }} diff --git a/src/test/resources/eager/allows-meta-context-var-overriding.jinja b/src/test/resources/eager/allows-meta-context-var-overriding/test.jinja similarity index 100% rename from src/test/resources/eager/allows-meta-context-var-overriding.jinja rename to src/test/resources/eager/allows-meta-context-var-overriding/test.jinja diff --git a/src/test/resources/eager/allows-modification-in-resolved-for-loop/test.expected.jinja b/src/test/resources/eager/allows-modification-in-resolved-for-loop/test.expected.jinja new file mode 100644 index 000000000..82af21f61 --- /dev/null +++ b/src/test/resources/eager/allows-modification-in-resolved-for-loop/test.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/test.jinja b/src/test/resources/eager/allows-modification-in-resolved-for-loop/test.jinja new file mode 100644 index 000000000..82ef7ee91 --- /dev/null +++ b/src/test/resources/eager/allows-modification-in-resolved-for-loop/test.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 }} diff --git a/src/test/resources/eager/allows-variable-sharing-alias-name/filters.jinja b/src/test/resources/eager/allows-variable-sharing-alias-name/filters.jinja new file mode 100644 index 000000000..85d767628 --- /dev/null +++ b/src/test/resources/eager/allows-variable-sharing-alias-name/filters.jinja @@ -0,0 +1,4 @@ +{% set foo = 123 %} +{% set bar = deferred %} +{% set filters = {} %} +{% do filters.update(deferred) %} diff --git a/src/test/resources/eager/allows-variable-sharing-alias-name/test.expected.jinja b/src/test/resources/eager/allows-variable-sharing-alias-name/test.expected.jinja new file mode 100644 index 000000000..fd7a323fa --- /dev/null +++ b/src/test/resources/eager/allows-variable-sharing-alias-name/test.expected.jinja @@ -0,0 +1,17 @@ +{% do %}\ +{% set __temp_meta_current_path_200847175__,current_path = current_path,'eager/allows-variable-sharing-alias-name/filters.jinja' %}\ +{% set __temp_meta_import_alias_854547461__ = {} %}\ +{% for __ignored__ in [0] %} +{% set bar = deferred %}\ +{% do __temp_meta_import_alias_854547461__.update({'bar': bar}) %} + +{% set filters = {} %}\ +{% do __temp_meta_import_alias_854547461__.update({'filters': filters}) %}\ +{% do filters.update(deferred) %} +{% do __temp_meta_import_alias_854547461__.update({'bar': bar,'foo': 123,'import_resource_path': 'eager/allows-variable-sharing-alias-name/filters.jinja','filters': filters}) %}\ +{% endfor %}\ +{% set filters = __temp_meta_import_alias_854547461__ %}\ +{% set current_path,__temp_meta_current_path_200847175__ = __temp_meta_current_path_200847175__,null %}\ +{% enddo %} + +{{ filters }} diff --git a/src/test/resources/eager/allows-variable-sharing-alias-name/test.jinja b/src/test/resources/eager/allows-variable-sharing-alias-name/test.jinja new file mode 100644 index 000000000..87fd7dc1d --- /dev/null +++ b/src/test/resources/eager/allows-variable-sharing-alias-name/test.jinja @@ -0,0 +1,3 @@ +{% import './filters.jinja' as filters %} + +{{ filters }} diff --git a/src/test/resources/eager/commits-variables-from-do-tag-when-partially-resolved/test.expected.expected.jinja b/src/test/resources/eager/commits-variables-from-do-tag-when-partially-resolved/test.expected.expected.jinja new file mode 100644 index 000000000..10e06ff9a --- /dev/null +++ b/src/test/resources/eager/commits-variables-from-do-tag-when-partially-resolved/test.expected.expected.jinja @@ -0,0 +1,3 @@ +L0: ['a'] +L1: ['b'] +L2: ['c', 'resolved'] diff --git a/src/test/resources/eager/commits-variables-from-do-tag-when-partially-resolved/test.expected.jinja b/src/test/resources/eager/commits-variables-from-do-tag-when-partially-resolved/test.expected.jinja new file mode 100644 index 000000000..804116b2a --- /dev/null +++ b/src/test/resources/eager/commits-variables-from-do-tag-when-partially-resolved/test.expected.jinja @@ -0,0 +1,15 @@ +{% do %}\ +{% set list1 = ['b'] %}\ +{% set list0 = ['a'] %}\ +{% set list2 = ['c'] %} + + +{% set list2 = ['c'] %}\ +{% do list2.append(deferred) %} +{% unless deferred %} +{% set list0 = ['a', 'a'] %} +{% endunless %} +{% enddo %} +L0: {{ list0 }} +L1: ['b'] +L2: {{ list2 }} \ No newline at end of file diff --git a/src/test/resources/eager/commits-variables-from-do-tag-when-partially-resolved/test.jinja b/src/test/resources/eager/commits-variables-from-do-tag-when-partially-resolved/test.jinja new file mode 100644 index 000000000..f9566c49d --- /dev/null +++ b/src/test/resources/eager/commits-variables-from-do-tag-when-partially-resolved/test.jinja @@ -0,0 +1,11 @@ +{% set list0 = ['a'] %}{% do %} +{% set list1 = ['b'] %} +{% set list2 = ['c'] %} +{% do list2.append(deferred) %} +{% unless deferred %} +{% set list0 = ['a', 'a'] %} +{% endunless %} +{% enddo %} +L0: {{ list0 }} +L1: {{ list1 }} +L2: {{ list2 }} diff --git a/src/test/resources/eager/correctly-defers-with-multiple-loops.expected.jinja b/src/test/resources/eager/correctly-defers-with-multiple-loops.expected.jinja deleted file mode 100644 index 234c34f0c..000000000 --- a/src/test/resources/eager/correctly-defers-with-multiple-loops.expected.jinja +++ /dev/null @@ -1,6 +0,0 @@ -{% set my_list = [] %}{% for i in [0, 1] %} -{% for j in deferred %} -{% do my_list.append(1) %} -{% endfor %} -{% endfor %} -{{ my_list }} diff --git a/src/test/resources/eager/correctly-defers-with-multiple-loops/test.expected.jinja b/src/test/resources/eager/correctly-defers-with-multiple-loops/test.expected.jinja new file mode 100644 index 000000000..7d11ac0a1 --- /dev/null +++ b/src/test/resources/eager/correctly-defers-with-multiple-loops/test.expected.jinja @@ -0,0 +1,11 @@ +{% set my_list = [] %}\ +{% for __ignored__ in [0] %} +{% for j in deferred %} +{% do my_list.append(0) %} +{% endfor %} + +{% for j in deferred %} +{% do my_list.append(1) %} +{% endfor %} +{% endfor %} +{{ my_list }} diff --git a/src/test/resources/eager/correctly-defers-with-multiple-loops.jinja b/src/test/resources/eager/correctly-defers-with-multiple-loops/test.jinja similarity index 80% rename from src/test/resources/eager/correctly-defers-with-multiple-loops.jinja rename to src/test/resources/eager/correctly-defers-with-multiple-loops/test.jinja index b4ef48b42..4fcea761c 100644 --- a/src/test/resources/eager/correctly-defers-with-multiple-loops.jinja +++ b/src/test/resources/eager/correctly-defers-with-multiple-loops/test.jinja @@ -1,7 +1,7 @@ {% set my_list = [] %} {% for i in range(2) %} {% for j in deferred %} -{% do my_list.append(1) %} +{% do my_list.append(i) %} {% endfor %} {% endfor %} {{ my_list }} \ No newline at end of file diff --git a/src/test/resources/eager/defers-call-tag-with-deferred-argument.expected.expected.jinja b/src/test/resources/eager/defers-call-tag-with-deferred-argument/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/defers-call-tag-with-deferred-argument.expected.expected.jinja rename to src/test/resources/eager/defers-call-tag-with-deferred-argument/test.expected.expected.jinja diff --git a/src/test/resources/eager/defers-call-tag-with-deferred-argument.expected.jinja b/src/test/resources/eager/defers-call-tag-with-deferred-argument/test.expected.jinja similarity index 60% rename from src/test/resources/eager/defers-call-tag-with-deferred-argument.expected.jinja rename to src/test/resources/eager/defers-call-tag-with-deferred-argument/test.expected.jinja index ddac4cae9..4a4a1459f 100644 --- a/src/test/resources/eager/defers-call-tag-with-deferred-argument.expected.jinja +++ b/src/test/resources/eager/defers-call-tag-with-deferred-argument/test.expected.jinja @@ -1,12 +1,14 @@ {% macro repeat(val) %} {{ val }} {{ caller() }} -{% endmacro %}{% call repeat(deferred) %} +{% endmacro %}\ +{% call repeat(deferred) %} macro 1 {% macro repeat(val) %} {{ val }} {{ caller() }} -{% endmacro %}{% call repeat(deferred + 1) %} +{% endmacro %}\ +{% call repeat(deferred + 1) %} macro2 {% endcall %} {% endcall %} diff --git a/src/test/resources/eager/defers-call-tag-with-deferred-argument.jinja b/src/test/resources/eager/defers-call-tag-with-deferred-argument/test.jinja similarity index 100% rename from src/test/resources/eager/defers-call-tag-with-deferred-argument.jinja rename to src/test/resources/eager/defers-call-tag-with-deferred-argument/test.jinja diff --git a/src/test/resources/eager/defers-caller.expected.jinja b/src/test/resources/eager/defers-caller.expected.jinja deleted file mode 100644 index d69bbc924..000000000 --- a/src/test/resources/eager/defers-caller.expected.jinja +++ /dev/null @@ -1,2 +0,0 @@ -Jack says: -How do I get a {{ deferred }}? diff --git a/src/test/resources/eager/defers-caller.expected.expected.jinja b/src/test/resources/eager/defers-caller/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/defers-caller.expected.expected.jinja rename to src/test/resources/eager/defers-caller/test.expected.expected.jinja diff --git a/src/test/resources/eager/defers-caller/test.expected.jinja b/src/test/resources/eager/defers-caller/test.expected.jinja new file mode 100644 index 000000000..79657d9af --- /dev/null +++ b/src/test/resources/eager/defers-caller/test.expected.jinja @@ -0,0 +1,6 @@ +{% for __ignored__ in [0] %}\ +Jack says: +{% for __ignored__ in [0] %}\ +How do I get a {{ deferred }}\ +?{% endfor %}\ +{% endfor %} diff --git a/src/test/resources/eager/defers-caller.jinja b/src/test/resources/eager/defers-caller/test.jinja similarity index 100% rename from src/test/resources/eager/defers-caller.jinja rename to src/test/resources/eager/defers-caller/test.jinja 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 deleted file mode 100644 index 8f42953dd..000000000 --- a/src/test/resources/eager/defers-changes-within-deferred-set-block.expected.jinja +++ /dev/null @@ -1,6 +0,0 @@ -1 -{% set bar,foo = [1],'1' %}{% if deferred %} -{% set foo %}2{% set bar = [1, 2] %}{% endset %} -{% endif %} -Bar: {{ bar }} -Foo: {{ foo }} diff --git a/src/test/resources/eager/defers-changes-within-deferred-set-block.expected.expected.jinja b/src/test/resources/eager/defers-changes-within-deferred-set-block/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/defers-changes-within-deferred-set-block.expected.expected.jinja rename to src/test/resources/eager/defers-changes-within-deferred-set-block/test.expected.expected.jinja diff --git a/src/test/resources/eager/defers-changes-within-deferred-set-block/test.expected.jinja b/src/test/resources/eager/defers-changes-within-deferred-set-block/test.expected.jinja new file mode 100644 index 000000000..1ba5b9f04 --- /dev/null +++ b/src/test/resources/eager/defers-changes-within-deferred-set-block/test.expected.jinja @@ -0,0 +1,10 @@ +1 +{% set bar = [1] %}\ +{% set foo = '1' %}\ +{% if deferred %} +{% set foo %}\ +2{% do bar.append(2) %}\ +{% endset %} +{% endif %} +Bar: {{ bar }} +Foo: {{ foo }} diff --git a/src/test/resources/eager/defers-changes-within-deferred-set-block.jinja b/src/test/resources/eager/defers-changes-within-deferred-set-block/test.jinja similarity index 100% rename from src/test/resources/eager/defers-changes-within-deferred-set-block.jinja rename to src/test/resources/eager/defers-changes-within-deferred-set-block/test.jinja diff --git a/src/test/resources/eager/defers-eager-child-scoped-vars.expected.jinja b/src/test/resources/eager/defers-eager-child-scoped-vars/test.expected.jinja similarity index 100% rename from src/test/resources/eager/defers-eager-child-scoped-vars.expected.jinja rename to src/test/resources/eager/defers-eager-child-scoped-vars/test.expected.jinja diff --git a/src/test/resources/eager/defers-eager-child-scoped-vars.jinja b/src/test/resources/eager/defers-eager-child-scoped-vars/test.jinja similarity index 100% rename from src/test/resources/eager/defers-eager-child-scoped-vars.jinja rename to src/test/resources/eager/defers-eager-child-scoped-vars/test.jinja diff --git a/src/test/resources/eager/defers-ifchanged.expected.jinja b/src/test/resources/eager/defers-ifchanged/test.expected.jinja similarity index 100% rename from src/test/resources/eager/defers-ifchanged.expected.jinja rename to src/test/resources/eager/defers-ifchanged/test.expected.jinja diff --git a/src/test/resources/eager/defers-ifchanged.jinja b/src/test/resources/eager/defers-ifchanged/test.jinja similarity index 100% rename from src/test/resources/eager/defers-ifchanged.jinja rename to src/test/resources/eager/defers-ifchanged/test.jinja diff --git a/src/test/resources/eager/defers-large-loop.expected.jinja b/src/test/resources/eager/defers-large-loop/test.expected.jinja similarity index 100% rename from src/test/resources/eager/defers-large-loop.expected.jinja rename to src/test/resources/eager/defers-large-loop/test.expected.jinja diff --git a/src/test/resources/eager/defers-large-loop.jinja b/src/test/resources/eager/defers-large-loop/test.jinja similarity index 100% rename from src/test/resources/eager/defers-large-loop.jinja rename to src/test/resources/eager/defers-large-loop/test.jinja diff --git a/src/test/resources/eager/defers-loop-setting-meta-context-var/test.expected.expected.jinja b/src/test/resources/eager/defers-loop-setting-meta-context-var/test.expected.expected.jinja new file mode 100644 index 000000000..a1a53b533 --- /dev/null +++ b/src/test/resources/eager/defers-loop-setting-meta-context-var/test.expected.expected.jinja @@ -0,0 +1,3 @@ +a + +b diff --git a/src/test/resources/eager/defers-loop-setting-meta-context-var/test.expected.jinja b/src/test/resources/eager/defers-loop-setting-meta-context-var/test.expected.jinja new file mode 100644 index 000000000..33c36f300 --- /dev/null +++ b/src/test/resources/eager/defers-loop-setting-meta-context-var/test.expected.jinja @@ -0,0 +1,17 @@ +{% for __ignored__ in [0] %} +{% macro render(content, query) %}\ +{% if query %}\ +{{ content.foo }}\ +{% endif %}\ +{% endmacro %}\ +{% set content = {'foo': 'a'} %}\ +{{ render(content, deferred) }} + +{% macro render(content, query) %}\ +{% if query %}\ +{{ content.foo }}\ +{% endif %}\ +{% endmacro %}\ +{% set content = {'foo': 'b'} %}\ +{{ render(content, deferred) }} +{% endfor %} diff --git a/src/test/resources/eager/defers-loop-setting-meta-context-var/test.jinja b/src/test/resources/eager/defers-loop-setting-meta-context-var/test.jinja new file mode 100644 index 000000000..539816cc5 --- /dev/null +++ b/src/test/resources/eager/defers-loop-setting-meta-context-var/test.jinja @@ -0,0 +1,11 @@ +{% macro render(content, query) -%} +{%- if query -%} +{{ content.foo }} +{%- endif -%} +{% endmacro %} + +{% set looper = [{'foo': 'a'}, {'foo': 'b'}] %} +{% for content in looper %} +{{ render(content, deferred) }} +{% endfor %} + diff --git a/src/test/resources/eager/defers-macro-for-do-and-print.expected.jinja b/src/test/resources/eager/defers-macro-for-do-and-print.expected.jinja deleted file mode 100644 index 5fda61c3b..000000000 --- a/src/test/resources/eager/defers-macro-for-do-and-print.expected.jinja +++ /dev/null @@ -1,5 +0,0 @@ -Is ([]), -Macro: [10] -Is ([10]),{% set my_list = [10] %}{% macro macro_append(num) %}{% do my_list.append(num) %}Macro: {{ my_list }}{% endmacro %}{% do macro_append(deferred) %} -Is ({{ my_list }}), -{% macro macro_append(num) %}{% do my_list.append(num) %}Macro: {{ my_list }}{% endmacro %}{% print macro_append(deferred2) %} diff --git a/src/test/resources/eager/defers-macro-for-do-and-print/test.expected.jinja b/src/test/resources/eager/defers-macro-for-do-and-print/test.expected.jinja new file mode 100644 index 000000000..7bc44be6f --- /dev/null +++ b/src/test/resources/eager/defers-macro-for-do-and-print/test.expected.jinja @@ -0,0 +1,15 @@ +Is ([]), +Macro: [10] +Is ([10]),{% set my_list = [10] %}\ +{% macro macro_append(num) %}\ +{% do my_list.append(num) %}\ +Macro: {{ my_list }}\ +{% endmacro %}\ +{% do macro_append(deferred) %} +Is ({{ my_list }}\ +), +{% macro macro_append(num) %}\ +{% do my_list.append(num) %}\ +Macro: {{ my_list }}\ +{% endmacro %}\ +{% print macro_append(deferred2) %} diff --git a/src/test/resources/eager/defers-macro-for-do-and-print.jinja b/src/test/resources/eager/defers-macro-for-do-and-print/test.jinja similarity index 100% rename from src/test/resources/eager/defers-macro-for-do-and-print.jinja rename to src/test/resources/eager/defers-macro-for-do-and-print/test.jinja diff --git a/src/test/resources/eager/defers-macro-in-expression.expected.jinja b/src/test/resources/eager/defers-macro-in-expression.expected.jinja deleted file mode 100644 index 96f2afbbc..000000000 --- a/src/test/resources/eager/defers-macro-in-expression.expected.jinja +++ /dev/null @@ -1,3 +0,0 @@ -2 -{% macro plus(foo, add) %}{{ foo + (filter:int.filter(add, ____int3rpr3t3r____)) }}{% endmacro %}{{ plus(deferred, 1.1) }}{% set deferred = deferred + 2 %} -{% macro plus(foo, add) %}{{ foo + (filter:int.filter(add, ____int3rpr3t3r____)) }}{% endmacro %}{{ plus(deferred, 3.1) }} \ No newline at end of file diff --git a/src/test/resources/eager/defers-macro-in-expression.expected.expected.jinja b/src/test/resources/eager/defers-macro-in-expression/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/defers-macro-in-expression.expected.expected.jinja rename to src/test/resources/eager/defers-macro-in-expression/test.expected.expected.jinja diff --git a/src/test/resources/eager/defers-macro-in-expression/test.expected.jinja b/src/test/resources/eager/defers-macro-in-expression/test.expected.jinja new file mode 100644 index 000000000..7443a94eb --- /dev/null +++ b/src/test/resources/eager/defers-macro-in-expression/test.expected.jinja @@ -0,0 +1,10 @@ +2 +{% macro plus(foo, add) %}\ +{{ foo + (filter:int.filter(add, ____int3rpr3t3r____)) }}\ +{% endmacro %}\ +{{ plus(deferred, 1.1) }}\ +{% set deferred = deferred + 2 %} +{% macro plus(foo, add) %}\ +{{ foo + (filter:int.filter(add, ____int3rpr3t3r____)) }}\ +{% endmacro %}\ +{{ plus(deferred, 3.1) }} \ No newline at end of file diff --git a/src/test/resources/eager/defers-macro-in-expression.jinja b/src/test/resources/eager/defers-macro-in-expression/test.jinja similarity index 100% rename from src/test/resources/eager/defers-macro-in-expression.jinja rename to src/test/resources/eager/defers-macro-in-expression/test.jinja diff --git a/src/test/resources/eager/defers-macro-in-for.expected.jinja b/src/test/resources/eager/defers-macro-in-for.expected.jinja deleted file mode 100644 index 16d63d7bc..000000000 --- a/src/test/resources/eager/defers-macro-in-for.expected.jinja +++ /dev/null @@ -1,3 +0,0 @@ -{% 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/defers-macro-in-for/test.expected.jinja b/src/test/resources/eager/defers-macro-in-for/test.expected.jinja new file mode 100644 index 000000000..3311b8714 --- /dev/null +++ b/src/test/resources/eager/defers-macro-in-for/test.expected.jinja @@ -0,0 +1,8 @@ +{% 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/defers-macro-in-for.jinja b/src/test/resources/eager/defers-macro-in-for/test.jinja similarity index 100% rename from src/test/resources/eager/defers-macro-in-for.jinja rename to src/test/resources/eager/defers-macro-in-for/test.jinja diff --git a/src/test/resources/eager/defers-macro-in-if.expected.jinja b/src/test/resources/eager/defers-macro-in-if.expected.jinja deleted file mode 100644 index 5e8ad31cf..000000000 --- a/src/test/resources/eager/defers-macro-in-if.expected.jinja +++ /dev/null @@ -1,3 +0,0 @@ -{% set my_list = [] %}{% set my_list = [] %}{% macro macro_append(num) %}{% do my_list.append(num) %}{{ my_list }}{% endmacro %}{% if [] == filter:split.filter(macro_append(deferred), ____int3rpr3t3r____, ',', 2) %} -{{ my_list }} -{% endif %} diff --git a/src/test/resources/eager/defers-macro-in-if/test.expected.jinja b/src/test/resources/eager/defers-macro-in-if/test.expected.jinja new file mode 100644 index 000000000..67f28d9e4 --- /dev/null +++ b/src/test/resources/eager/defers-macro-in-if/test.expected.jinja @@ -0,0 +1,8 @@ +{% set my_list = [] %}\ +{% macro macro_append(num) %}\ +{% do my_list.append(num) %}\ +{{ my_list }}\ +{% endmacro %}\ +{% if [] == filter:split.filter(macro_append(deferred), ____int3rpr3t3r____, ',', 2) %} +{{ my_list }} +{% endif %} diff --git a/src/test/resources/eager/defers-macro-in-if.jinja b/src/test/resources/eager/defers-macro-in-if/test.jinja similarity index 100% rename from src/test/resources/eager/defers-macro-in-if.jinja rename to src/test/resources/eager/defers-macro-in-if/test.jinja diff --git a/src/test/resources/eager/defers-on-immutable-mode.expected.jinja b/src/test/resources/eager/defers-on-immutable-mode.expected.jinja deleted file mode 100644 index 1e6732434..000000000 --- a/src/test/resources/eager/defers-on-immutable-mode.expected.jinja +++ /dev/null @@ -1,9 +0,0 @@ -{% set foo = 1 %}{% if deferred %} -{% set foo = 2 %} -{% else %} -{% set foo = 3 %} -{% endif %} -{{ foo }} - -{% set bar = 1 %}{% for item in [0, 1] %}{% set bar = bar + deferred %} -{% endfor %}{{ bar }} diff --git a/src/test/resources/eager/defers-on-immutable-mode/test.expected.jinja b/src/test/resources/eager/defers-on-immutable-mode/test.expected.jinja new file mode 100644 index 000000000..21cb364d5 --- /dev/null +++ b/src/test/resources/eager/defers-on-immutable-mode/test.expected.jinja @@ -0,0 +1,13 @@ +{% set foo = 1 %}\ +{% if deferred %} +{% set foo = 2 %} +{% else %} +{% set foo = 3 %} +{% endif %} +{{ foo }} + +{% for __ignored__ in [0] %}\ +{% set bar = 1 + deferred %} +{% set bar = bar + deferred %} +{% endfor %}\ +1 diff --git a/src/test/resources/eager/defers-on-immutable-mode.jinja b/src/test/resources/eager/defers-on-immutable-mode/test.jinja similarity index 100% rename from src/test/resources/eager/defers-on-immutable-mode.jinja rename to src/test/resources/eager/defers-on-immutable-mode/test.jinja diff --git a/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/base.jinja b/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/base.jinja new file mode 100644 index 000000000..844a1e631 --- /dev/null +++ b/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/base.jinja @@ -0,0 +1,12 @@ +{% set tracker_base = '1_base' %} + + +-----Pre-First----- +{% block first -%} +{%- endblock %} +-----Post-First----- +-----Pre-Second----- +{% block second -%} +{%- endblock %} +-----Post-Second----- +We aren't deferring tracker base.{# This message WILL show up in final output #} diff --git a/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/middle.jinja b/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/middle.jinja new file mode 100644 index 000000000..b0a0c6aee --- /dev/null +++ b/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/middle.jinja @@ -0,0 +1,13 @@ +{% extends '../../eager/does-not-defer-block-when-only-middle-defers/base.jinja' %} +{% set tracker_middle = '2_middle' %} +{% block first %} +I WON'T SHOW UP +{% endblock %} + +{% block second %} +tracker_base is '1_base': {{ tracker_base }}? {{ tracker_base == '1_base' }} +tracker_middle is 'resolved': {{ tracker_middle }}? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}? {{ tracker_test == 'resolved' }} +{% endblock %} +Deferring tracker middle.{# This message will not show up in final output #} +{% set tracker_middle = deferred %} diff --git a/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/test.expected.expected.jinja b/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/test.expected.expected.jinja new file mode 100644 index 000000000..02aa14818 --- /dev/null +++ b/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/test.expected.expected.jinja @@ -0,0 +1,15 @@ +-----Pre-First----- + +tracker_base is '1_base': 1_base? true +tracker_middle is 'resolved': resolved? true +tracker_test is 'resolved': resolved? true + +-----Post-First----- +-----Pre-Second----- + +tracker_base is '1_base': 1_base? true +tracker_middle is 'resolved': resolved? true +tracker_test is 'resolved': resolved? true + +-----Post-Second----- +We aren't deferring tracker base. \ No newline at end of file diff --git a/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/test.expected.jinja b/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/test.expected.jinja new file mode 100644 index 000000000..b91210414 --- /dev/null +++ b/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/test.expected.jinja @@ -0,0 +1,36 @@ +{# Start Label: ignored_output_from_extends #}{% do %} + + + +Deferring tracker test. +{% set tracker_test = deferred %} + + + + + +Deferring tracker middle. +{% set tracker_middle = deferred %} +{% enddo %}\ +{# End Label: ignored_output_from_extends #}{% set current_path = 'eager/does-not-defer-block-when-only-middle-defers/base.jinja' %} + + +-----Pre-First----- +{% set __temp_meta_current_path_1008935144__,current_path = current_path,'eager/does-not-defer-block-when-only-middle-defers/test.jinja' %} +tracker_base is '1_base': 1_base? true +tracker_middle is 'resolved': {{ tracker_middle }}\ +? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}\ +? {{ tracker_test == 'resolved' }} +{% set current_path,__temp_meta_current_path_1008935144__ = __temp_meta_current_path_1008935144__,null %} +-----Post-First----- +-----Pre-Second----- +{% set __temp_meta_current_path_245328778__,current_path = current_path,'eager/does-not-defer-block-when-only-middle-defers/middle.jinja' %} +tracker_base is '1_base': 1_base? true +tracker_middle is 'resolved': {{ tracker_middle }}\ +? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}\ +? {{ tracker_test == 'resolved' }} +{% set current_path,__temp_meta_current_path_245328778__ = __temp_meta_current_path_245328778__,null %} +-----Post-Second----- +We aren't deferring tracker base. \ No newline at end of file diff --git a/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/test.jinja b/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/test.jinja new file mode 100644 index 000000000..2cd4582cc --- /dev/null +++ b/src/test/resources/eager/does-not-defer-block-when-only-middle-defers/test.jinja @@ -0,0 +1,10 @@ +{% extends '../../eager/does-not-defer-block-when-only-middle-defers/middle.jinja' %} + +{% set tracker_test = '3_test' %} +{% block first %} +tracker_base is '1_base': {{ tracker_base }}? {{ tracker_base == '1_base' }} +tracker_middle is 'resolved': {{ tracker_middle }}? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}? {{ tracker_test == 'resolved' }} +{% endblock %} +Deferring tracker test.{# This message will not show up in final output #} +{% set tracker_test = deferred %} 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 deleted file mode 100644 index 0e21ec7bc..000000000 --- a/src/test/resources/eager/does-not-override-import-modification-in-for.expected.jinja +++ /dev/null @@ -1,40 +0,0 @@ -{% for __ignored__ in [0] %} -{% set __ignored__ %}{% 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 %} -{{ bar1.foo }} -{% set __ignored__ %}{% 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 %} -{{ bar2.foo }} - -{% set __ignored__ %}{% 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 %} -{{ bar1.foo }} -{% set __ignored__ %}{% 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 %} -{{ bar2.foo }} -{% endfor %} -start diff --git a/src/test/resources/eager/does-not-override-import-modification-in-for.jinja b/src/test/resources/eager/does-not-override-import-modification-in-for.jinja deleted file mode 100644 index 7f488cbb8..000000000 --- a/src/test/resources/eager/does-not-override-import-modification-in-for.jinja +++ /dev/null @@ -1,8 +0,0 @@ -{% set foo = 'start' %} -{% for i in range(2) %} -{% import "deferred-modification.jinja" as bar1 %} -{{ bar1.foo }} -{% import "deferred-modification.jinja" as bar2 %} -{{ bar2.foo }} -{% endfor %} -{{ foo }} diff --git a/src/test/resources/eager/does-not-override-import-modification-in-for.expected.expected.jinja b/src/test/resources/eager/does-not-override-import-modification-in-for/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/does-not-override-import-modification-in-for.expected.expected.jinja rename to src/test/resources/eager/does-not-override-import-modification-in-for/test.expected.expected.jinja diff --git a/src/test/resources/eager/does-not-override-import-modification-in-for/test.expected.jinja b/src/test/resources/eager/does-not-override-import-modification-in-for/test.expected.jinja new file mode 100644 index 000000000..0e1e4a8a2 --- /dev/null +++ b/src/test/resources/eager/does-not-override-import-modification-in-for/test.expected.jinja @@ -0,0 +1,81 @@ +{% set foo = 'start' %}\ +{% for __ignored__ in [0] %} +{% do %}\ +{% set __temp_meta_current_path_461135149__,current_path = current_path,'eager/supplements/deferred-modification.jinja' %}\ +{% set __temp_meta_import_alias_3016318__ = {} %}\ +{% for __ignored__ in [0] %}\ +{% if deferred %} + +{% set foo = 'starta' %}\ +{% do __temp_meta_import_alias_3016318__.update({'foo': foo}) %} + +{% endif %} + +{% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}\ +{% do __temp_meta_import_alias_3016318__.update({'foo': foo}) %} +{% do __temp_meta_import_alias_3016318__.update({'foo': foo,'import_resource_path': 'eager/supplements/deferred-modification.jinja'}) %}\ +{% endfor %}\ +{% set bar1 = __temp_meta_import_alias_3016318__ %}\ +{% set current_path,__temp_meta_current_path_461135149__ = __temp_meta_current_path_461135149__,null %}\ +{% enddo %} +{{ bar1.foo }} +{% do %}\ +{% set __temp_meta_current_path_461135149__,current_path = current_path,'eager/supplements/deferred-modification.jinja' %}\ +{% set __temp_meta_import_alias_3016319__ = {} %}\ +{% for __ignored__ in [0] %}\ +{% if deferred %} + +{% set foo = filter:join.filter([foo, 'a'], ____int3rpr3t3r____, '') %}\ +{% do __temp_meta_import_alias_3016319__.update({'foo': foo}) %} + +{% endif %} + +{% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}\ +{% do __temp_meta_import_alias_3016319__.update({'foo': foo}) %} +{% do __temp_meta_import_alias_3016319__.update({'import_resource_path': 'eager/supplements/deferred-modification.jinja'}) %}\ +{% endfor %}\ +{% set bar2 = __temp_meta_import_alias_3016319__ %}\ +{% set current_path,__temp_meta_current_path_461135149__ = __temp_meta_current_path_461135149__,null %}\ +{% enddo %} +{{ bar2.foo }} + +{% do %}\ +{% set __temp_meta_current_path_461135149__,current_path = current_path,'eager/supplements/deferred-modification.jinja' %}\ +{% set __temp_meta_import_alias_3016318__ = {} %}\ +{% for __ignored__ in [0] %}\ +{% if deferred %} + +{% set foo = filter:join.filter([foo, 'a'], ____int3rpr3t3r____, '') %}\ +{% do __temp_meta_import_alias_3016318__.update({'foo': foo}) %} + +{% endif %} + +{% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}\ +{% do __temp_meta_import_alias_3016318__.update({'foo': foo}) %} +{% do __temp_meta_import_alias_3016318__.update({'import_resource_path': 'eager/supplements/deferred-modification.jinja'}) %}\ +{% endfor %}\ +{% set bar1 = __temp_meta_import_alias_3016318__ %}\ +{% set current_path,__temp_meta_current_path_461135149__ = __temp_meta_current_path_461135149__,null %}\ +{% enddo %} +{{ bar1.foo }} +{% do %}\ +{% set __temp_meta_current_path_461135149__,current_path = current_path,'eager/supplements/deferred-modification.jinja' %}\ +{% set __temp_meta_import_alias_3016319__ = {} %}\ +{% for __ignored__ in [0] %}\ +{% if deferred %} + +{% set foo = filter:join.filter([foo, 'a'], ____int3rpr3t3r____, '') %}\ +{% do __temp_meta_import_alias_3016319__.update({'foo': foo}) %} + +{% endif %} + +{% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}\ +{% do __temp_meta_import_alias_3016319__.update({'foo': foo}) %} +{% do __temp_meta_import_alias_3016319__.update({'import_resource_path': 'eager/supplements/deferred-modification.jinja'}) %}\ +{% endfor %}\ +{% set bar2 = __temp_meta_import_alias_3016319__ %}\ +{% set current_path,__temp_meta_current_path_461135149__ = __temp_meta_current_path_461135149__,null %}\ +{% enddo %} +{{ bar2.foo }} +{% endfor %} +{{ foo }} \ No newline at end of file diff --git a/src/test/resources/eager/does-not-override-import-modification-in-for/test.jinja b/src/test/resources/eager/does-not-override-import-modification-in-for/test.jinja new file mode 100644 index 000000000..e5d54cb53 --- /dev/null +++ b/src/test/resources/eager/does-not-override-import-modification-in-for/test.jinja @@ -0,0 +1,8 @@ +{% set foo = 'start' %} +{% for i in range(2) %} +{% import "../supplements/deferred-modification.jinja" as bar1 %} +{{ bar1.foo }} +{% import "../supplements/deferred-modification.jinja" as bar2 %} +{{ bar2.foo }} +{% endfor %} +{{ foo }} diff --git a/src/test/resources/eager/does-not-reconstruct-extra-times/test.expected.jinja b/src/test/resources/eager/does-not-reconstruct-extra-times/test.expected.jinja new file mode 100644 index 000000000..eddbba409 --- /dev/null +++ b/src/test/resources/eager/does-not-reconstruct-extra-times/test.expected.jinja @@ -0,0 +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 }} diff --git a/src/test/resources/eager/does-not-reconstruct-extra-times/test.jinja b/src/test/resources/eager/does-not-reconstruct-extra-times/test.jinja new file mode 100644 index 000000000..29e3a601a --- /dev/null +++ b/src/test/resources/eager/does-not-reconstruct-extra-times/test.jinja @@ -0,0 +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 }} + diff --git a/src/test/resources/eager/does-not-reconstruct-variable-in-set-in-wrong-scope/test.expected.jinja b/src/test/resources/eager/does-not-reconstruct-variable-in-set-in-wrong-scope/test.expected.jinja new file mode 100644 index 000000000..44ebdcc44 --- /dev/null +++ b/src/test/resources/eager/does-not-reconstruct-variable-in-set-in-wrong-scope/test.expected.jinja @@ -0,0 +1,6 @@ +{% set my_list = [] %}\ +{% set foo %} +{% do my_list.append(deferred) %}\ +a +{% endset %} +{{ my_list }} \ No newline at end of file diff --git a/src/test/resources/eager/does-not-reconstruct-variable-in-set-in-wrong-scope/test.jinja b/src/test/resources/eager/does-not-reconstruct-variable-in-set-in-wrong-scope/test.jinja new file mode 100644 index 000000000..b1465b18a --- /dev/null +++ b/src/test/resources/eager/does-not-reconstruct-variable-in-set-in-wrong-scope/test.jinja @@ -0,0 +1,5 @@ +{% set my_list = [] %} +{% set foo %} +{% do my_list.append(deferred) %}a +{% endset %} +{{ my_list }} \ No newline at end of file diff --git a/src/test/resources/eager/does-not-reconstruct-variable-in-wrong-scope/test.expected.expected.jinja b/src/test/resources/eager/does-not-reconstruct-variable-in-wrong-scope/test.expected.expected.jinja new file mode 100644 index 000000000..f7050f228 --- /dev/null +++ b/src/test/resources/eager/does-not-reconstruct-variable-in-wrong-scope/test.expected.expected.jinja @@ -0,0 +1 @@ +['a', 'b', 'c', 'd'] diff --git a/src/test/resources/eager/does-not-reconstruct-variable-in-wrong-scope/test.expected.jinja b/src/test/resources/eager/does-not-reconstruct-variable-in-wrong-scope/test.expected.jinja new file mode 100644 index 000000000..a403d3d68 --- /dev/null +++ b/src/test/resources/eager/does-not-reconstruct-variable-in-wrong-scope/test.expected.jinja @@ -0,0 +1,19 @@ +{% set my_list = ['a'] %}\ +{% if deferred %} +{% set __macro_append_stuff_153654787_temp_variable_0__ %} +{% set __macro_foo_97643642_temp_variable_1__ %} +{% do my_list.append('b') %} +{% endset %}\ +{{ __macro_foo_97643642_temp_variable_1__ }} +{% set __macro_foo_97643642_temp_variable_2__ %} +{% do my_list.append('c') %} +{% endset %}\ +{{ __macro_foo_97643642_temp_variable_2__ }} +{% endset %}\ +{{ __macro_append_stuff_153654787_temp_variable_0__ }} +{% endif %} +{% for __ignored__ in [0] %} +{% do my_list.append('d') %} +{% endfor %} + +{{ my_list }} diff --git a/src/test/resources/eager/does-not-reconstruct-variable-in-wrong-scope/test.jinja b/src/test/resources/eager/does-not-reconstruct-variable-in-wrong-scope/test.jinja new file mode 100644 index 000000000..c83d5883d --- /dev/null +++ b/src/test/resources/eager/does-not-reconstruct-variable-in-wrong-scope/test.jinja @@ -0,0 +1,17 @@ +{% macro foo(var) %} +{% do my_list.append(var) %} +{% endmacro %} + +{% macro append_stuff() %} +{{ foo('b') }} +{{ foo('c') }} +{% endmacro %} + +{% set my_list = [] %} +{{ foo('a') }} +{% if deferred %} +{{ append_stuff() }} +{% endif %} +{{ foo('d') }} + +{{ my_list }} diff --git a/src/test/resources/eager/does-not-referential-defer-for-set-vars.expected.jinja b/src/test/resources/eager/does-not-referential-defer-for-set-vars/test.expected.jinja similarity index 100% rename from src/test/resources/eager/does-not-referential-defer-for-set-vars.expected.jinja rename to src/test/resources/eager/does-not-referential-defer-for-set-vars/test.expected.jinja diff --git a/src/test/resources/eager/does-not-referential-defer-for-set-vars.jinja b/src/test/resources/eager/does-not-referential-defer-for-set-vars/test.jinja similarity index 100% rename from src/test/resources/eager/does-not-referential-defer-for-set-vars.jinja rename to src/test/resources/eager/does-not-referential-defer-for-set-vars/test.jinja diff --git a/src/test/resources/eager/does-not-stack-overflow-trying-to-build-hashcode/test.expected.jinja b/src/test/resources/eager/does-not-stack-overflow-trying-to-build-hashcode/test.expected.jinja new file mode 100644 index 000000000..889c365e4 --- /dev/null +++ b/src/test/resources/eager/does-not-stack-overflow-trying-to-build-hashcode/test.expected.jinja @@ -0,0 +1,3 @@ +{% for i in deferred %} +hey +{% endfor %} \ No newline at end of file diff --git a/src/test/resources/eager/does-not-stack-overflow-trying-to-build-hashcode/test.jinja b/src/test/resources/eager/does-not-stack-overflow-trying-to-build-hashcode/test.jinja new file mode 100644 index 000000000..1e55549b4 --- /dev/null +++ b/src/test/resources/eager/does-not-stack-overflow-trying-to-build-hashcode/test.jinja @@ -0,0 +1,12 @@ +{% set l1000_1 = [] %} +{% set l1000_2 = [] %} +{% set l1000_3 = [] %} + +{% do l1000_1.append(l1000_2) %} +{% do l1000_2.append(l1000_3) %} +{% do l1000_3.append(l1000_1) %} + + +{% for i in deferred %} +{{ 'hey' }} +{% endfor %} \ No newline at end of file diff --git a/src/test/resources/eager/doesnt-affect-parent-from-eager-if.expected.jinja b/src/test/resources/eager/doesnt-affect-parent-from-eager-if/test.expected.jinja similarity index 65% rename from src/test/resources/eager/doesnt-affect-parent-from-eager-if.expected.jinja rename to src/test/resources/eager/doesnt-affect-parent-from-eager-if/test.expected.jinja index 8d3828801..5a91d037a 100644 --- a/src/test/resources/eager/doesnt-affect-parent-from-eager-if.expected.jinja +++ b/src/test/resources/eager/doesnt-affect-parent-from-eager-if/test.expected.jinja @@ -1,4 +1,5 @@ -{% set foo = 1 %}{% if deferred %} +{% set foo = 1 %}\ +{% if deferred %} {% set foo = 2 %} {% else %} {% set foo = 3 %} diff --git a/src/test/resources/eager/doesnt-affect-parent-from-eager-if.jinja b/src/test/resources/eager/doesnt-affect-parent-from-eager-if/test.jinja similarity index 100% rename from src/test/resources/eager/doesnt-affect-parent-from-eager-if.jinja rename to src/test/resources/eager/doesnt-affect-parent-from-eager-if/test.jinja 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 deleted file mode 100644 index bc50799a0..000000000 --- a/src/test/resources/eager/doesnt-double-append-in-deferred-if-tag.expected.jinja +++ /dev/null @@ -1,8 +0,0 @@ -{% set foo = [0, 1] %}{% if deferred == true %} -[0, 1] -{% set foo = [0, 1, 2] %} -{% else %} -[0, 1] -{% set foo = [0, 1, 3] %} -{% endif %} -{{ foo }} diff --git a/src/test/resources/eager/doesnt-double-append-in-deferred-if-tag/test.expected.jinja b/src/test/resources/eager/doesnt-double-append-in-deferred-if-tag/test.expected.jinja new file mode 100644 index 000000000..818d51cd0 --- /dev/null +++ b/src/test/resources/eager/doesnt-double-append-in-deferred-if-tag/test.expected.jinja @@ -0,0 +1,9 @@ +{% set foo = [0, 1] %}\ +{% if deferred == true %} +[0, 1] +{% do foo.append(2) %} +{% else %} +[0, 1] +{% do foo.append(3) %} +{% endif %} +{{ foo }} diff --git a/src/test/resources/eager/doesnt-double-append-in-deferred-if-tag.jinja b/src/test/resources/eager/doesnt-double-append-in-deferred-if-tag/test.jinja similarity index 100% rename from src/test/resources/eager/doesnt-double-append-in-deferred-if-tag.jinja rename to src/test/resources/eager/doesnt-double-append-in-deferred-if-tag/test.jinja diff --git a/src/test/resources/eager/doesnt-double-append-in-deferred-macro/test.expected.jinja b/src/test/resources/eager/doesnt-double-append-in-deferred-macro/test.expected.jinja new file mode 100644 index 000000000..383a93c39 --- /dev/null +++ b/src/test/resources/eager/doesnt-double-append-in-deferred-macro/test.expected.jinja @@ -0,0 +1,10 @@ +{% set my_list = ['a'] %}\ +{% set __macro_foo_97643642_temp_variable_0__ %} +a +{% if deferred %} +{% do my_list.append('b') %}\ +b +{% endif %} +{% endset %}\ +{{ __macro_foo_97643642_temp_variable_0__ }} +{{ my_list }} diff --git a/src/test/resources/eager/doesnt-double-append-in-deferred-macro/test.jinja b/src/test/resources/eager/doesnt-double-append-in-deferred-macro/test.jinja new file mode 100644 index 000000000..73d7137e8 --- /dev/null +++ b/src/test/resources/eager/doesnt-double-append-in-deferred-macro/test.jinja @@ -0,0 +1,9 @@ +{% set my_list = [] %} +{% macro foo() %} +{% do my_list.append('a') %}a +{% if deferred %} +{% do my_list.append('b') %}b +{% endif %} +{% endmacro %} +{{ foo() }} +{{ my_list }} diff --git a/src/test/resources/eager/doesnt-double-append-in-deferred-set/test.expected.jinja b/src/test/resources/eager/doesnt-double-append-in-deferred-set/test.expected.jinja new file mode 100644 index 000000000..36932e628 --- /dev/null +++ b/src/test/resources/eager/doesnt-double-append-in-deferred-set/test.expected.jinja @@ -0,0 +1,9 @@ +{% set my_list = ['a'] %}\ +{% set foo %} +a +{% if deferred %} +{% do my_list.append('b') %}\ +b +{% endif %} +{% endset %} +{{ my_list }} diff --git a/src/test/resources/eager/doesnt-double-append-in-deferred-set/test.jinja b/src/test/resources/eager/doesnt-double-append-in-deferred-set/test.jinja new file mode 100644 index 000000000..ec70ff1ca --- /dev/null +++ b/src/test/resources/eager/doesnt-double-append-in-deferred-set/test.jinja @@ -0,0 +1,8 @@ +{% set my_list = [] %} +{% set foo %} +{% do my_list.append('a') %}a +{% if deferred %} +{% do my_list.append('b') %}b +{% endif %} +{% endset %} +{{ my_list }} diff --git a/src/test/resources/eager/doesnt-overwrite-elif.expected.jinja b/src/test/resources/eager/doesnt-overwrite-elif.expected.jinja deleted file mode 100644 index 0d301c30e..000000000 --- a/src/test/resources/eager/doesnt-overwrite-elif.expected.jinja +++ /dev/null @@ -1,2 +0,0 @@ -{% set foo = [0] %}{% if false %}{% elif deferred && foo.append(1) %}1{% elif deferred && foo.append(2) %}2{% endif %} -{{ foo }} diff --git a/src/test/resources/eager/doesnt-overwrite-elif/test.expected.jinja b/src/test/resources/eager/doesnt-overwrite-elif/test.expected.jinja new file mode 100644 index 000000000..bf6893d6a --- /dev/null +++ b/src/test/resources/eager/doesnt-overwrite-elif/test.expected.jinja @@ -0,0 +1,6 @@ +{% set foo = [0] %}\ +{% if false %}\ +{% elif deferred && foo.append(1) %}\ +1{% elif deferred && foo.append(2) %}\ +2{% endif %} +{{ foo }} diff --git a/src/test/resources/eager/doesnt-overwrite-elif.jinja b/src/test/resources/eager/doesnt-overwrite-elif/test.jinja similarity index 100% rename from src/test/resources/eager/doesnt-overwrite-elif.jinja rename to src/test/resources/eager/doesnt-overwrite-elif/test.jinja diff --git a/src/test/resources/eager/eagerly-defers-macro.expected.jinja b/src/test/resources/eager/eagerly-defers-macro.expected.jinja deleted file mode 100644 index cd45e9ec6..000000000 --- a/src/test/resources/eager/eagerly-defers-macro.expected.jinja +++ /dev/null @@ -1,6 +0,0 @@ -{% macro big_guy() %} -{% if deferred %}I am foo{% else %}I am bar{% endif %} -{% endmacro %}{% print big_guy() %} -{% macro big_guy() %} -{% if deferred %}No more foo{% else %}I am bar{% endif %} -{% endmacro %}{% print big_guy() %} \ No newline at end of file diff --git a/src/test/resources/eager/eagerly-defers-macro.expected.expected.jinja b/src/test/resources/eager/eagerly-defers-macro/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/eagerly-defers-macro.expected.expected.jinja rename to src/test/resources/eager/eagerly-defers-macro/test.expected.expected.jinja diff --git a/src/test/resources/eager/eagerly-defers-macro/test.expected.jinja b/src/test/resources/eager/eagerly-defers-macro/test.expected.jinja new file mode 100644 index 000000000..d8dc2cff7 --- /dev/null +++ b/src/test/resources/eager/eagerly-defers-macro/test.expected.jinja @@ -0,0 +1,12 @@ +{% set __macro_big_guy_1311704000_temp_variable_0__ %} +{% if deferred %}\ +I am foo{% else %}\ +I am bar{% endif %} +{% 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_1311704000_temp_variable_1__ %} diff --git a/src/test/resources/eager/eagerly-defers-macro.jinja b/src/test/resources/eager/eagerly-defers-macro/test.jinja similarity index 100% rename from src/test/resources/eager/eagerly-defers-macro.jinja rename to src/test/resources/eager/eagerly-defers-macro/test.jinja diff --git a/src/test/resources/eager/eagerly-defers-set.expected.jinja b/src/test/resources/eager/eagerly-defers-set/test.expected.jinja similarity index 100% rename from src/test/resources/eager/eagerly-defers-set.expected.jinja rename to src/test/resources/eager/eagerly-defers-set/test.expected.jinja diff --git a/src/test/resources/eager/eagerly-defers-set.jinja b/src/test/resources/eager/eagerly-defers-set/test.jinja similarity index 100% rename from src/test/resources/eager/eagerly-defers-set.jinja rename to src/test/resources/eager/eagerly-defers-set/test.jinja diff --git a/src/test/resources/eager/evaluates-non-eager-set.expected.jinja b/src/test/resources/eager/evaluates-non-eager-set/test.expected.jinja similarity index 100% rename from src/test/resources/eager/evaluates-non-eager-set.expected.jinja rename to src/test/resources/eager/evaluates-non-eager-set/test.expected.jinja diff --git a/src/test/resources/eager/evaluates-non-eager-set.jinja b/src/test/resources/eager/evaluates-non-eager-set/test.jinja similarity index 100% rename from src/test/resources/eager/evaluates-non-eager-set.jinja rename to src/test/resources/eager/evaluates-non-eager-set/test.jinja diff --git a/src/test/resources/eager/fails-on-modification-in-aliased-macro/settings.jinja b/src/test/resources/eager/fails-on-modification-in-aliased-macro/settings.jinja new file mode 100644 index 000000000..7c9feac3c --- /dev/null +++ b/src/test/resources/eager/fails-on-modification-in-aliased-macro/settings.jinja @@ -0,0 +1,5 @@ +{% set settings = {} %} + +{% macro load_settings() %} +{% do settings.put('foo', 'bar') %} +{% endmacro %} diff --git a/src/test/resources/eager/fails-on-modification-in-aliased-macro/test.jinja b/src/test/resources/eager/fails-on-modification-in-aliased-macro/test.jinja new file mode 100644 index 000000000..54f2dd54d --- /dev/null +++ b/src/test/resources/eager/fails-on-modification-in-aliased-macro/test.jinja @@ -0,0 +1,6 @@ +{% import './settings.jinja' as shared %} + +{% if deferred %} +{{ shared.load_settings() }} +{% endif %} +{{ shared.settings }} diff --git a/src/test/resources/eager/partially-resolves-eager-set.jinja b/src/test/resources/eager/finds-deferred-words-inside-reconstructed-string/test.expected.jinja similarity index 100% rename from src/test/resources/eager/partially-resolves-eager-set.jinja rename to src/test/resources/eager/finds-deferred-words-inside-reconstructed-string/test.expected.jinja diff --git a/src/test/resources/eager/finds-deferred-words-inside-reconstructed-string/test.jinja b/src/test/resources/eager/finds-deferred-words-inside-reconstructed-string/test.jinja new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/eager/fully-defers-filtered-macro.expected.jinja b/src/test/resources/eager/fully-defers-filtered-macro.expected.jinja deleted file mode 100644 index 492a80e38..000000000 --- a/src/test/resources/eager/fully-defers-filtered-macro.expected.jinja +++ /dev/null @@ -1,5 +0,0 @@ -{% macro flashy(foo) %}{{ filter:upper.filter(foo, ____int3rpr3t3r____) }} - A flashy {{ deferred }}.{% endmacro %}{{ flashy(flashy('bar')) }} ---- - -{% macro silly() %}A silly {{ deferred }}.{% endmacro %}{{ filter:upper.filter(silly(), ____int3rpr3t3r____) }} diff --git a/src/test/resources/eager/fully-defers-filtered-macro.expected.expected.jinja b/src/test/resources/eager/fully-defers-filtered-macro/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/fully-defers-filtered-macro.expected.expected.jinja rename to src/test/resources/eager/fully-defers-filtered-macro/test.expected.expected.jinja diff --git a/src/test/resources/eager/fully-defers-filtered-macro/test.expected.jinja b/src/test/resources/eager/fully-defers-filtered-macro/test.expected.jinja new file mode 100644 index 000000000..4938403bd --- /dev/null +++ b/src/test/resources/eager/fully-defers-filtered-macro/test.expected.jinja @@ -0,0 +1,15 @@ +{% macro flashy(foo) %}\ +{{ filter:upper.filter(foo, ____int3rpr3t3r____) }} + A flashy {{ deferred }}\ +.{% endmacro %}\ +{% set __macro_flashy_1625622909_temp_variable_0__ %}\ +BAR + A flashy {{ deferred }}\ +.{% endset %}\ +{{ flashy(__macro_flashy_1625622909_temp_variable_0__) }} +--- + +{% set __macro_silly_2092874071_temp_variable_0__ %}\ +A silly {{ deferred }}\ +.{% endset %}\ +{{ filter:upper.filter(__macro_silly_2092874071_temp_variable_0__, ____int3rpr3t3r____) }} \ No newline at end of file diff --git a/src/test/resources/eager/fully-defers-filtered-macro.jinja b/src/test/resources/eager/fully-defers-filtered-macro/test.jinja similarity index 100% rename from src/test/resources/eager/fully-defers-filtered-macro.jinja rename to src/test/resources/eager/fully-defers-filtered-macro/test.jinja diff --git a/src/test/resources/eager/handles-auto-escape.expected.jinja b/src/test/resources/eager/handles-auto-escape.expected.jinja deleted file mode 100644 index a74c0de08..000000000 --- a/src/test/resources/eager/handles-auto-escape.expected.jinja +++ /dev/null @@ -1,4 +0,0 @@ -1. foo < bar (Only expression nodes get escaped currently) -2. {% autoescape %}{% print deferred %}{% endautoescape %} -3. foo < bar -4. {% autoescape %}{{ deferred }}{% endautoescape %} diff --git a/src/test/resources/eager/handles-auto-escape/test.expected.jinja b/src/test/resources/eager/handles-auto-escape/test.expected.jinja new file mode 100644 index 000000000..7c2796908 --- /dev/null +++ b/src/test/resources/eager/handles-auto-escape/test.expected.jinja @@ -0,0 +1,8 @@ +1. foo < bar (Only expression nodes get escaped currently) +2. {% autoescape %}\ +{% print deferred %}\ +{% endautoescape %} +3. foo < bar +4. {% autoescape %}\ +{{ deferred }}\ +{% endautoescape %} diff --git a/src/test/resources/eager/handles-auto-escape.jinja b/src/test/resources/eager/handles-auto-escape/test.jinja similarity index 100% rename from src/test/resources/eager/handles-auto-escape.jinja rename to src/test/resources/eager/handles-auto-escape/test.jinja diff --git a/src/test/resources/eager/handles-block-set-in-deferred-if.expected.jinja b/src/test/resources/eager/handles-block-set-in-deferred-if.expected.jinja deleted file mode 100644 index c0b628f7e..000000000 --- a/src/test/resources/eager/handles-block-set-in-deferred-if.expected.jinja +++ /dev/null @@ -1,4 +0,0 @@ -{% set foo = 'empty' %}{% if deferred %} -{% set foo %}i am iron man{% endset %}{% set foo = 'I AM IRON MAN' %} -{% endif %} -{{ foo }} diff --git a/src/test/resources/eager/handles-block-set-in-deferred-if.expected.expected.jinja b/src/test/resources/eager/handles-block-set-in-deferred-if/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-block-set-in-deferred-if.expected.expected.jinja rename to src/test/resources/eager/handles-block-set-in-deferred-if/test.expected.expected.jinja diff --git a/src/test/resources/eager/handles-block-set-in-deferred-if/test.expected.jinja b/src/test/resources/eager/handles-block-set-in-deferred-if/test.expected.jinja new file mode 100644 index 000000000..56b32565b --- /dev/null +++ b/src/test/resources/eager/handles-block-set-in-deferred-if/test.expected.jinja @@ -0,0 +1,7 @@ +{% set foo = 'empty' %}\ +{% if deferred %} +{% set foo %}\ +i am iron man{% endset %}\ +{% set foo = 'I AM IRON MAN' %} +{% endif %} +{{ foo }} diff --git a/src/test/resources/eager/handles-block-set-in-deferred-if.jinja b/src/test/resources/eager/handles-block-set-in-deferred-if/test.jinja similarity index 100% rename from src/test/resources/eager/handles-block-set-in-deferred-if.jinja rename to src/test/resources/eager/handles-block-set-in-deferred-if/test.jinja diff --git a/src/test/resources/eager/handles-break-in-deferred-for-loop/test.expected.expected.jinja b/src/test/resources/eager/handles-break-in-deferred-for-loop/test.expected.expected.jinja new file mode 100644 index 000000000..4df3001da --- /dev/null +++ b/src/test/resources/eager/handles-break-in-deferred-for-loop/test.expected.expected.jinja @@ -0,0 +1,4 @@ +Start loop +i is: 0 +i is: 1 +End loop \ No newline at end of file diff --git a/src/test/resources/eager/handles-break-in-deferred-for-loop/test.expected.jinja b/src/test/resources/eager/handles-break-in-deferred-for-loop/test.expected.jinja new file mode 100644 index 000000000..c7677739d --- /dev/null +++ b/src/test/resources/eager/handles-break-in-deferred-for-loop/test.expected.jinja @@ -0,0 +1,8 @@ +Start loop +{% for i in deferred %}\ + {% if i > 1 %}\ + {% break '' %}\ + {% endif %}\ + i is: {{ i }} +{% endfor %}\ +End loop \ No newline at end of file diff --git a/src/test/resources/eager/handles-break-in-deferred-for-loop/test.jinja b/src/test/resources/eager/handles-break-in-deferred-for-loop/test.jinja new file mode 100644 index 000000000..8cd047c06 --- /dev/null +++ b/src/test/resources/eager/handles-break-in-deferred-for-loop/test.jinja @@ -0,0 +1,8 @@ +Start loop +{% for i in deferred -%} + {% if i > 1 -%} + {% break -%} + {% endif -%} + i is: {{ i }} +{% endfor -%} +End loop \ No newline at end of file diff --git a/src/test/resources/eager/handles-clashing-name-in-macro.expected.jinja b/src/test/resources/eager/handles-clashing-name-in-macro.expected.jinja deleted file mode 100644 index 7c376088d..000000000 --- a/src/test/resources/eager/handles-clashing-name-in-macro.expected.jinja +++ /dev/null @@ -1,4 +0,0 @@ -{% macro func(foo) %} -{{ foo }} -{% endmacro %}{{ func(foo=deferred) }} -1 diff --git a/src/test/resources/eager/handles-clashing-name-in-macro.expected.expected.jinja b/src/test/resources/eager/handles-clashing-name-in-macro/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-clashing-name-in-macro.expected.expected.jinja rename to src/test/resources/eager/handles-clashing-name-in-macro/test.expected.expected.jinja diff --git a/src/test/resources/eager/handles-clashing-name-in-macro/test.expected.jinja b/src/test/resources/eager/handles-clashing-name-in-macro/test.expected.jinja new file mode 100644 index 000000000..b018fdabe --- /dev/null +++ b/src/test/resources/eager/handles-clashing-name-in-macro/test.expected.jinja @@ -0,0 +1,5 @@ +{% macro func(foo) %} +{{ foo }} +{% endmacro %}\ +{{ func(foo=deferred) }} +1 diff --git a/src/test/resources/eager/handles-clashing-name-in-macro.jinja b/src/test/resources/eager/handles-clashing-name-in-macro/test.jinja similarity index 100% rename from src/test/resources/eager/handles-clashing-name-in-macro.jinja rename to src/test/resources/eager/handles-clashing-name-in-macro/test.jinja diff --git a/src/test/resources/eager/handles-complex-raw.expected.jinja b/src/test/resources/eager/handles-complex-raw.expected.jinja deleted file mode 100644 index f7fb82e5b..000000000 --- a/src/test/resources/eager/handles-complex-raw.expected.jinja +++ /dev/null @@ -1,4 +0,0 @@ -1 -{% raw %}{{ 2 }}{% endraw %} -3 -{% raw %}{{ 4 }}{% endraw %} diff --git a/src/test/resources/eager/handles-complex-raw/test.expected.jinja b/src/test/resources/eager/handles-complex-raw/test.expected.jinja new file mode 100644 index 000000000..5b922a92f --- /dev/null +++ b/src/test/resources/eager/handles-complex-raw/test.expected.jinja @@ -0,0 +1,8 @@ +1 +{% raw %}\ +{{ 2 }}\ +{% endraw %} +3 +{% raw %}\ +{{ 4 }}\ +{% endraw %} diff --git a/src/test/resources/eager/handles-complex-raw.jinja b/src/test/resources/eager/handles-complex-raw/test.jinja similarity index 100% rename from src/test/resources/eager/handles-complex-raw.jinja rename to src/test/resources/eager/handles-complex-raw/test.jinja diff --git a/src/test/resources/eager/handles-continue-in-deferred-for-loop/test.expected.expected.jinja b/src/test/resources/eager/handles-continue-in-deferred-for-loop/test.expected.expected.jinja new file mode 100644 index 000000000..a134c702a --- /dev/null +++ b/src/test/resources/eager/handles-continue-in-deferred-for-loop/test.expected.expected.jinja @@ -0,0 +1,5 @@ +Start loop +i is: 1 +i is: 3 +i is: 5 +End loop \ No newline at end of file diff --git a/src/test/resources/eager/handles-continue-in-deferred-for-loop/test.expected.jinja b/src/test/resources/eager/handles-continue-in-deferred-for-loop/test.expected.jinja new file mode 100644 index 000000000..c2ba0a12c --- /dev/null +++ b/src/test/resources/eager/handles-continue-in-deferred-for-loop/test.expected.jinja @@ -0,0 +1,8 @@ +Start loop +{% for i in deferred %}\ + {% if i % 2 == 0 %}\ + {% continue '' %}\ + {% endif %}\ + i is: {{ i }} +{% endfor %}\ +End loop \ No newline at end of file diff --git a/src/test/resources/eager/handles-continue-in-deferred-for-loop/test.jinja b/src/test/resources/eager/handles-continue-in-deferred-for-loop/test.jinja new file mode 100644 index 000000000..710517afa --- /dev/null +++ b/src/test/resources/eager/handles-continue-in-deferred-for-loop/test.jinja @@ -0,0 +1,8 @@ +Start loop +{% for i in deferred -%} + {% if i % 2 == 0 -%} + {% continue -%} + {% endif -%} + i is: {{ i }} +{% endfor -%} +End loop \ No newline at end of file diff --git a/src/test/resources/eager/handles-cycle-in-deferred-for.expected.expected.jinja b/src/test/resources/eager/handles-cycle-in-deferred-for/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-cycle-in-deferred-for.expected.expected.jinja rename to src/test/resources/eager/handles-cycle-in-deferred-for/test.expected.expected.jinja diff --git a/src/test/resources/eager/handles-cycle-in-deferred-for.expected.jinja b/src/test/resources/eager/handles-cycle-in-deferred-for/test.expected.jinja similarity index 57% rename from src/test/resources/eager/handles-cycle-in-deferred-for.expected.jinja rename to src/test/resources/eager/handles-cycle-in-deferred-for/test.expected.jinja index 6570d194a..f5289ebf5 100644 --- a/src/test/resources/eager/handles-cycle-in-deferred-for.expected.jinja +++ b/src/test/resources/eager/handles-cycle-in-deferred-for/test.expected.jinja @@ -1,3 +1,4 @@ {% for item in deferred %} {% cycle '1','2','3' %} -{% cycle '1','2','3' %}{% endfor %} +{% cycle '1','2','3' %}\ +{% endfor %} diff --git a/src/test/resources/eager/handles-cycle-in-deferred-for.jinja b/src/test/resources/eager/handles-cycle-in-deferred-for/test.jinja similarity index 50% rename from src/test/resources/eager/handles-cycle-in-deferred-for.jinja rename to src/test/resources/eager/handles-cycle-in-deferred-for/test.jinja index 75db4a6e2..9cbe73032 100644 --- a/src/test/resources/eager/handles-cycle-in-deferred-for.jinja +++ b/src/test/resources/eager/handles-cycle-in-deferred-for/test.jinja @@ -1,5 +1,8 @@ {% set foo = ['1','2','3'] %} +{% set one = '1' %} +{% set two = '2' %} +{% set three = '3' %} {%- for item in deferred %} {% cycle foo %} -{% cycle foo[0],foo[1],foo[2] %} +{% cycle one,two,three %} {%- endfor -%} diff --git a/src/test/resources/eager/handles-cycle-with-quote.expected.jinja b/src/test/resources/eager/handles-cycle-with-quote/test.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-cycle-with-quote.expected.jinja rename to src/test/resources/eager/handles-cycle-with-quote/test.expected.jinja diff --git a/src/test/resources/eager/handles-cycle-with-quote.jinja b/src/test/resources/eager/handles-cycle-with-quote/test.jinja similarity index 100% rename from src/test/resources/eager/handles-cycle-with-quote.jinja rename to src/test/resources/eager/handles-cycle-with-quote/test.jinja diff --git a/src/test/resources/eager/handles-deferred-break-in-for-loop/test.expected.expected.jinja b/src/test/resources/eager/handles-deferred-break-in-for-loop/test.expected.expected.jinja new file mode 100644 index 000000000..4df3001da --- /dev/null +++ b/src/test/resources/eager/handles-deferred-break-in-for-loop/test.expected.expected.jinja @@ -0,0 +1,4 @@ +Start loop +i is: 0 +i is: 1 +End loop \ No newline at end of file diff --git a/src/test/resources/eager/handles-deferred-break-in-for-loop/test.expected.jinja b/src/test/resources/eager/handles-deferred-break-in-for-loop/test.expected.jinja new file mode 100644 index 000000000..b37cf88b3 --- /dev/null +++ b/src/test/resources/eager/handles-deferred-break-in-for-loop/test.expected.jinja @@ -0,0 +1,24 @@ +Start loop +{% for __ignored__ in [0] %}\ +{% if 0 > deferred %}\ + {% break '' %}\ + {% endif %}\ +i is: 0 +{% if 1 > deferred %}\ + {% break '' %}\ + {% endif %}\ +i is: 1 +{% if 2 > deferred %}\ + {% break '' %}\ + {% endif %}\ +i is: 2 +{% if 3 > deferred %}\ + {% break '' %}\ + {% endif %}\ +i is: 3 +{% if 4 > deferred %}\ + {% break '' %}\ + {% endif %}\ + i is: 4 +{% endfor %}\ +End loop \ No newline at end of file diff --git a/src/test/resources/eager/handles-deferred-break-in-for-loop/test.jinja b/src/test/resources/eager/handles-deferred-break-in-for-loop/test.jinja new file mode 100644 index 000000000..65718591e --- /dev/null +++ b/src/test/resources/eager/handles-deferred-break-in-for-loop/test.jinja @@ -0,0 +1,8 @@ +Start loop +{% for i in range(5) -%} + {% if i > deferred -%} + {% break -%} + {% endif -%} + i is: {{ i }} +{% endfor -%} +End loop \ No newline at end of file diff --git a/src/test/resources/eager/handles-deferred-continue-in-for-loop/test.expected.expected.jinja b/src/test/resources/eager/handles-deferred-continue-in-for-loop/test.expected.expected.jinja new file mode 100644 index 000000000..d1616c7c9 --- /dev/null +++ b/src/test/resources/eager/handles-deferred-continue-in-for-loop/test.expected.expected.jinja @@ -0,0 +1,4 @@ +Start loop +i is: 1 +i is: 3 +End loop \ No newline at end of file diff --git a/src/test/resources/eager/handles-deferred-continue-in-for-loop/test.expected.jinja b/src/test/resources/eager/handles-deferred-continue-in-for-loop/test.expected.jinja new file mode 100644 index 000000000..52f85138e --- /dev/null +++ b/src/test/resources/eager/handles-deferred-continue-in-for-loop/test.expected.jinja @@ -0,0 +1,8 @@ +Start loop +{% for i in [0, 1, 2, 3, 4] %}\ + {% if i % deferred == 0 %}\ + {% continue '' %}\ + {% endif %}\ + i is: {{ i }} +{% endfor %}\ +End loop \ No newline at end of file diff --git a/src/test/resources/eager/handles-deferred-continue-in-for-loop/test.jinja b/src/test/resources/eager/handles-deferred-continue-in-for-loop/test.jinja new file mode 100644 index 000000000..07173400f --- /dev/null +++ b/src/test/resources/eager/handles-deferred-continue-in-for-loop/test.jinja @@ -0,0 +1,8 @@ +Start loop +{% for i in range(5) -%} + {% if i % deferred == 0 -%} + {% continue -%} + {% endif -%} + i is: {{ i }} +{% endfor -%} +End loop \ No newline at end of file diff --git a/src/test/resources/eager/handles-deferred-cycle-as.expected.jinja b/src/test/resources/eager/handles-deferred-cycle-as.expected.jinja deleted file mode 100644 index 55937f1c5..000000000 --- a/src/test/resources/eager/handles-deferred-cycle-as.expected.jinja +++ /dev/null @@ -1,7 +0,0 @@ -{% for __ignored__ in [0] %} -{% set c = [1, deferred] %} -{% if exptest:iterable.evaluate(c, ____int3rpr3t3r____) %}{{ c[0 % filter:length.filter(c, ____int3rpr3t3r____)] }}{% else %}{{ c }}{% endif %} -{% set c = [2, deferred] %} -{% if exptest:iterable.evaluate(c, ____int3rpr3t3r____) %}{{ c[1 % filter:length.filter(c, ____int3rpr3t3r____)] }}{% else %}{{ c }}{% endif %} -{% set c = [3, deferred] %} -{% if exptest:iterable.evaluate(c, ____int3rpr3t3r____) %}{{ c[2 % filter:length.filter(c, ____int3rpr3t3r____)] }}{% else %}{{ c }}{% endif %}{% endfor %} diff --git a/src/test/resources/eager/handles-deferred-cycle-as.expected.expected.jinja b/src/test/resources/eager/handles-deferred-cycle-as/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-deferred-cycle-as.expected.expected.jinja rename to src/test/resources/eager/handles-deferred-cycle-as/test.expected.expected.jinja diff --git a/src/test/resources/eager/handles-deferred-cycle-as/test.expected.jinja b/src/test/resources/eager/handles-deferred-cycle-as/test.expected.jinja new file mode 100644 index 000000000..6c84f4a4f --- /dev/null +++ b/src/test/resources/eager/handles-deferred-cycle-as/test.expected.jinja @@ -0,0 +1,20 @@ +{% for __ignored__ in [0] %} +{% set c = [1, deferred] %} +{% if exptest:iterable.evaluate(c, null) %}\ +{{ c[0 % filter:length.filter(c, ____int3rpr3t3r____)] }}\ +{% else %}\ +{{ c }}\ +{% endif %} +{% set c = [2, deferred] %} +{% if exptest:iterable.evaluate(c, null) %}\ +{{ c[1 % filter:length.filter(c, ____int3rpr3t3r____)] }}\ +{% else %}\ +{{ c }}\ +{% endif %} +{% set c = [3, deferred] %} +{% if exptest:iterable.evaluate(c, null) %}\ +{{ c[2 % filter:length.filter(c, ____int3rpr3t3r____)] }}\ +{% else %}\ +{{ c }}\ +{% endif %}\ +{% endfor %} diff --git a/src/test/resources/eager/handles-deferred-cycle-as.jinja b/src/test/resources/eager/handles-deferred-cycle-as/test.jinja similarity index 100% rename from src/test/resources/eager/handles-deferred-cycle-as.jinja rename to src/test/resources/eager/handles-deferred-cycle-as/test.jinja 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 deleted file mode 100644 index 22ad3b882..000000000 --- a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.jinja +++ /dev/null @@ -1,13 +0,0 @@ -{% macro getData() %} - - -{% for __ignored__ in [0] %} -{% macro doIt(val) %} -{{ deferred ~ filter:tojson.filter(val, ____int3rpr3t3r____) }} -{% endmacro %}{% set val = {'a': 'a'} %}{{ filter:upper.filter(doIt(val), ____int3rpr3t3r____) }} - -{% macro doIt(val) %} -{{ deferred ~ filter:tojson.filter(val, ____int3rpr3t3r____) }} -{% endmacro %}{% set val = {'b': 'b'} %}{{ filter:upper.filter(doIt(val), ____int3rpr3t3r____) }} -{% endfor %} -{% endmacro %}{{ filter:upper.filter(getData(), ____int3rpr3t3r____) }} diff --git a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.expected.jinja b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-deferred-for-loop-var-from-macro.expected.expected.jinja rename to src/test/resources/eager/handles-deferred-for-loop-var-from-macro/test.expected.expected.jinja diff --git a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro/test.expected.jinja b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro/test.expected.jinja new file mode 100644 index 000000000..54d9fc69a --- /dev/null +++ b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro/test.expected.jinja @@ -0,0 +1,16 @@ +{% set __macro_getData_357124436_temp_variable_0__ %} + + +{% for __ignored__ in [0] %} +{% set __macro_doIt_1327224118_temp_variable_0__ %} +{{ deferred ~ '{\"a\":\"a\"}' }} +{% endset %}\ +{{ filter:upper.filter(__macro_doIt_1327224118_temp_variable_0__, ____int3rpr3t3r____) }} + +{% set __macro_doIt_1327224118_temp_variable_1__ %} +{{ deferred ~ '{\"b\":\"b\"}' }} +{% endset %}\ +{{ filter:upper.filter(__macro_doIt_1327224118_temp_variable_1__, ____int3rpr3t3r____) }} +{% endfor %} +{% endset %}\ +{{ filter:upper.filter(__macro_getData_357124436_temp_variable_0__, ____int3rpr3t3r____) }} diff --git a/src/test/resources/eager/handles-deferred-for-loop-var-from-macro.jinja b/src/test/resources/eager/handles-deferred-for-loop-var-from-macro/test.jinja similarity index 100% rename from src/test/resources/eager/handles-deferred-for-loop-var-from-macro.jinja rename to src/test/resources/eager/handles-deferred-for-loop-var-from-macro/test.jinja 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 deleted file mode 100644 index c7418e8a3..000000000 --- a/src/test/resources/eager/handles-deferred-from-import-as.expected.jinja +++ /dev/null @@ -1,5 +0,0 @@ -{% set myname = deferred + 7 %}{% set __ignored__ %} -{% set bar = myname + 19 %} -Hello {{ myname }} -{% set from_bar = bar %}{% endset %}from_foo: Hello {{ myname }} -from_bar: {{ from_bar }} diff --git a/src/test/resources/eager/handles-deferred-from-import-as.expected.expected.jinja b/src/test/resources/eager/handles-deferred-from-import-as/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-deferred-from-import-as.expected.expected.jinja rename to src/test/resources/eager/handles-deferred-from-import-as/test.expected.expected.jinja diff --git a/src/test/resources/eager/handles-deferred-from-import-as/test.expected.jinja b/src/test/resources/eager/handles-deferred-from-import-as/test.expected.jinja new file mode 100644 index 000000000..f4b6093cd --- /dev/null +++ b/src/test/resources/eager/handles-deferred-from-import-as/test.expected.jinja @@ -0,0 +1,12 @@ +{% set myname = deferred + 7 %}\ +{% do %} +{% set bar = myname + 19 %} +{% for __ignored__ in [0] %}\ +Hello {{ myname }}\ +{% endfor %} +{% set from_bar = bar %}\ +{% enddo %}\ +from_foo: {% for __ignored__ in [0] %}\ +Hello {{ myname }}\ +{% endfor %} +from_bar: {{ from_bar }} diff --git a/src/test/resources/eager/handles-deferred-from-import-as.jinja b/src/test/resources/eager/handles-deferred-from-import-as/test.jinja similarity index 50% rename from src/test/resources/eager/handles-deferred-from-import-as.jinja rename to src/test/resources/eager/handles-deferred-from-import-as/test.jinja index 193a3e315..5716196eb 100644 --- a/src/test/resources/eager/handles-deferred-from-import-as.jinja +++ b/src/test/resources/eager/handles-deferred-from-import-as/test.jinja @@ -1,4 +1,4 @@ {%- set myname = deferred + (3 + 4) -%} -{%- from "macro-and-set.jinja" import foo as from_foo, bar as from_bar -%} +{%- from "../supplements/macro-and-set.jinja" import foo as from_foo, bar as from_bar -%} from_foo: {{ from_foo() }} 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 deleted file mode 100644 index 8210627d6..000000000 --- a/src/test/resources/eager/handles-deferred-import-vars.expected.jinja +++ /dev/null @@ -1,11 +0,0 @@ -{% set myname = deferred + 3 %}{% set __ignored__ %} -{% set bar = myname + 19 %} -Hello {{ myname }} -{% endset %}foo: 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}) %} -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-import-vars.expected.expected.jinja b/src/test/resources/eager/handles-deferred-import-vars/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-deferred-import-vars.expected.expected.jinja rename to src/test/resources/eager/handles-deferred-import-vars/test.expected.expected.jinja diff --git a/src/test/resources/eager/handles-deferred-import-vars/test.expected.jinja b/src/test/resources/eager/handles-deferred-import-vars/test.expected.jinja new file mode 100644 index 000000000..a04803b11 --- /dev/null +++ b/src/test/resources/eager/handles-deferred-import-vars/test.expected.jinja @@ -0,0 +1,34 @@ +{% set myname = deferred + 3 %}\ +{% do %} +{% set bar = myname + 19 %} +{% for __ignored__ in [0] %}\ +Hello {{ myname }}\ +{% endfor %} +{% enddo %}\ +foo: {% for __ignored__ in [0] %}\ +Hello {{ myname }}\ +{% endfor %} +bar: {{ bar }} +--- +{% set myname = deferred + 7 %}\ +{% do %}\ +{% set __temp_meta_current_path_822093108__,current_path = current_path,'eager/supplements/macro-and-set.jinja' %}\ +{% set __temp_meta_import_alias_902286926__ = {} %}\ +{% for __ignored__ in [0] %} +{% set bar = myname + 19 %}\ +{% do __temp_meta_import_alias_902286926__.update({'bar': bar}) %} +{% for __ignored__ in [0] %}\ +Hello {{ myname }}\ +{% endfor %} +{% do __temp_meta_import_alias_902286926__.update({'import_resource_path': 'eager/supplements/macro-and-set.jinja'}) %}\ +{% endfor %}\ +{% set simple = __temp_meta_import_alias_902286926__ %}\ +{% set current_path,__temp_meta_current_path_822093108__ = __temp_meta_current_path_822093108__,null %}\ +{% enddo %}\ +simple.foo: {% set deferred_import_resource_path = 'eager/supplements/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-import-vars.jinja b/src/test/resources/eager/handles-deferred-import-vars/test.jinja similarity index 57% rename from src/test/resources/eager/handles-deferred-import-vars.jinja rename to src/test/resources/eager/handles-deferred-import-vars/test.jinja index 885f2d3f7..303c076aa 100644 --- a/src/test/resources/eager/handles-deferred-import-vars.jinja +++ b/src/test/resources/eager/handles-deferred-import-vars/test.jinja @@ -1,9 +1,9 @@ {%- set myname = deferred + (1 + 2) -%} -{%- from "macro-and-set.jinja" import foo, bar -%} +{%- from "../supplements/macro-and-set.jinja" import foo, bar -%} foo: {{ foo() }} bar: {{ bar }} --- {% set myname = deferred + (3 + 4) -%} -{%- import "macro-and-set.jinja" as simple -%} +{%- import "../supplements/macro-and-set.jinja" as simple -%} simple.foo: {{ simple.foo() }} simple.bar: {{ simple.bar }} diff --git a/src/test/resources/eager/handles-deferred-in-cycle.expected.jinja b/src/test/resources/eager/handles-deferred-in-cycle/test.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-deferred-in-cycle.expected.jinja rename to src/test/resources/eager/handles-deferred-in-cycle/test.expected.jinja diff --git a/src/test/resources/eager/handles-deferred-in-cycle.jinja b/src/test/resources/eager/handles-deferred-in-cycle/test.jinja similarity index 100% rename from src/test/resources/eager/handles-deferred-in-cycle.jinja rename to src/test/resources/eager/handles-deferred-in-cycle/test.jinja diff --git a/src/test/resources/eager/handles-deferred-in-ifchanged.expected.jinja b/src/test/resources/eager/handles-deferred-in-ifchanged.expected.jinja deleted file mode 100644 index fa2d2883a..000000000 --- a/src/test/resources/eager/handles-deferred-in-ifchanged.expected.jinja +++ /dev/null @@ -1 +0,0 @@ -{% for __ignored__ in [0] %}{{ deferred[1] }}{{ deferred[2] }}{{ deferred[1] }}{% endfor %} diff --git a/src/test/resources/eager/handles-deferred-in-ifchanged/test.expected.jinja b/src/test/resources/eager/handles-deferred-in-ifchanged/test.expected.jinja new file mode 100644 index 000000000..c7fc38922 --- /dev/null +++ b/src/test/resources/eager/handles-deferred-in-ifchanged/test.expected.jinja @@ -0,0 +1,5 @@ +{% for __ignored__ in [0] %}\ +{{ deferred[1] }}\ +{{ deferred[2] }}\ +{{ deferred[1] }}\ +{% endfor %} diff --git a/src/test/resources/eager/handles-deferred-in-ifchanged.jinja b/src/test/resources/eager/handles-deferred-in-ifchanged/test.jinja similarity index 100% rename from src/test/resources/eager/handles-deferred-in-ifchanged.jinja rename to src/test/resources/eager/handles-deferred-in-ifchanged/test.jinja diff --git a/src/test/resources/eager/handles-deferred-in-namespace.expected.jinja b/src/test/resources/eager/handles-deferred-in-namespace.expected.jinja deleted file mode 100644 index 3a3108fb7..000000000 --- a/src/test/resources/eager/handles-deferred-in-namespace.expected.jinja +++ /dev/null @@ -1,6 +0,0 @@ -{% set ns2 = namespace({}) %}{% do ns2.update({'a': deferred}) %} -{% set ns1 = namespace({'a': false}) %}{% if deferred %} - {% set ns1.a = true %} -{% endif %} -{{ ns1.a }} -{{ ns2.a }} diff --git a/src/test/resources/eager/handles-deferred-in-namespace.expected.expected.jinja b/src/test/resources/eager/handles-deferred-in-namespace/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-deferred-in-namespace.expected.expected.jinja rename to src/test/resources/eager/handles-deferred-in-namespace/test.expected.expected.jinja diff --git a/src/test/resources/eager/handles-deferred-in-namespace/test.expected.jinja b/src/test/resources/eager/handles-deferred-in-namespace/test.expected.jinja new file mode 100644 index 000000000..948dd0bc5 --- /dev/null +++ b/src/test/resources/eager/handles-deferred-in-namespace/test.expected.jinja @@ -0,0 +1,8 @@ +{% set ns2 = namespace({} ) %}\ +{% do ns2.update({'a': deferred}) %} +{% set ns1 = namespace({'a': false} ) %}\ +{% if deferred %} + {% set ns1.a = true %} +{% endif %} +{{ ns1.a }} +{{ ns2.a }} diff --git a/src/test/resources/eager/handles-deferred-in-namespace.jinja b/src/test/resources/eager/handles-deferred-in-namespace/test.jinja similarity index 100% rename from src/test/resources/eager/handles-deferred-in-namespace.jinja rename to src/test/resources/eager/handles-deferred-in-namespace/test.jinja diff --git a/src/test/resources/eager/handles-deferred-modification-in-caller/test.expected.expected.jinja b/src/test/resources/eager/handles-deferred-modification-in-caller/test.expected.expected.jinja new file mode 100644 index 000000000..06d144d5b --- /dev/null +++ b/src/test/resources/eager/handles-deferred-modification-in-caller/test.expected.expected.jinja @@ -0,0 +1,2 @@ +['a', 'b', 'c'] +['a', 'b', 'c', 'd'] diff --git a/src/test/resources/eager/handles-deferred-modification-in-caller/test.expected.jinja b/src/test/resources/eager/handles-deferred-modification-in-caller/test.expected.jinja new file mode 100644 index 000000000..5933433f9 --- /dev/null +++ b/src/test/resources/eager/handles-deferred-modification-in-caller/test.expected.jinja @@ -0,0 +1,14 @@ +{% set my_list = ['a', 'b'] %}\ +{% set __macro_callerino_729568755_temp_variable_0__ %}\ +{% set __macro_caller_172086791_temp_variable_0__ %}\ +{% do my_list.append(deferred) %}\ +{{ my_list }}\ +{% endset %}\ +{{ __macro_caller_172086791_temp_variable_0__ }}\ +{% do my_list.append('d') %}\ +{% endset %}\ +{% call __macro_callerino_729568755_temp_variable_0__ %}\ +{% do my_list.append(deferred) %}\ +{{ my_list }}\ +{% endcall %} +{{ my_list }} diff --git a/src/test/resources/eager/handles-deferred-modification-in-caller/test.jinja b/src/test/resources/eager/handles-deferred-modification-in-caller/test.jinja new file mode 100644 index 000000000..c0ae33a3c --- /dev/null +++ b/src/test/resources/eager/handles-deferred-modification-in-caller/test.jinja @@ -0,0 +1,11 @@ +{% macro callerino() -%} + {% do my_list.append('b') -%} + {{ caller() }} + {%- do my_list.append('d') -%} +{%- endmacro %} +{% set my_list = ['a'] %} +{% call callerino() -%} + {%- do my_list.append(deferred) -%} + {{ my_list }} +{%- endcall %} +{{ my_list }} diff --git a/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/base.jinja b/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/base.jinja new file mode 100644 index 000000000..36cc1947c --- /dev/null +++ b/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/base.jinja @@ -0,0 +1,27 @@ +{% set tracker_base = '1_base' %} + +tracker_base is '1_base': {{ tracker_base }}? {{ tracker_base == '1_base' }} +tracker_middle is 'resolved': {{ tracker_middle }}? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}? {{ tracker_test == 'resolved' }} +-----Pre-First----- +{% block first -%} +tracker_base is 'resolved': {{ tracker_base }}? {{ tracker_base == 'resolved' }} +tracker_middle is 'resolved': {{ tracker_middle }}? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}? {{ tracker_test == 'resolved' }} +{%- endblock %} +-----Post-First----- +tracker_base is '1_base': {{ tracker_base }}? {{ tracker_base == '1_base' }} +tracker_middle is 'resolved': {{ tracker_middle }}? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}? {{ tracker_test == 'resolved' }} +-----Pre-Second----- +{% block second -%} +tracker_base is 'resolved': {{ tracker_base }}? {{ tracker_base == 'resolved' }} +tracker_middle is 'resolved': {{ tracker_middle }}? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}? {{ tracker_test == 'resolved' }} +{%- endblock %} +-----Post-Second----- +Deferring tracker base.{# This message WILL show up in final output #} +{% set tracker_base = deferred %} +tracker_base is 'resolved': {{ tracker_base }}? {{ tracker_base == 'resolved' }} +tracker_middle is 'resolved': {{ tracker_middle }}? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}? {{ tracker_test == 'resolved' }} diff --git a/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/middle.jinja b/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/middle.jinja new file mode 100644 index 000000000..c4f5dcbde --- /dev/null +++ b/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/middle.jinja @@ -0,0 +1,13 @@ +{% extends '../../eager/handles-deferred-used-in-multiple-block-levels/base.jinja' %} +{% set tracker_middle = '2_middle' %} +{% block first %} +I WON'T SHOW UP +{% endblock %} + +{% block second %} +tracker_base is 'resolved': {{ tracker_base }}? {{ tracker_base == 'resolved' }} +tracker_middle is 'resolved': {{ tracker_middle }}? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}? {{ tracker_test == 'resolved' }} +{% endblock %} +Deferring tracker middle.{# This message will not show up in final output #} +{% set tracker_middle = deferred %} diff --git a/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/test.expected.expected.jinja b/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/test.expected.expected.jinja new file mode 100644 index 000000000..0c6b1adf8 --- /dev/null +++ b/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/test.expected.expected.jinja @@ -0,0 +1,25 @@ +tracker_base is '1_base': 1_base? true +tracker_middle is 'resolved': resolved? true +tracker_test is 'resolved': resolved? true +-----Pre-First----- + +tracker_base is 'resolved': resolved? true +tracker_middle is 'resolved': resolved? true +tracker_test is 'resolved': resolved? true + +-----Post-First----- +tracker_base is '1_base': 1_base? true +tracker_middle is 'resolved': resolved? true +tracker_test is 'resolved': resolved? true +-----Pre-Second----- + +tracker_base is 'resolved': resolved? true +tracker_middle is 'resolved': resolved? true +tracker_test is 'resolved': resolved? true + +-----Post-Second----- +Deferring tracker base. + +tracker_base is 'resolved': resolved? true +tracker_middle is 'resolved': resolved? true +tracker_test is 'resolved': resolved? true \ No newline at end of file diff --git a/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/test.expected.jinja b/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/test.expected.jinja new file mode 100644 index 000000000..2e70222aa --- /dev/null +++ b/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/test.expected.jinja @@ -0,0 +1,58 @@ +{# Start Label: ignored_output_from_extends #}{% do %} + + + +Deferring tracker test. +{% set tracker_test = deferred %} + + + + + +Deferring tracker middle. +{% set tracker_middle = deferred %} +{% enddo %}\ +{# End Label: ignored_output_from_extends #}{% set current_path = 'eager/handles-deferred-used-in-multiple-block-levels/base.jinja' %} + +tracker_base is '1_base': 1_base? true +tracker_middle is 'resolved': {{ tracker_middle }}\ +? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}\ +? {{ tracker_test == 'resolved' }} +-----Pre-First----- +{% block first %}\ +{% set __temp_meta_current_path_1057627035__,current_path = current_path,'eager/handles-deferred-used-in-multiple-block-levels/test.jinja' %} +tracker_base is 'resolved': {{ tracker_base }}\ +? {{ tracker_base == 'resolved' }} +tracker_middle is 'resolved': {{ tracker_middle }}\ +? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}\ +? {{ tracker_test == 'resolved' }} +{% set current_path,__temp_meta_current_path_1057627035__ = __temp_meta_current_path_1057627035__,null %}\ +{% endblock first %} +-----Post-First----- +tracker_base is '1_base': 1_base? true +tracker_middle is 'resolved': {{ tracker_middle }}\ +? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}\ +? {{ tracker_test == 'resolved' }} +-----Pre-Second----- +{% block second %}\ +{% set __temp_meta_current_path_697061783__,current_path = current_path,'eager/handles-deferred-used-in-multiple-block-levels/middle.jinja' %} +tracker_base is 'resolved': {{ tracker_base }}\ +? {{ tracker_base == 'resolved' }} +tracker_middle is 'resolved': {{ tracker_middle }}\ +? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}\ +? {{ tracker_test == 'resolved' }} +{% set current_path,__temp_meta_current_path_697061783__ = __temp_meta_current_path_697061783__,null %}\ +{% endblock second %} +-----Post-Second----- +Deferring tracker base. +{% set tracker_base = deferred %} +tracker_base is 'resolved': {{ tracker_base }}\ +? {{ tracker_base == 'resolved' }} +tracker_middle is 'resolved': {{ tracker_middle }}\ +? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}\ +? {{ tracker_test == 'resolved' }} \ No newline at end of file diff --git a/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/test.jinja b/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/test.jinja new file mode 100644 index 000000000..94aecd88c --- /dev/null +++ b/src/test/resources/eager/handles-deferred-used-in-multiple-block-levels/test.jinja @@ -0,0 +1,10 @@ +{% extends '../../eager/handles-deferred-used-in-multiple-block-levels/middle.jinja' %} + +{% set tracker_test = '3_test' %} +{% block first %} +tracker_base is 'resolved': {{ tracker_base }}? {{ tracker_base == 'resolved' }} +tracker_middle is 'resolved': {{ tracker_middle }}? {{ tracker_middle == 'resolved' }} +tracker_test is 'resolved': {{ tracker_test }}? {{ tracker_test == 'resolved' }} +{% endblock %} +Deferring tracker test.{# This message will not show up in final output #} +{% set tracker_test = deferred %} diff --git a/src/test/resources/eager/handles-deferred-value-in-render-filter/test.expected.jinja b/src/test/resources/eager/handles-deferred-value-in-render-filter/test.expected.jinja new file mode 100644 index 000000000..21a51dcc1 --- /dev/null +++ b/src/test/resources/eager/handles-deferred-value-in-render-filter/test.expected.jinja @@ -0,0 +1,9 @@ +{% set __render_524436216_temp_variable__ %}\ +Hi {{ filter:escape.filter(deferred, ____int3rpr3t3r____) }}\ +{% endset %}\ +{{ filter:escape_jinjava.filter(__render_524436216_temp_variable__, ____int3rpr3t3r____) }} + +{% set __render_524436216_temp_variable__ %}\ +Hi {{ filter:escape.filter(deferred, ____int3rpr3t3r____) }}\ +{% endset %}\ +{{ __render_524436216_temp_variable__ }} \ No newline at end of file diff --git a/src/test/resources/eager/handles-deferred-value-in-render-filter/test.jinja b/src/test/resources/eager/handles-deferred-value-in-render-filter/test.jinja new file mode 100644 index 000000000..25c5adf9d --- /dev/null +++ b/src/test/resources/eager/handles-deferred-value-in-render-filter/test.jinja @@ -0,0 +1,4 @@ +{% set foo = "Hi {{ deferred|escape }}" %} +{{ foo|render|escape_jinjava }} + +{{ foo|render }} \ No newline at end of file diff --git a/src/test/resources/eager/handles-deferring-loop-variable.expected.jinja b/src/test/resources/eager/handles-deferring-loop-variable/test.expected.jinja similarity index 51% rename from src/test/resources/eager/handles-deferring-loop-variable.expected.jinja rename to src/test/resources/eager/handles-deferring-loop-variable/test.expected.jinja index 740c79b89..d27a1c16c 100644 --- a/src/test/resources/eager/handles-deferring-loop-variable.expected.jinja +++ b/src/test/resources/eager/handles-deferring-loop-variable/test.expected.jinja @@ -1,14 +1,17 @@ {% for __ignored__ in [0] %} -{% if deferred && true %}first time! +{% if deferred && true %}\ +first time! {% endif %} 1 -{% if deferred && false %}first time! +{% if deferred && false %}\ +first time! {% endif %} 2 {% endfor %} {% for i in [0, 1] %} -{% if deferred && loop.isLast() %}last time! +{% if deferred && loop.isLast() %}\ +last time! {% endif %} {{ loop.index }} {% endfor %} diff --git a/src/test/resources/eager/handles-deferring-loop-variable.jinja b/src/test/resources/eager/handles-deferring-loop-variable/test.jinja similarity index 100% rename from src/test/resources/eager/handles-deferring-loop-variable.jinja rename to src/test/resources/eager/handles-deferring-loop-variable/test.jinja diff --git a/src/test/resources/eager/handles-double-import-modification.expected.jinja b/src/test/resources/eager/handles-double-import-modification.expected.jinja deleted file mode 100644 index 34ca58f1e..000000000 --- a/src/test/resources/eager/handles-double-import-modification.expected.jinja +++ /dev/null @@ -1,20 +0,0 @@ -{% 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}) %} - -{% 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 %} ---- -{% 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}) %} - -{% 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 %} ---- -{{ bar1.foo }} -{{ bar2.foo }} \ No newline at end of file diff --git a/src/test/resources/eager/handles-double-import-modification.jinja b/src/test/resources/eager/handles-double-import-modification.jinja deleted file mode 100644 index f43bdf282..000000000 --- a/src/test/resources/eager/handles-double-import-modification.jinja +++ /dev/null @@ -1,6 +0,0 @@ -{% import "deferred-modification.jinja" as bar1 %} ---- -{% import "deferred-modification.jinja" as bar2 %} ---- -{{ bar1.foo }} -{{ bar2.foo }} diff --git a/src/test/resources/eager/handles-double-import-modification.expected.expected.jinja b/src/test/resources/eager/handles-double-import-modification/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-double-import-modification.expected.expected.jinja rename to src/test/resources/eager/handles-double-import-modification/test.expected.expected.jinja diff --git a/src/test/resources/eager/handles-double-import-modification/test.expected.jinja b/src/test/resources/eager/handles-double-import-modification/test.expected.jinja new file mode 100644 index 000000000..66acdde36 --- /dev/null +++ b/src/test/resources/eager/handles-double-import-modification/test.expected.jinja @@ -0,0 +1,40 @@ +{% do %}\ +{% set __temp_meta_current_path_461135149__,current_path = current_path,'eager/supplements/deferred-modification.jinja' %}\ +{% set __temp_meta_import_alias_3016318__ = {} %}\ +{% for __ignored__ in [0] %}\ +{% if deferred %} + +{% set foo = 'a' %}\ +{% do __temp_meta_import_alias_3016318__.update({'foo': foo}) %} + +{% endif %} + +{% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}\ +{% do __temp_meta_import_alias_3016318__.update({'foo': foo}) %} +{% do __temp_meta_import_alias_3016318__.update({'foo': foo,'import_resource_path': 'eager/supplements/deferred-modification.jinja'}) %}\ +{% endfor %}\ +{% set bar1 = __temp_meta_import_alias_3016318__ %}\ +{% set current_path,__temp_meta_current_path_461135149__ = __temp_meta_current_path_461135149__,null %}\ +{% enddo %} +--- +{% do %}\ +{% set __temp_meta_current_path_461135149__,current_path = current_path,'eager/supplements/deferred-modification.jinja' %}\ +{% set __temp_meta_import_alias_3016319__ = {} %}\ +{% for __ignored__ in [0] %}\ +{% if deferred %} + +{% set foo = 'a' %}\ +{% do __temp_meta_import_alias_3016319__.update({'foo': foo}) %} + +{% endif %} + +{% set foo = filter:join.filter([foo, 'b'], ____int3rpr3t3r____, '') %}\ +{% do __temp_meta_import_alias_3016319__.update({'foo': foo}) %} +{% do __temp_meta_import_alias_3016319__.update({'foo': foo,'import_resource_path': 'eager/supplements/deferred-modification.jinja'}) %}\ +{% endfor %}\ +{% set bar2 = __temp_meta_import_alias_3016319__ %}\ +{% set current_path,__temp_meta_current_path_461135149__ = __temp_meta_current_path_461135149__,null %}\ +{% enddo %} +--- +{{ bar1.foo }} +{{ bar2.foo }} diff --git a/src/test/resources/eager/handles-double-import-modification/test.jinja b/src/test/resources/eager/handles-double-import-modification/test.jinja new file mode 100644 index 000000000..ba3e60e62 --- /dev/null +++ b/src/test/resources/eager/handles-double-import-modification/test.jinja @@ -0,0 +1,6 @@ +{% import "../supplements/deferred-modification.jinja" as bar1 %} +--- +{% import "../supplements/deferred-modification.jinja" as bar2 %} +--- +{{ bar1.foo }} +{{ bar2.foo }} 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 deleted file mode 100644 index 6ba812599..000000000 --- a/src/test/resources/eager/handles-duplicate-variable-reference-modification.expected.jinja +++ /dev/null @@ -1,9 +0,0 @@ -{% set some_list = [] %}{% set the_list = some_list %}{% if deferred %} -{% do some_list.append(deferred) %} -{% endif %} -{{ the_list }} - - -{% set foo = [1] %}{% do foo.append(deferred) %} -{% do foo.append(2) %} -{% set bar = foo %}{{ foo ~ 'and' ~ bar }} diff --git a/src/test/resources/eager/handles-duplicate-variable-reference-modification/test.expected.jinja b/src/test/resources/eager/handles-duplicate-variable-reference-modification/test.expected.jinja new file mode 100644 index 000000000..99f473cf6 --- /dev/null +++ b/src/test/resources/eager/handles-duplicate-variable-reference-modification/test.expected.jinja @@ -0,0 +1,16 @@ +{% set the_list = [] %}\ +{% set __macro_appender_2138849093_temp_variable_0__ %}\ +{% set some_list = the_list %}\ +{% if deferred %} +{% do some_list.append(deferred) %} +{% endif %}\ +{% endset %}\ +{{ __macro_appender_2138849093_temp_variable_0__ }} +{{ the_list }} + + +{% set foo = [1] %}\ +{% do foo.append(deferred) %} +{% do foo.append(2) %} +{% set bar = foo %}\ +{{ foo ~ 'and' ~ bar }} diff --git a/src/test/resources/eager/handles-duplicate-variable-reference-modification.jinja b/src/test/resources/eager/handles-duplicate-variable-reference-modification/test.jinja similarity index 100% rename from src/test/resources/eager/handles-duplicate-variable-reference-modification.jinja rename to src/test/resources/eager/handles-duplicate-variable-reference-modification/test.jinja diff --git a/src/test/resources/eager/handles-duplicate-variable-reference-speculative-modification/test.expected.jinja b/src/test/resources/eager/handles-duplicate-variable-reference-speculative-modification/test.expected.jinja new file mode 100644 index 000000000..92a3012b1 --- /dev/null +++ b/src/test/resources/eager/handles-duplicate-variable-reference-speculative-modification/test.expected.jinja @@ -0,0 +1,7 @@ +{% set foo = ['a', 1] %}\ +{% set bar = foo %}\ +{% if deferred %} +{% do bar.append(2) %} +{% endif %} +{% do bar.append(3) %} +{{ foo ~ 'and' ~ bar }} diff --git a/src/test/resources/eager/handles-duplicate-variable-reference-speculative-modification/test.jinja b/src/test/resources/eager/handles-duplicate-variable-reference-speculative-modification/test.jinja new file mode 100644 index 000000000..ecf1af7be --- /dev/null +++ b/src/test/resources/eager/handles-duplicate-variable-reference-speculative-modification/test.jinja @@ -0,0 +1,8 @@ +{% set foo = ['a'] -%} +{%- set bar = foo -%} +{% do bar.append(1) %} +{% if deferred %} +{% do bar.append(2) %} +{% endif %} +{% do bar.append(3) %} +{{ foo ~ 'and' ~ bar }} diff --git a/src/test/resources/eager/handles-eager-print-and-do.expected.jinja b/src/test/resources/eager/handles-eager-print-and-do/test.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-eager-print-and-do.expected.jinja rename to src/test/resources/eager/handles-eager-print-and-do/test.expected.jinja diff --git a/src/test/resources/eager/handles-eager-print-and-do.jinja b/src/test/resources/eager/handles-eager-print-and-do/test.jinja similarity index 100% rename from src/test/resources/eager/handles-eager-print-and-do.jinja rename to src/test/resources/eager/handles-eager-print-and-do/test.jinja 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 deleted file mode 100644 index 98f6f7bc2..000000000 --- a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.expected.jinja +++ /dev/null @@ -1,8 +0,0 @@ -C: ['a', 'b', 'c']. -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']. 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 deleted file mode 100644 index 3933f23f6..000000000 --- a/src/test/resources/eager/handles-higher-scope-reference-modification.expected.jinja +++ /dev/null @@ -1,10 +0,0 @@ -{% 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' : '') %} -A: {{ a_list }}. diff --git a/src/test/resources/eager/handles-higher-scope-reference-modification/test.expected.expected.jinja b/src/test/resources/eager/handles-higher-scope-reference-modification/test.expected.expected.jinja new file mode 100644 index 000000000..b32d28d29 --- /dev/null +++ b/src/test/resources/eager/handles-higher-scope-reference-modification/test.expected.expected.jinja @@ -0,0 +1,8 @@ +C: ['a', 'b', 'c']. +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']. diff --git a/src/test/resources/eager/handles-higher-scope-reference-modification/test.expected.jinja b/src/test/resources/eager/handles-higher-scope-reference-modification/test.expected.jinja new file mode 100644 index 000000000..bbb06de9d --- /dev/null +++ b/src/test/resources/eager/handles-higher-scope-reference-modification/test.expected.jinja @@ -0,0 +1,35 @@ +{% set a_list = ['a'] %}\ +{% set __macro_b_125206_temp_variable_0__ %}\ +{% set b_list = a_list %}\ +{% 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 }}\ +.{% endset %}\ +{{ __macro_b_125206_temp_variable_0__ }}\ +{% do a_list.append(deferred ? 'A' : '') %} +A: {{ a_list }}\ +. +--- +{% set a_list = ['a', 'b'] %}\ +{% for __ignored__ in [0] %}\ +{% set b_list = a_list %}\ +{% for __ignored__ in [0] %}\ +{% set c_list = a_list %}\ +{% if !deferred %}\ +{% do c_list.append(deferred ? 'c' : '') %}\ +{% else %}\ +{% do c_list.append(deferred ? 'c' : '') %}\ +{% endif %} +C: {{ c_list }}\ +.{% endfor %}\ +{% do b_list.append(deferred ? 'B' : '') %} +B: {{ 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/test.jinja similarity index 69% rename from src/test/resources/eager/handles-higher-scope-reference-modification.jinja rename to src/test/resources/eager/handles-higher-scope-reference-modification/test.jinja index 28a7615c3..bb4ef7645 100644 --- a/src/test/resources/eager/handles-higher-scope-reference-modification.jinja +++ b/src/test/resources/eager/handles-higher-scope-reference-modification/test.jinja @@ -7,11 +7,11 @@ 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) }} -{%- do a_list.append(deferred ? 'a' : '') %} +{%- do a_list.append(deferred ? 'A' : '') %} A: {{ a_list }}. --- {% set a_list = [] %} @@ -21,11 +21,15 @@ A: {{ a_list }}. {%- do b_list.append('b') %} {%- for j in range(1) %} {%- set c_list = b_list %} +{%- if !deferred %} +{%- do c_list.append(deferred ? 'c' : '') %} +{%- else %} {%- do c_list.append(deferred ? 'c' : '') %} +{%- endif %} 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 }}. 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 deleted file mode 100644 index 1ead133c6..000000000 --- a/src/test/resources/eager/handles-import-in-deferred-if.expected.jinja +++ /dev/null @@ -1,9 +0,0 @@ -{% 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 }} diff --git a/src/test/resources/eager/handles-import-in-deferred-if/test.expected.jinja b/src/test/resources/eager/handles-import-in-deferred-if/test.expected.jinja new file mode 100644 index 000000000..b26dd23fc --- /dev/null +++ b/src/test/resources/eager/handles-import-in-deferred-if/test.expected.jinja @@ -0,0 +1,23 @@ +{% set primary_line_height = 100 %}\ +{% if deferred %} +{% do %}\ +{% set __temp_meta_current_path_724462665__,current_path = current_path,'eager/supplements/set-val.jinja' %}\ +{% set __temp_meta_import_alias_902286926__ = {} %}\ +{% for __ignored__ in [0] %}\ +{% set primary_line_height = 42 %}\ +{% do __temp_meta_import_alias_902286926__.update({'primary_line_height': primary_line_height}) %}\ +{% do __temp_meta_import_alias_902286926__.update({'primary_line_height': 42,'import_resource_path': 'eager/supplements/set-val.jinja'}) %}\ +{% endfor %}\ +{% set simple = __temp_meta_import_alias_902286926__ %}\ +{% set current_path,__temp_meta_current_path_724462665__ = __temp_meta_current_path_724462665__,null %}\ +{% enddo %} +{% else %} +{% do %}\ +{% set __temp_meta_current_path_724462665__,current_path = current_path,'eager/supplements/set-val.jinja' %}\ +{% set primary_line_height = 42 %}\ +{% set current_path,__temp_meta_current_path_724462665__ = __temp_meta_current_path_724462665__,null %}\ +{% enddo %} +{% 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/test.jinja b/src/test/resources/eager/handles-import-in-deferred-if/test.jinja new file mode 100644 index 000000000..5cb4cb5da --- /dev/null +++ b/src/test/resources/eager/handles-import-in-deferred-if/test.jinja @@ -0,0 +1,10 @@ +{% set primary_line_height = 100 %} +{% set secondary_line_height = 200 %} +{% if deferred %} +{% import "../supplements/set-val.jinja" as simple %} +{% else %} +{% import "../supplements/set-val.jinja" %} +{% endif %} +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-in-deferred-if.jinja b/src/test/resources/eager/handles-import-with-macros-in-deferred-if/test.jinja similarity index 50% rename from src/test/resources/eager/handles-import-in-deferred-if.jinja rename to src/test/resources/eager/handles-import-with-macros-in-deferred-if/test.jinja index 4bbe1af62..956af0381 100644 --- a/src/test/resources/eager/handles-import-in-deferred-if.jinja +++ b/src/test/resources/eager/handles-import-with-macros-in-deferred-if/test.jinja @@ -1,8 +1,8 @@ {% set myname = 'person' %} {% if deferred %} -{%- import "macro-and-set.jinja" as simple -%} +{%- import "../supplements/macro-and-set.jinja" as simple -%} {% else %} -{%- import "macro-and-set.jinja" as simple -%} +{%- import "../supplements/macro-and-set.jinja" as simple -%} {% endif %} simple.foo: {{ simple.foo() }} simple.bar: {{ simple.bar }} diff --git a/src/test/resources/eager/handles-loop-var-against-deferred-in-loop.expected.jinja b/src/test/resources/eager/handles-loop-var-against-deferred-in-loop.expected.jinja deleted file mode 100644 index b5d29833f..000000000 --- a/src/test/resources/eager/handles-loop-var-against-deferred-in-loop.expected.jinja +++ /dev/null @@ -1,4 +0,0 @@ -{% for __ignored__ in [0] %}item1 {{ deferred }}.{% set temp = 'item1' ~ deferred %}{{ temp }} -item2 {{ deferred }}.{% set temp = 'item2' ~ deferred %}{{ temp }} -item3 {{ deferred }}.{% set temp = 'item3' ~ deferred %}{{ temp }} -{% endfor %} diff --git a/src/test/resources/eager/handles-loop-var-against-deferred-in-loop.expected.expected.jinja b/src/test/resources/eager/handles-loop-var-against-deferred-in-loop/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-loop-var-against-deferred-in-loop.expected.expected.jinja rename to src/test/resources/eager/handles-loop-var-against-deferred-in-loop/test.expected.expected.jinja diff --git a/src/test/resources/eager/handles-loop-var-against-deferred-in-loop/test.expected.jinja b/src/test/resources/eager/handles-loop-var-against-deferred-in-loop/test.expected.jinja new file mode 100644 index 000000000..79193546c --- /dev/null +++ b/src/test/resources/eager/handles-loop-var-against-deferred-in-loop/test.expected.jinja @@ -0,0 +1,11 @@ +{% for __ignored__ in [0] %}\ +item1 {{ deferred }}\ +.{% set temp = 'item1' ~ deferred %}\ +{{ temp }} +item2 {{ deferred }}\ +.{% set temp = 'item2' ~ deferred %}\ +{{ temp }} +item3 {{ deferred }}\ +.{% set temp = 'item3' ~ deferred %}\ +{{ temp }} +{% endfor %} diff --git a/src/test/resources/eager/handles-loop-var-against-deferred-in-loop.jinja b/src/test/resources/eager/handles-loop-var-against-deferred-in-loop/test.jinja similarity index 100% rename from src/test/resources/eager/handles-loop-var-against-deferred-in-loop.jinja rename to src/test/resources/eager/handles-loop-var-against-deferred-in-loop/test.jinja diff --git a/src/test/resources/eager/handles-modified-include-path/a.jinja b/src/test/resources/eager/handles-modified-include-path/a.jinja new file mode 100644 index 000000000..f110cc07a --- /dev/null +++ b/src/test/resources/eager/handles-modified-include-path/a.jinja @@ -0,0 +1,3 @@ +This is include a +{% if deferred %}{% set include_path = './b.jinja' %}{% endif %} +{% include include_path %} diff --git a/src/test/resources/eager/handles-modified-include-path/b.jinja b/src/test/resources/eager/handles-modified-include-path/b.jinja new file mode 100644 index 000000000..89dd18796 --- /dev/null +++ b/src/test/resources/eager/handles-modified-include-path/b.jinja @@ -0,0 +1 @@ +This is include b \ No newline at end of file diff --git a/src/test/resources/eager/handles-modified-include-path/test.expected.expected.jinja b/src/test/resources/eager/handles-modified-include-path/test.expected.expected.jinja new file mode 100644 index 000000000..41ccc3a6b --- /dev/null +++ b/src/test/resources/eager/handles-modified-include-path/test.expected.expected.jinja @@ -0,0 +1,6 @@ +Before include +This is include a + +This is include b + +After include \ No newline at end of file diff --git a/src/test/resources/eager/handles-modified-include-path/test.expected.jinja b/src/test/resources/eager/handles-modified-include-path/test.expected.jinja new file mode 100644 index 000000000..a42233536 --- /dev/null +++ b/src/test/resources/eager/handles-modified-include-path/test.expected.jinja @@ -0,0 +1,10 @@ +Before include +{% set __temp_meta_current_path_1035152568__,current_path = current_path,'eager/handles-modified-include-path/a.jinja' %}\ +This is include a +{% set include_path = './a.jinja' %}\ +{% if deferred %}\ +{% set include_path = './b.jinja' %}\ +{% endif %} +{% include include_path %} +{% set current_path,__temp_meta_current_path_1035152568__ = __temp_meta_current_path_1035152568__,null %} +After include \ No newline at end of file diff --git a/src/test/resources/eager/handles-modified-include-path/test.jinja b/src/test/resources/eager/handles-modified-include-path/test.jinja new file mode 100644 index 000000000..21ec080fc --- /dev/null +++ b/src/test/resources/eager/handles-modified-include-path/test.jinja @@ -0,0 +1,4 @@ +{% set include_path = './a.jinja' %} +Before include +{% include include_path %} +After include diff --git a/src/test/resources/eager/handles-non-deferred-import-vars.expected.jinja b/src/test/resources/eager/handles-non-deferred-import-vars/test.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-non-deferred-import-vars.expected.jinja rename to src/test/resources/eager/handles-non-deferred-import-vars/test.expected.jinja diff --git a/src/test/resources/eager/handles-non-deferred-import-vars.jinja b/src/test/resources/eager/handles-non-deferred-import-vars/test.jinja similarity index 54% rename from src/test/resources/eager/handles-non-deferred-import-vars.jinja rename to src/test/resources/eager/handles-non-deferred-import-vars/test.jinja index 6d6a99c62..37982ee09 100644 --- a/src/test/resources/eager/handles-non-deferred-import-vars.jinja +++ b/src/test/resources/eager/handles-non-deferred-import-vars/test.jinja @@ -1,9 +1,9 @@ {%- set myname = (1 + 2) -%} -{%- from "macro-and-set.jinja" import foo, bar -%} +{%- from "../supplements/macro-and-set.jinja" import foo, bar -%} foo: {{ foo() }} bar: {{ bar }} --- {% set myname = (3 + 4) -%} -{%- import "macro-and-set.jinja" as simple -%} +{%- import "../supplements/macro-and-set.jinja" as simple -%} simple.foo: {{ simple.foo() }} simple.bar: {{ simple.bar }} diff --git a/src/test/resources/eager/handles-non-deferring-cycles.expected.jinja b/src/test/resources/eager/handles-non-deferring-cycles/test.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-non-deferring-cycles.expected.jinja rename to src/test/resources/eager/handles-non-deferring-cycles/test.expected.jinja diff --git a/src/test/resources/eager/handles-non-deferring-cycles.jinja b/src/test/resources/eager/handles-non-deferring-cycles/test.jinja similarity index 100% rename from src/test/resources/eager/handles-non-deferring-cycles.jinja rename to src/test/resources/eager/handles-non-deferring-cycles/test.jinja diff --git a/src/test/resources/eager/handles-reference-modification-when-source-is-lost/test.expected.jinja b/src/test/resources/eager/handles-reference-modification-when-source-is-lost/test.expected.jinja new file mode 100644 index 000000000..dccbe067b --- /dev/null +++ b/src/test/resources/eager/handles-reference-modification-when-source-is-lost/test.expected.jinja @@ -0,0 +1,19 @@ +{% set a_list = ['a'] %}\ +{% for __ignored__ in [0] %}\ +{% set b_list = a_list %}\ +{% do b_list.append(deferred) %} +{% endfor %} +{{ a_list }} +--- +{% for __ignored__ in [0] %} + +{% set a_list = [] %}\ +{% for __ignored__ in [0] %} +{% if deferred %} +{% set b_list = [] %} +{% 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/test.jinja b/src/test/resources/eager/handles-reference-modification-when-source-is-lost/test.jinja new file mode 100644 index 000000000..53197b6d1 --- /dev/null +++ b/src/test/resources/eager/handles-reference-modification-when-source-is-lost/test.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 %} 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 34d8af234..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 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 }} -{% endif %} -{% do my_var.update({'current_path': current_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/eager/handles-same-name-import-var.jinja b/src/test/resources/eager/handles-same-name-import-var.jinja deleted file mode 100644 index 226c07f02..000000000 --- a/src/test/resources/eager/handles-same-name-import-var.jinja +++ /dev/null @@ -1,4 +0,0 @@ -{% if deferred %} -{% import '../settag/set-var-and-deferred.jinja' as my_var %} -{{ my_var }} -{% endif %} diff --git a/src/test/resources/eager/handles-same-name-import-var/set-var-and-deferred.jinja b/src/test/resources/eager/handles-same-name-import-var/set-var-and-deferred.jinja new file mode 100644 index 000000000..74989f05e --- /dev/null +++ b/src/test/resources/eager/handles-same-name-import-var/set-var-and-deferred.jinja @@ -0,0 +1,6 @@ +{% if deferred %} +{% do %}{% set path = 'eager/handles-same-name-import-var/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': 'eager/handles-same-name-import-var/set-var-and-deferred.jinja','value': value}) %}{% set path = '' %}{% enddo %} +{{ my_var }} +{% endif %} diff --git a/src/test/resources/eager/handles-same-name-import-var/test.expected.expected.jinja b/src/test/resources/eager/handles-same-name-import-var/test.expected.expected.jinja new file mode 100644 index 000000000..1baae9e13 --- /dev/null +++ b/src/test/resources/eager/handles-same-name-import-var/test.expected.expected.jinja @@ -0,0 +1 @@ +[fn:map_entry('import_resource_path', 'eager/handles-same-name-import-var/set-var-and-deferred.jinja'), fn:map_entry('my_var', {'my_var': {'foo': 'bar'} , 'value': 'resolved', 'import_resource_path': 'eager/handles-same-name-import-var/set-var-and-deferred.jinja'} ), fn:map_entry('path', ''), fn:map_entry('value', 'resolved')] \ No newline at end of file diff --git a/src/test/resources/eager/handles-same-name-import-var/test.expected.jinja b/src/test/resources/eager/handles-same-name-import-var/test.expected.jinja new file mode 100644 index 000000000..66cfa99f0 --- /dev/null +++ b/src/test/resources/eager/handles-same-name-import-var/test.expected.jinja @@ -0,0 +1,39 @@ +{% if deferred %} +{% do %}\ +{% set __temp_meta_current_path_944750549__,current_path = current_path,'eager/handles-same-name-import-var/set-var-and-deferred.jinja' %}\ +{% set __temp_meta_import_alias_1059697132__ = {} %}\ +{% for __ignored__ in [0] %}\ +{% if deferred %} +{% do %}\ +{% set path = '' %}\ +{% do __temp_meta_import_alias_1059697132__.update({'path': path}) %}\ +{% set my_var = {'my_var': {'foo': 'bar'} } %}\ +{% do __temp_meta_import_alias_1059697132__.update({'my_var': my_var}) %}\ +{% set path = 'eager/handles-same-name-import-var/set-var-and-deferred.jinja' %}\ +{% do __temp_meta_import_alias_1059697132__.update({'path': path}) %}\ +{% set value = null %}\ +{% do __temp_meta_import_alias_1059697132__.update({'value': value}) %}\ +{% set my_var = {} %}\ +{% do __temp_meta_import_alias_1059697132__.update({'my_var': my_var}) %}\ +{% set my_var = {'foo': 'bar'} %}\ +{% do __temp_meta_import_alias_1059697132__.update({'my_var': my_var}) %}\ +{% set my_var = {'my_var': {'foo': 'bar'} } %}\ +{% do __temp_meta_import_alias_1059697132__.update({'my_var': my_var}) %} +{% set value = deferred %}\ +{% do __temp_meta_import_alias_1059697132__.update({'value': value}) %}\ +{% set my_var = {'my_var': {'foo': 'bar'} } %}\ +{% do __temp_meta_import_alias_1059697132__.update({'my_var': my_var}) %}\ +{% do my_var.update({'value': value}) %} +{% do my_var.update({'import_resource_path': 'eager/handles-same-name-import-var/set-var-and-deferred.jinja', 'value': value}) %}\ +{% set path = '' %}\ +{% do __temp_meta_import_alias_1059697132__.update({'path': path}) %}\ +{% enddo %} +{{ my_var }} +{% endif %} +{% do __temp_meta_import_alias_1059697132__.update({'path': path,'import_resource_path': 'eager/handles-same-name-import-var/set-var-and-deferred.jinja','value': value}) %}\ +{% endfor %}\ +{% set my_var = __temp_meta_import_alias_1059697132__ %}\ +{% set current_path,__temp_meta_current_path_944750549__ = __temp_meta_current_path_944750549__,null %}\ +{% enddo %} +{{ filter:dictsort.filter(my_var, ____int3rpr3t3r____, false, 'key') }} +{% endif %} diff --git a/src/test/resources/eager/handles-same-name-import-var/test.jinja b/src/test/resources/eager/handles-same-name-import-var/test.jinja new file mode 100644 index 000000000..efdfeed46 --- /dev/null +++ b/src/test/resources/eager/handles-same-name-import-var/test.jinja @@ -0,0 +1,4 @@ +{% if deferred %} +{% import './set-var-and-deferred.jinja' as my_var %} +{{ my_var|dictsort(false, 'key') }} +{% endif %} diff --git a/src/test/resources/eager/handles-set-and-modified-in-for.expected.jinja b/src/test/resources/eager/handles-set-and-modified-in-for/test.expected.jinja similarity index 75% rename from src/test/resources/eager/handles-set-and-modified-in-for.expected.jinja rename to src/test/resources/eager/handles-set-and-modified-in-for/test.expected.jinja index 51fd3039f..63d333221 100644 --- a/src/test/resources/eager/handles-set-and-modified-in-for.expected.jinja +++ b/src/test/resources/eager/handles-set-and-modified-in-for/test.expected.jinja @@ -1,4 +1,5 @@ -{% set list = [] %}{% for item in deferred %} +{% set list = [] %}\ +{% for item in deferred %} {% unless count %} {% set count = 0 %} {% endunless %} diff --git a/src/test/resources/eager/handles-set-and-modified-in-for.jinja b/src/test/resources/eager/handles-set-and-modified-in-for/test.jinja similarity index 100% rename from src/test/resources/eager/handles-set-and-modified-in-for.jinja rename to src/test/resources/eager/handles-set-and-modified-in-for/test.jinja diff --git a/src/test/resources/eager/handles-set-in-for.expected.jinja b/src/test/resources/eager/handles-set-in-for/test.expected.jinja similarity index 65% rename from src/test/resources/eager/handles-set-in-for.expected.jinja rename to src/test/resources/eager/handles-set-in-for/test.expected.jinja index 652ae0b77..1278f7a47 100644 --- a/src/test/resources/eager/handles-set-in-for.expected.jinja +++ b/src/test/resources/eager/handles-set-in-for/test.expected.jinja @@ -1,4 +1,5 @@ -{% set list = [] %}{% for item in deferred %} +{% set list = [] %}\ +{% for item in deferred %} {% set count = 0 %} {% do list.append(0) %} {% set count = 1 %} diff --git a/src/test/resources/eager/handles-set-in-for.jinja b/src/test/resources/eager/handles-set-in-for/test.jinja similarity index 100% rename from src/test/resources/eager/handles-set-in-for.jinja rename to src/test/resources/eager/handles-set-in-for/test.jinja diff --git a/src/test/resources/eager/handles-set-in-inner-scope.expected.jinja b/src/test/resources/eager/handles-set-in-inner-scope/test.expected.jinja similarity index 51% rename from src/test/resources/eager/handles-set-in-inner-scope.expected.jinja rename to src/test/resources/eager/handles-set-in-inner-scope/test.expected.jinja index 6ce5d9353..84ead841d 100644 --- a/src/test/resources/eager/handles-set-in-inner-scope.expected.jinja +++ b/src/test/resources/eager/handles-set-in-inner-scope/test.expected.jinja @@ -1,5 +1,5 @@ -{% set foo = 1 %}{% for i in [0] %} +{% for __ignored__ in [0] %} {% set foo = deferred %} {{ foo }} {% endfor %} -{{ foo }} +1 diff --git a/src/test/resources/eager/handles-set-in-inner-scope.jinja b/src/test/resources/eager/handles-set-in-inner-scope/test.jinja similarity index 100% rename from src/test/resources/eager/handles-set-in-inner-scope.jinja rename to src/test/resources/eager/handles-set-in-inner-scope/test.jinja diff --git a/src/test/resources/eager/handles-unknown-function-errors.expected.jinja b/src/test/resources/eager/handles-unknown-function-errors/test.expected.jinja similarity index 100% rename from src/test/resources/eager/handles-unknown-function-errors.expected.jinja rename to src/test/resources/eager/handles-unknown-function-errors/test.expected.jinja diff --git a/src/test/resources/eager/handles-unknown-function-errors.jinja b/src/test/resources/eager/handles-unknown-function-errors/test.jinja similarity index 100% rename from src/test/resources/eager/handles-unknown-function-errors.jinja rename to src/test/resources/eager/handles-unknown-function-errors/test.jinja diff --git a/src/test/resources/eager/handles-value-modified-in-macro.expected.jinja b/src/test/resources/eager/handles-value-modified-in-macro/test.expected.jinja similarity index 63% rename from src/test/resources/eager/handles-value-modified-in-macro.expected.jinja rename to src/test/resources/eager/handles-value-modified-in-macro/test.expected.jinja index 09057c16f..ed3cd4b73 100644 --- a/src/test/resources/eager/handles-value-modified-in-macro.expected.jinja +++ b/src/test/resources/eager/handles-value-modified-in-macro/test.expected.jinja @@ -1,15 +1,17 @@ {% macro counter(foo) %} {% set level = level + 2 %} {% if level < foo %} -{{ counter() }} +{{ counter(foo) }} {% endif %} {{ level }} -{% endmacro %}{{ counter(deferred) }} +{% endmacro %}\ +{{ counter(deferred) }} {% macro counter(foo) %} {% set level = level + 2 %} {% if level < foo %} -{{ counter() }} +{{ counter(foo) }} {% endif %} {{ level }} -{% endmacro %}{{ counter(2) }} +{% endmacro %}\ +{{ counter(2) }} diff --git a/src/test/resources/eager/handles-value-modified-in-macro.jinja b/src/test/resources/eager/handles-value-modified-in-macro/test.jinja similarity index 90% rename from src/test/resources/eager/handles-value-modified-in-macro.jinja rename to src/test/resources/eager/handles-value-modified-in-macro/test.jinja index 9ee613a99..c4dcbb32d 100644 --- a/src/test/resources/eager/handles-value-modified-in-macro.jinja +++ b/src/test/resources/eager/handles-value-modified-in-macro/test.jinja @@ -2,7 +2,7 @@ {% macro counter(foo) %} {% set level = level + increment %} {% if level < foo %} -{{ counter() }} +{{ counter(foo) }} {% endif %} {{ level }} {% endmacro %} diff --git a/src/test/resources/eager/has-proper-line-stripping.expected.jinja b/src/test/resources/eager/has-proper-line-stripping.expected.jinja deleted file mode 100644 index a56e56254..000000000 --- a/src/test/resources/eager/has-proper-line-stripping.expected.jinja +++ /dev/null @@ -1,3 +0,0 @@ -1 -2 -3{% if deferred > 0 %}{{ deferred }}{% elif deferred == 0 %}null{% else %}{{ deferred }}{% endif %} diff --git a/src/test/resources/eager/has-proper-line-stripping/test.expected.jinja b/src/test/resources/eager/has-proper-line-stripping/test.expected.jinja new file mode 100644 index 000000000..fe4d2dad2 --- /dev/null +++ b/src/test/resources/eager/has-proper-line-stripping/test.expected.jinja @@ -0,0 +1,8 @@ +1 +2 +3{% if deferred > 0 %}\ +{{ deferred }}\ +{% elif deferred == 0 %}\ +null{% else %}\ +{{ deferred }}\ +{% endif %} diff --git a/src/test/resources/eager/has-proper-line-stripping.jinja b/src/test/resources/eager/has-proper-line-stripping/test.jinja similarity index 100% rename from src/test/resources/eager/has-proper-line-stripping.jinja rename to src/test/resources/eager/has-proper-line-stripping/test.jinja diff --git a/src/test/resources/eager/keeps-macro-modifications-in-scope/test.expected.expected.jinja b/src/test/resources/eager/keeps-macro-modifications-in-scope/test.expected.expected.jinja new file mode 100644 index 000000000..23d81650a --- /dev/null +++ b/src/test/resources/eager/keeps-macro-modifications-in-scope/test.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/test.expected.jinja b/src/test/resources/eager/keeps-macro-modifications-in-scope/test.expected.jinja new file mode 100644 index 000000000..98ddd3fac --- /dev/null +++ b/src/test/resources/eager/keeps-macro-modifications-in-scope/test.expected.jinja @@ -0,0 +1,35 @@ +{% set list = [] %}\ +{% if deferred %} + +{% set __macro_inc_100372882_temp_variable_0__ %}\ +{% do list.append(1) %}\ +1{% set depth = 2 %} +{% set __macro_inc_100372882_temp_variable_1__ %}\ +{% do list.append(2) %}\ +2{% set depth = 3 %} +{% set __macro_inc_100372882_temp_variable_2__ %}\ +{% do list.append(3) %}\ +3{% endset %}\ +{{ __macro_inc_100372882_temp_variable_2__ }} +{% set __macro_inc_100372882_temp_variable_3__ %}\ +{% do list.append(3) %}\ +3{% endset %}\ +{{ __macro_inc_100372882_temp_variable_3__ }}\ +{% endset %}\ +{{ __macro_inc_100372882_temp_variable_1__ }} +{% set __macro_inc_100372882_temp_variable_4__ %}\ +{% do list.append(2) %}\ +2{% set depth = 3 %} +{% set __macro_inc_100372882_temp_variable_5__ %}\ +{% do list.append(3) %}\ +3{% endset %}\ +{{ __macro_inc_100372882_temp_variable_5__ }} +{% set __macro_inc_100372882_temp_variable_6__ %}\ +{% do list.append(3) %}\ +3{% endset %}\ +{{ __macro_inc_100372882_temp_variable_6__ }}\ +{% endset %}\ +{{ __macro_inc_100372882_temp_variable_4__ }}\ +{% endset %}\ +{{ __macro_inc_100372882_temp_variable_0__ }} +{% endif %} diff --git a/src/test/resources/eager/keeps-macro-modifications-in-scope/test.jinja b/src/test/resources/eager/keeps-macro-modifications-in-scope/test.jinja new file mode 100644 index 000000000..89423a419 --- /dev/null +++ b/src/test/resources/eager/keeps-macro-modifications-in-scope/test.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 %} + diff --git a/src/test/resources/eager/keeps-max-macro-recursion-depth.expected.jinja b/src/test/resources/eager/keeps-max-macro-recursion-depth/test.expected.jinja similarity index 100% rename from src/test/resources/eager/keeps-max-macro-recursion-depth.expected.jinja rename to src/test/resources/eager/keeps-max-macro-recursion-depth/test.expected.jinja diff --git a/src/test/resources/eager/keeps-max-macro-recursion-depth.jinja b/src/test/resources/eager/keeps-max-macro-recursion-depth/test.jinja similarity index 100% rename from src/test/resources/eager/keeps-max-macro-recursion-depth.jinja rename to src/test/resources/eager/keeps-max-macro-recursion-depth/test.jinja diff --git a/src/test/resources/eager/keeps-meta-context-variables-through-import/import-target.jinja b/src/test/resources/eager/keeps-meta-context-variables-through-import/import-target.jinja new file mode 100644 index 000000000..707978124 --- /dev/null +++ b/src/test/resources/eager/keeps-meta-context-variables-through-import/import-target.jinja @@ -0,0 +1 @@ +I am a boring import \ No newline at end of file diff --git a/src/test/resources/eager/keeps-meta-context-variables-through-import/test.expected.jinja b/src/test/resources/eager/keeps-meta-context-variables-through-import/test.expected.jinja new file mode 100644 index 000000000..d44133a16 --- /dev/null +++ b/src/test/resources/eager/keeps-meta-context-variables-through-import/test.expected.jinja @@ -0,0 +1,5 @@ +{% set list = deferred %} + +{% set meta = ['overridden'] %}\ +{% do list.append(meta) %} +{{ list }} \ No newline at end of file diff --git a/src/test/resources/eager/keeps-meta-context-variables-through-import/test.jinja b/src/test/resources/eager/keeps-meta-context-variables-through-import/test.jinja new file mode 100644 index 000000000..5b1adc00c --- /dev/null +++ b/src/test/resources/eager/keeps-meta-context-variables-through-import/test.jinja @@ -0,0 +1,5 @@ +{% set meta = ['overridden'] %} +{% set list = deferred %} +{% import '../../eager/keeps-meta-context-variables-through-import/import-target.jinja' %} +{% do list.append(meta) %} +{{ list }} \ No newline at end of file diff --git a/src/test/resources/eager/keeps-scope-isolation-from-for-loops.expected.jinja b/src/test/resources/eager/keeps-scope-isolation-from-for-loops/test.expected.jinja similarity index 100% rename from src/test/resources/eager/keeps-scope-isolation-from-for-loops.expected.jinja rename to src/test/resources/eager/keeps-scope-isolation-from-for-loops/test.expected.jinja diff --git a/src/test/resources/eager/keeps-scope-isolation-from-for-loops.jinja b/src/test/resources/eager/keeps-scope-isolation-from-for-loops/test.jinja similarity index 100% rename from src/test/resources/eager/keeps-scope-isolation-from-for-loops.jinja rename to src/test/resources/eager/keeps-scope-isolation-from-for-loops/test.jinja diff --git a/src/test/resources/eager/loads-imported-macro-syntax.expected.jinja b/src/test/resources/eager/loads-imported-macro-syntax/test.expected.jinja similarity index 100% rename from src/test/resources/eager/loads-imported-macro-syntax.expected.jinja rename to src/test/resources/eager/loads-imported-macro-syntax/test.expected.jinja diff --git a/src/test/resources/eager/loads-imported-macro-syntax.jinja b/src/test/resources/eager/loads-imported-macro-syntax/test.jinja similarity index 100% rename from src/test/resources/eager/loads-imported-macro-syntax.jinja rename to src/test/resources/eager/loads-imported-macro-syntax/test.jinja diff --git a/src/test/resources/eager/modifies-variable-in-deferred-macro.expected.jinja b/src/test/resources/eager/modifies-variable-in-deferred-macro/test.expected.jinja similarity index 51% rename from src/test/resources/eager/modifies-variable-in-deferred-macro.expected.jinja rename to src/test/resources/eager/modifies-variable-in-deferred-macro/test.expected.jinja index 384c52ede..431eba66b 100644 --- a/src/test/resources/eager/modifies-variable-in-deferred-macro.expected.jinja +++ b/src/test/resources/eager/modifies-variable-in-deferred-macro/test.expected.jinja @@ -1,14 +1,20 @@ {% macro my_macro(list, other) %} {% do list.append(other) %} -{% endmacro %}{% set list = [] %}{% do my_macro(list, deferred) %} +{% endmacro %}\ +{% set list = [] %}\ +{% do my_macro(list, deferred) %} {{ list }} {% macro my_macro(list, other) %} {% do list.append(other) %} -{% endmacro %}{% set var = [] %}{% do my_macro(var, deferred) %} +{% endmacro %}\ +{% set var = [] %}\ +{% do my_macro(var, deferred) %} {{ var }} {% macro my_macro(list, other) %} {% do list.append(other) %} -{% endmacro %}{% set var2 = [] %}{% do my_macro(var2, 1) %} +{% endmacro %}\ +{% set var2 = [] %}\ +{% do my_macro(var2, 1) %} {{ var2 }} diff --git a/src/test/resources/eager/modifies-variable-in-deferred-macro.jinja b/src/test/resources/eager/modifies-variable-in-deferred-macro/test.jinja similarity index 100% rename from src/test/resources/eager/modifies-variable-in-deferred-macro.jinja rename to src/test/resources/eager/modifies-variable-in-deferred-macro/test.jinja diff --git a/src/test/resources/eager/only-defers-loop-item-on-current-context/test.expected.jinja b/src/test/resources/eager/only-defers-loop-item-on-current-context/test.expected.jinja new file mode 100644 index 000000000..760c22ba2 --- /dev/null +++ b/src/test/resources/eager/only-defers-loop-item-on-current-context/test.expected.jinja @@ -0,0 +1,15 @@ +{% 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/test.jinja b/src/test/resources/eager/only-defers-loop-item-on-current-context/test.jinja new file mode 100644 index 000000000..3bc01a0a5 --- /dev/null +++ b/src/test/resources/eager/only-defers-loop-item-on-current-context/test.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 diff --git a/src/test/resources/eager/partially-resolves-eager-set/test.jinja b/src/test/resources/eager/partially-resolves-eager-set/test.jinja new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/eager/prepends-set-if-state-changes.expected.jinja b/src/test/resources/eager/prepends-set-if-state-changes/test.expected.jinja similarity index 69% rename from src/test/resources/eager/prepends-set-if-state-changes.expected.jinja rename to src/test/resources/eager/prepends-set-if-state-changes/test.expected.jinja index c3f4122f1..102f5477a 100644 --- a/src/test/resources/eager/prepends-set-if-state-changes.expected.jinja +++ b/src/test/resources/eager/prepends-set-if-state-changes/test.expected.jinja @@ -1,4 +1,5 @@ -{% set foo = [0] %}{% set deferred = deferred && foo.append(1) %} +{% set foo = [0] %}\ +{% set deferred = deferred && foo.append(1) %} {% do foo.append(2) %} {% set deferred = deferred && foo.append(3) %} {% print foo.append(4) %} diff --git a/src/test/resources/eager/prepends-set-if-state-changes.jinja b/src/test/resources/eager/prepends-set-if-state-changes/test.jinja similarity index 100% rename from src/test/resources/eager/prepends-set-if-state-changes.jinja rename to src/test/resources/eager/prepends-set-if-state-changes/test.jinja diff --git a/src/test/resources/eager/preserves-blocks-for-reconstruction-order/base.jinja b/src/test/resources/eager/preserves-blocks-for-reconstruction-order/base.jinja new file mode 100644 index 000000000..438fedb55 --- /dev/null +++ b/src/test/resources/eager/preserves-blocks-for-reconstruction-order/base.jinja @@ -0,0 +1,13 @@ +{% if deferred %} +{% set deferred_list = [] %} +{% endif %} +{% do deferred_list.append('Before block') %} +Deferred list after block is: {{ deferred_list }} +Deferred list after block should be: ['Before block'] +-----Pre-First----- +{% block first -%} +{%- endblock %} +-----Post-First----- +{% do deferred_list.append('After block') %} +Deferred list after block is: {{ deferred_list }} +Deferred list after block should be: ['Before block', 'After block'] diff --git a/src/test/resources/eager/preserves-blocks-for-reconstruction-order/test.expected.expected.jinja b/src/test/resources/eager/preserves-blocks-for-reconstruction-order/test.expected.expected.jinja new file mode 100644 index 000000000..82ee6830e --- /dev/null +++ b/src/test/resources/eager/preserves-blocks-for-reconstruction-order/test.expected.expected.jinja @@ -0,0 +1,12 @@ +Deferred list after block is: ['Before block'] +Deferred list after block should be: ['Before block'] +-----Pre-First----- + + +Deferred list inside of block is: ['Before block', 'After block', 'In child block'] +Deferred list inside of block should be: ['Before block', 'After block', 'In child block'] + +-----Post-First----- + +Deferred list after block is: ['Before block', 'After block'] +Deferred list after block should be: ['Before block', 'After block'] \ No newline at end of file diff --git a/src/test/resources/eager/preserves-blocks-for-reconstruction-order/test.expected.jinja b/src/test/resources/eager/preserves-blocks-for-reconstruction-order/test.expected.jinja new file mode 100644 index 000000000..7eccab5cf --- /dev/null +++ b/src/test/resources/eager/preserves-blocks-for-reconstruction-order/test.expected.jinja @@ -0,0 +1,19 @@ +{% set current_path = 'eager/preserves-blocks-for-reconstruction-order/base.jinja' %}\ +{% if deferred %} +{% set deferred_list = [] %} +{% endif %} +{% do deferred_list.append('Before block') %} +Deferred list after block is: {{ deferred_list }} +Deferred list after block should be: ['Before block'] +-----Pre-First----- +{% block first %}\ +{% set __temp_meta_current_path_1012932725__,current_path = current_path,'eager/preserves-blocks-for-reconstruction-order/test.jinja' %} +{% do deferred_list.append('In child block') %} +Deferred list inside of block is: {{ deferred_list }} +Deferred list inside of block should be: ['Before block', 'After block', 'In child block'] +{% set current_path,__temp_meta_current_path_1012932725__ = __temp_meta_current_path_1012932725__,null %}\ +{% endblock first %} +-----Post-First----- +{% do deferred_list.append('After block') %} +Deferred list after block is: {{ deferred_list }} +Deferred list after block should be: ['Before block', 'After block'] \ No newline at end of file diff --git a/src/test/resources/eager/preserves-blocks-for-reconstruction-order/test.jinja b/src/test/resources/eager/preserves-blocks-for-reconstruction-order/test.jinja new file mode 100644 index 000000000..40cfc0105 --- /dev/null +++ b/src/test/resources/eager/preserves-blocks-for-reconstruction-order/test.jinja @@ -0,0 +1,7 @@ +{% extends '../../eager/preserves-blocks-for-reconstruction-order/base.jinja' %} + +{% block first %} +{% do deferred_list.append('In child block') %} +Deferred list inside of block is: {{ deferred_list }} +Deferred list inside of block should be: ['Before block', 'After block', 'In child block'] +{% endblock %} diff --git a/src/test/resources/eager/preserves-raw-inside-deferred-set-block/test.expected.jinja b/src/test/resources/eager/preserves-raw-inside-deferred-set-block/test.expected.jinja new file mode 100644 index 000000000..eb79d845d --- /dev/null +++ b/src/test/resources/eager/preserves-raw-inside-deferred-set-block/test.expected.jinja @@ -0,0 +1,10 @@ +{% set foo %} +{% if deferred %} +{% raw %}\ +{{ 'fire' }}\ +{% endraw %} +{% else %} +water +{% endif %} +{% endset %} +{% print foo %} diff --git a/src/test/resources/eager/preserves-raw-inside-deferred-set-block/test.jinja b/src/test/resources/eager/preserves-raw-inside-deferred-set-block/test.jinja new file mode 100644 index 000000000..df246bfc0 --- /dev/null +++ b/src/test/resources/eager/preserves-raw-inside-deferred-set-block/test.jinja @@ -0,0 +1,8 @@ +{% set foo %} +{% if deferred %} +{% raw %}{{ 'fire' }}{% endraw %} +{% else %} +{{ 'water' }} +{% endif %} +{% endset %} +{% print foo %} diff --git a/src/test/resources/eager/preserves-value-set-in-if.jinja b/src/test/resources/eager/preserves-value-set-in-if/test.expected.jinja similarity index 56% rename from src/test/resources/eager/preserves-value-set-in-if.jinja rename to src/test/resources/eager/preserves-value-set-in-if/test.expected.jinja index e34006f5a..b59f49423 100644 --- a/src/test/resources/eager/preserves-value-set-in-if.jinja +++ b/src/test/resources/eager/preserves-value-set-in-if/test.expected.jinja @@ -1,5 +1,6 @@ 2 -{% set foo = 2 %}{% if deferred %} +{% set foo = 2 %}\ +{% if deferred %} {% set foo = deferred %} {% endif %} {{ foo }} diff --git a/src/test/resources/eager/preserves-value-set-in-if.expected.jinja b/src/test/resources/eager/preserves-value-set-in-if/test.jinja similarity index 100% rename from src/test/resources/eager/preserves-value-set-in-if.expected.jinja rename to src/test/resources/eager/preserves-value-set-in-if/test.jinja 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 deleted file mode 100644 index 720db4c8c..000000000 --- a/src/test/resources/eager/puts-deferred-fromed-macro-in-output.expected.jinja +++ /dev/null @@ -1 +0,0 @@ -{% 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() %} diff --git a/src/test/resources/eager/puts-deferred-fromed-macro-in-output.jinja b/src/test/resources/eager/puts-deferred-fromed-macro-in-output.jinja deleted file mode 100644 index 9f40335c0..000000000 --- a/src/test/resources/eager/puts-deferred-fromed-macro-in-output.jinja +++ /dev/null @@ -1,3 +0,0 @@ -{%- from "simple-with-call.jinja" import getPath -%} -{%- set myname = deferred + (1 + 2) -%} -{% print getPath() %} diff --git a/src/test/resources/eager/puts-deferred-fromed-macro-in-output/test.expected.jinja b/src/test/resources/eager/puts-deferred-fromed-macro-in-output/test.expected.jinja new file mode 100644 index 000000000..571f118c8 --- /dev/null +++ b/src/test/resources/eager/puts-deferred-fromed-macro-in-output/test.expected.jinja @@ -0,0 +1,5 @@ +{% set myname = deferred + 3 %}\ +{% set __macro_getPath_1519775617_temp_variable_1__ %}\ +Hello {{ myname }}\ +{% endset %}\ +{% print __macro_getPath_1519775617_temp_variable_1__ %} diff --git a/src/test/resources/eager/puts-deferred-fromed-macro-in-output/test.jinja b/src/test/resources/eager/puts-deferred-fromed-macro-in-output/test.jinja new file mode 100644 index 000000000..4f01f9dbf --- /dev/null +++ b/src/test/resources/eager/puts-deferred-fromed-macro-in-output/test.jinja @@ -0,0 +1,3 @@ +{%- from "../supplements/simple-with-call.jinja" import getPath -%} +{%- set myname = deferred + (1 + 2) -%} +{% print getPath() %} diff --git a/src/test/resources/eager/puts-deferred-imported-macro-in-output.expected.jinja b/src/test/resources/eager/puts-deferred-imported-macro-in-output.expected.jinja deleted file mode 100644 index f95022d16..000000000 --- a/src/test/resources/eager/puts-deferred-imported-macro-in-output.expected.jinja +++ /dev/null @@ -1 +0,0 @@ -{% set myname = deferred + 3 %}{% set deferred_import_resource_path = 'simple-with-call.jinja' %}{% macro simple.getPath() %}Hello {{ myname }}{% endmacro %}{% set deferred_import_resource_path = null %}{% print simple.getPath() %} diff --git a/src/test/resources/eager/puts-deferred-imported-macro-in-output.expected.expected.jinja b/src/test/resources/eager/puts-deferred-imported-macro-in-output/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/puts-deferred-imported-macro-in-output.expected.expected.jinja rename to src/test/resources/eager/puts-deferred-imported-macro-in-output/test.expected.expected.jinja diff --git a/src/test/resources/eager/puts-deferred-imported-macro-in-output/test.expected.jinja b/src/test/resources/eager/puts-deferred-imported-macro-in-output/test.expected.jinja new file mode 100644 index 000000000..571f118c8 --- /dev/null +++ b/src/test/resources/eager/puts-deferred-imported-macro-in-output/test.expected.jinja @@ -0,0 +1,5 @@ +{% set myname = deferred + 3 %}\ +{% set __macro_getPath_1519775617_temp_variable_1__ %}\ +Hello {{ myname }}\ +{% endset %}\ +{% print __macro_getPath_1519775617_temp_variable_1__ %} diff --git a/src/test/resources/eager/puts-deferred-imported-macro-in-output.jinja b/src/test/resources/eager/puts-deferred-imported-macro-in-output/test.jinja similarity index 51% rename from src/test/resources/eager/puts-deferred-imported-macro-in-output.jinja rename to src/test/resources/eager/puts-deferred-imported-macro-in-output/test.jinja index d3bd9e447..63415185d 100644 --- a/src/test/resources/eager/puts-deferred-imported-macro-in-output.jinja +++ b/src/test/resources/eager/puts-deferred-imported-macro-in-output/test.jinja @@ -1,3 +1,3 @@ -{%- import "simple-with-call.jinja" as simple -%} +{%- import "../supplements/simple-with-call.jinja" as simple -%} {%- set myname = deferred + (1 + 2) -%} {% print simple.getPath() %} diff --git a/src/test/resources/eager/reconstructs-aliased-macro/takes-param.jinja b/src/test/resources/eager/reconstructs-aliased-macro/takes-param.jinja new file mode 100644 index 000000000..b77c4ee4c --- /dev/null +++ b/src/test/resources/eager/reconstructs-aliased-macro/takes-param.jinja @@ -0,0 +1,5 @@ +{% macro takes_param(foo) %} +{% print foo %} +{% endmacro %} + +{% set bar = 'bar' %} \ No newline at end of file diff --git a/src/test/resources/eager/reconstructs-aliased-macro/test.expected.expected.jinja b/src/test/resources/eager/reconstructs-aliased-macro/test.expected.expected.jinja new file mode 100644 index 000000000..4c218176d --- /dev/null +++ b/src/test/resources/eager/reconstructs-aliased-macro/test.expected.expected.jinja @@ -0,0 +1 @@ +resolved3 diff --git a/src/test/resources/eager/reconstructs-aliased-macro/test.expected.jinja b/src/test/resources/eager/reconstructs-aliased-macro/test.expected.jinja new file mode 100644 index 000000000..43d9267c9 --- /dev/null +++ b/src/test/resources/eager/reconstructs-aliased-macro/test.expected.jinja @@ -0,0 +1,9 @@ +{% set myname = deferred + 3 %}\ +{% set deferred_import_resource_path = 'eager/reconstructs-aliased-macro/takes-param.jinja' %}\ +{% macro macros.takes_param(foo) %}\ +{% set bar = 'bar' %} +{% print foo %} +{% endmacro %}\ +{% set deferred_import_resource_path = null %}\ +{% set answer = macros.takes_param(myname) %} +{{ answer }} diff --git a/src/test/resources/eager/reconstructs-aliased-macro/test.jinja b/src/test/resources/eager/reconstructs-aliased-macro/test.jinja new file mode 100644 index 000000000..a1326a645 --- /dev/null +++ b/src/test/resources/eager/reconstructs-aliased-macro/test.jinja @@ -0,0 +1,4 @@ +{%- import "./takes-param.jinja" as macros -%} +{%- set myname = deferred + (1 + 2) -%} +{% set answer = macros.takes_param(myname) %} +{{ answer }} diff --git a/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/base.jinja b/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/base.jinja new file mode 100644 index 000000000..4514f4c22 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/base.jinja @@ -0,0 +1,11 @@ +{% set prefix = deferred ? "current" : "current" %} +Parent's current path is: {{ '{{' + prefix + '_path }}' }} +-----Pre-First----- +{% block first -%} +{%- endblock %} +-----Post-First----- +-----Pre-Second----- +{% block second -%} +{%- endblock %} +-----Post-Second----- +Parent's current path is: {{ '{{' + prefix + '_path }}' }} diff --git a/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/middle.jinja b/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/middle.jinja new file mode 100644 index 000000000..791bbc101 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/middle.jinja @@ -0,0 +1,10 @@ +{% extends '../../eager/reconstructs-block-path-when-deferred-nested/base.jinja' %} +{% block first %} +{%- set prefix = deferred ? "current" : "current" -%} +Middle's first current path is: {{ '{{' + prefix + '_path }}' }} +{% endblock %} + +{% block second %} +{%- set prefix = deferred ? "current" : "current" -%} +Middle's second current path is: {{ '{{' + prefix + '_path }}' }} +{% endblock %} \ No newline at end of file diff --git a/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/test.expected.expected.jinja b/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/test.expected.expected.jinja new file mode 100644 index 000000000..ba93542f5 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/test.expected.expected.jinja @@ -0,0 +1,10 @@ +Parent's current path is: eager/reconstructs-block-path-when-deferred-nested/base.jinja +-----Pre-First----- +Child's first current path is: eager/reconstructs-block-path-when-deferred-nested/test.jinja + +-----Post-First----- +-----Pre-Second----- +Middle's second current path is: eager/reconstructs-block-path-when-deferred-nested/middle.jinja + +-----Post-Second----- +Parent's current path is: eager/reconstructs-block-path-when-deferred-nested/base.jinja \ No newline at end of file diff --git a/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/test.expected.jinja b/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/test.expected.jinja new file mode 100644 index 000000000..b81a5e8a6 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/test.expected.jinja @@ -0,0 +1,24 @@ +{% set current_path = 'eager/reconstructs-block-path-when-deferred-nested/base.jinja' %}\ +{% set prefix = deferred ? 'current' : 'current' %} +Parent's current path is: {{ '{{' + prefix + '_path }}\ +' }} +-----Pre-First----- +{% block first %}\ +{% set __temp_meta_current_path_389897147__,current_path = current_path,'eager/reconstructs-block-path-when-deferred-nested/test.jinja' %}\ +{% set prefix = deferred ? 'current' : 'current' %}\ +Child's first current path is: {{ '{{' + prefix + '_path }}\ +' }} +{% set current_path,__temp_meta_current_path_389897147__ = __temp_meta_current_path_389897147__,null %}\ +{% endblock first %} +-----Post-First----- +-----Pre-Second----- +{% block second %}\ +{% set __temp_meta_current_path_198396781__,current_path = current_path,'eager/reconstructs-block-path-when-deferred-nested/middle.jinja' %}\ +{% set prefix = deferred ? 'current' : 'current' %}\ +Middle's second current path is: {{ '{{' + prefix + '_path }}\ +' }} +{% set current_path,__temp_meta_current_path_198396781__ = __temp_meta_current_path_198396781__,null %}\ +{% endblock second %} +-----Post-Second----- +Parent's current path is: {{ '{{' + prefix + '_path }}\ +' }} \ No newline at end of file diff --git a/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/test.jinja b/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/test.jinja new file mode 100644 index 000000000..882a8cb22 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-path-when-deferred-nested/test.jinja @@ -0,0 +1,5 @@ +{% extends '../../eager/reconstructs-block-path-when-deferred-nested/middle.jinja' %} +{% block first %} +{%- set prefix = deferred ? "current" : "current" -%} +Child's first current path is: {{ '{{' + prefix + '_path }}' }} +{% endblock %} diff --git a/src/test/resources/eager/reconstructs-block-path-when-deferred/base.jinja b/src/test/resources/eager/reconstructs-block-path-when-deferred/base.jinja new file mode 100644 index 000000000..82268f242 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-path-when-deferred/base.jinja @@ -0,0 +1,7 @@ +{% set prefix = deferred ? "current" : "current" %} +Parent's current path is: {{ '{{' + prefix + '_path }}' }} +-----Pre-Block----- +{% block body -%} +{%- endblock %} +-----Post-Block----- +Parent's current path is: {{ '{{' + prefix + '_path }}' }} diff --git a/src/test/resources/eager/reconstructs-block-path-when-deferred/test.expected.expected.jinja b/src/test/resources/eager/reconstructs-block-path-when-deferred/test.expected.expected.jinja new file mode 100644 index 000000000..8cd850b99 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-path-when-deferred/test.expected.expected.jinja @@ -0,0 +1,6 @@ +Parent's current path is: eager/reconstructs-block-path-when-deferred/base.jinja +-----Pre-Block----- +Block's current path is: eager/reconstructs-block-path-when-deferred/test.jinja + +-----Post-Block----- +Parent's current path is: eager/reconstructs-block-path-when-deferred/base.jinja diff --git a/src/test/resources/eager/reconstructs-block-path-when-deferred/test.expected.jinja b/src/test/resources/eager/reconstructs-block-path-when-deferred/test.expected.jinja new file mode 100644 index 000000000..6c3a8e954 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-path-when-deferred/test.expected.jinja @@ -0,0 +1,15 @@ +{% set current_path = 'eager/reconstructs-block-path-when-deferred/base.jinja' %}\ +{% set prefix = deferred ? 'current' : 'current' %} +Parent's current path is: {{ '{{' + prefix + '_path }}\ +' }} +-----Pre-Block----- +{% block body %}\ +{% set __temp_meta_current_path_329664044__,current_path = current_path,'eager/reconstructs-block-path-when-deferred/test.jinja' %}\ +{% set prefix = deferred ? 'current' : 'current' %}\ +Block's current path is: {{ '{{' + prefix + '_path }}\ +' }} +{% set current_path,__temp_meta_current_path_329664044__ = __temp_meta_current_path_329664044__,null %}\ +{% endblock body %} +-----Post-Block----- +Parent's current path is: {{ '{{' + prefix + '_path }}\ +' }} \ No newline at end of file diff --git a/src/test/resources/eager/reconstructs-block-path-when-deferred/test.jinja b/src/test/resources/eager/reconstructs-block-path-when-deferred/test.jinja new file mode 100644 index 000000000..7280f3165 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-path-when-deferred/test.jinja @@ -0,0 +1,5 @@ +{% extends '../../eager/reconstructs-block-path-when-deferred/base.jinja' %} +{% block body %} +{%- set prefix = deferred ? "current" : "current" -%} +Block's current path is: {{ '{{' + prefix + '_path }}' }} +{% endblock %} diff --git a/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop/test.expected.jinja b/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop/test.expected.jinja new file mode 100644 index 000000000..d0152a7f9 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop/test.expected.jinja @@ -0,0 +1,6 @@ +{% for i in range(deferred) %} +{% set __macro_foo_97643642_temp_variable_0__ %} +{{ deferred }} +{% endset %}\ +{{ filter:int.filter(__macro_foo_97643642_temp_variable_0__, ____int3rpr3t3r____) + 3 }} +{% endfor %} diff --git a/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop/test.jinja b/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop/test.jinja new file mode 100644 index 000000000..443054813 --- /dev/null +++ b/src/test/resources/eager/reconstructs-block-set-variables-in-for-loop/test.jinja @@ -0,0 +1,6 @@ +{% for i in range(deferred) %} +{%- macro foo() %} +{{ deferred }} +{% endmacro %} +{{ foo()|int + 3 }} +{% endfor %} diff --git a/src/test/resources/eager/reconstructs-deferred-variable-eventually/test.expected.jinja b/src/test/resources/eager/reconstructs-deferred-variable-eventually/test.expected.jinja new file mode 100644 index 000000000..cf2e3900d --- /dev/null +++ b/src/test/resources/eager/reconstructs-deferred-variable-eventually/test.expected.jinja @@ -0,0 +1,17 @@ +{% set my_list = [] %}\ +{% set __macro_append_stuff_153654787_temp_variable_0__ %} +{% if deferred %} + +{% set __macro_foo_97643642_temp_variable_0__ %} +{% do my_list.append('b') %} +{% endset %}\ +{{ __macro_foo_97643642_temp_variable_0__ }} +{% set __macro_foo_97643642_temp_variable_1__ %} +{% do my_list.append('c') %} +{% endset %}\ +{{ __macro_foo_97643642_temp_variable_1__ }} +{% endif %} +{% endset %}\ +{{ __macro_append_stuff_153654787_temp_variable_0__ }} + +{{ my_list }} diff --git a/src/test/resources/eager/reconstructs-deferred-variable-eventually/test.jinja b/src/test/resources/eager/reconstructs-deferred-variable-eventually/test.jinja new file mode 100644 index 000000000..8379c8fd4 --- /dev/null +++ b/src/test/resources/eager/reconstructs-deferred-variable-eventually/test.jinja @@ -0,0 +1,16 @@ +{% macro foo(var) %} +{% do my_list.append(var) %} +{% endmacro %} + +{% macro append_stuff() %} +{% if deferred %} + +{{ foo('b') }} +{{ foo('c') }} +{% endif %} +{% endmacro %} + +{% set my_list = [] %} +{{ append_stuff() }} + +{{ my_list }} \ No newline at end of file diff --git a/src/test/resources/eager/reconstructs-fromed-macro/has-macro.jinja b/src/test/resources/eager/reconstructs-fromed-macro/has-macro.jinja new file mode 100644 index 000000000..f08325779 --- /dev/null +++ b/src/test/resources/eager/reconstructs-fromed-macro/has-macro.jinja @@ -0,0 +1,3 @@ +{% macro upper(param) %} + {{ param|upper }} +{% endmacro %} \ No newline at end of file diff --git a/src/test/resources/eager/reconstructs-fromed-macro/test.expected.jinja b/src/test/resources/eager/reconstructs-fromed-macro/test.expected.jinja new file mode 100644 index 000000000..c7213b008 --- /dev/null +++ b/src/test/resources/eager/reconstructs-fromed-macro/test.expected.jinja @@ -0,0 +1,6 @@ +{% set deferred_import_resource_path = 'eager/reconstructs-fromed-macro/has-macro.jinja' %}\ +{% macro to_upper(param) %} + {{ filter:upper.filter(param, ____int3rpr3t3r____) }} +{% endmacro %}\ +{% set deferred_import_resource_path = null %}\ +{{ to_upper(deferred) }} \ No newline at end of file diff --git a/src/test/resources/eager/reconstructs-fromed-macro/test.jinja b/src/test/resources/eager/reconstructs-fromed-macro/test.jinja new file mode 100644 index 000000000..6c934e61a --- /dev/null +++ b/src/test/resources/eager/reconstructs-fromed-macro/test.jinja @@ -0,0 +1,3 @@ +{% from './has-macro.jinja' import upper as to_upper %} + +{{ to_upper(deferred) }} \ No newline at end of file diff --git a/src/test/resources/eager/reconstructs-map-node.expected.jinja b/src/test/resources/eager/reconstructs-map-node.expected.jinja deleted file mode 100644 index a1d272f5d..000000000 --- a/src/test/resources/eager/reconstructs-map-node.expected.jinja +++ /dev/null @@ -1,7 +0,0 @@ -{% set my_list = [] %}{% for key, val in [fn:map_entry('foo', 'ff'), fn:map_entry('bar', 'bb')] %}{% do my_list.append(deferred) %} -{{ key ~ ' ' ~ val }}{% endfor %} -{{ my_list }} - -{% set my_list = [] %}{% for i in [fn:map_entry('foo', 'ff'), fn:map_entry('bar', 'bb')] %}{% do my_list.append(deferred) %} -{{ i.key }}{% endfor %} -{{ my_list }} diff --git a/src/test/resources/eager/reconstructs-map-node.expected.expected.jinja b/src/test/resources/eager/reconstructs-map-node/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/reconstructs-map-node.expected.expected.jinja rename to src/test/resources/eager/reconstructs-map-node/test.expected.expected.jinja diff --git a/src/test/resources/eager/reconstructs-map-node/test.expected.jinja b/src/test/resources/eager/reconstructs-map-node/test.expected.jinja new file mode 100644 index 000000000..12eeae891 --- /dev/null +++ b/src/test/resources/eager/reconstructs-map-node/test.expected.jinja @@ -0,0 +1,16 @@ +{% if deferred %} +{% set foo = [fn:map_entry('foo', 'ff'), fn:map_entry('bar', 'bb')] %} +{% endif %} +{% set my_list = [] %}\ +{% for key, val in foo %}\ +{% do my_list.append(deferred) %} +{{ key ~ ' ' ~ val }}\ +{% endfor %} +{{ my_list }} + +{% set my_list = [] %}\ +{% for __ignored__ in [0] %}\ +{% do my_list.append(deferred) %} +foo{% do my_list.append(deferred) %} +bar{% endfor %} +{{ my_list }} diff --git a/src/test/resources/eager/reconstructs-map-node.jinja b/src/test/resources/eager/reconstructs-map-node/test.jinja similarity index 70% rename from src/test/resources/eager/reconstructs-map-node.jinja rename to src/test/resources/eager/reconstructs-map-node/test.jinja index 1a3858aea..4c95bd29b 100644 --- a/src/test/resources/eager/reconstructs-map-node.jinja +++ b/src/test/resources/eager/reconstructs-map-node/test.jinja @@ -1,5 +1,8 @@ {% set my_list = [] %} -{% for key, val in {'foo': 'ff', 'bar': 'bb'}.items() -%} +{% if deferred %} +{% set foo = {'foo': 'ff', 'bar': 'bb'}.items() %} +{% endif %} +{% for key, val in foo -%} {% do my_list.append(deferred) %} {{ key ~ ' ' ~ val }} {%- endfor %} diff --git a/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period/test.expected.expected.jinja b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period/test.expected.expected.jinja new file mode 100644 index 000000000..337506cd0 --- /dev/null +++ b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period/test.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/test.expected.jinja b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period/test.expected.jinja new file mode 100644 index 000000000..72b11121d --- /dev/null +++ b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period/test.expected.jinja @@ -0,0 +1,11 @@ +{% 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/test.jinja b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period/test.jinja new file mode 100644 index 000000000..b380d12fa --- /dev/null +++ b/src/test/resources/eager/reconstructs-namespace-for-set-tags-using-period/test.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 }} diff --git a/src/test/resources/eager/reconstructs-nested-value-in-string-representation/test.expected.expected.jinja b/src/test/resources/eager/reconstructs-nested-value-in-string-representation/test.expected.expected.jinja new file mode 100644 index 000000000..111d01463 --- /dev/null +++ b/src/test/resources/eager/reconstructs-nested-value-in-string-representation/test.expected.expected.jinja @@ -0,0 +1,11 @@ +map.foo: Foo is I am foo. +map.bar: Bar is I am bar. + + + +a + + + + +{'b': 'b'} diff --git a/src/test/resources/eager/reconstructs-nested-value-in-string-representation/test.expected.jinja b/src/test/resources/eager/reconstructs-nested-value-in-string-representation/test.expected.jinja new file mode 100644 index 000000000..6fb1f5520 --- /dev/null +++ b/src/test/resources/eager/reconstructs-nested-value-in-string-representation/test.expected.jinja @@ -0,0 +1,30 @@ +{% set i_am = 'I am' %}\ +{% set bar = '{{ i_am }} bar' %}\ +{% set foo = '{{ i_am }} foo' %}\ +{% set map_with_vals = {'foo': 'Foo is {{ foo }}\ +.'} %}\ +{% if deferred %} +{% do map_with_vals.put('bar', 'Bar is {{ bar }}\ +.') %} +{% endif %} +map.foo: {{ map_with_vals.foo }} +map.bar: {{ map_with_vals.bar }} + +{% set a = 'a' %}\ +{% set aa = '{{ a }}\ +' %}\ +{% if deferred %} +{% set aaa = '{{ aa }}\ +' %} +{{ aa }} +{% endif %} + +{% set b = 'b' %}\ +{% set bb = '{{ b }}\ +' %}\ +{% if deferred %} +{% set bbb = {'b': '{{ bb }}\ +'} %} +{'b': '{{ bb }}\ +'} +{% endif %} \ No newline at end of file diff --git a/src/test/resources/eager/reconstructs-nested-value-in-string-representation/test.jinja b/src/test/resources/eager/reconstructs-nested-value-in-string-representation/test.jinja new file mode 100644 index 000000000..93d747df9 --- /dev/null +++ b/src/test/resources/eager/reconstructs-nested-value-in-string-representation/test.jinja @@ -0,0 +1,24 @@ +{% set i_am = 'I am' %} +{% set foo = '{{ i_am }} foo' %} +{% set bar = '{{ i_am }} bar' %} +{% set map_with_vals = {'foo': 'Foo is {{ foo }}.'} %} +{% if deferred %} +{% do map_with_vals.put('bar', 'Bar is {{ bar }}.') %} +{% endif %} +map.foo: {{ map_with_vals.foo }} +map.bar: {{ map_with_vals.bar }} + +{%- set a = 'a' %} +{% set aa = '{{ a }}' %} +{% if deferred %} +{% set aaa = '{{ aa }}' %} +{{ aaa }} +{% endif -%} + +{%- set b = 'b' %} +{% set bb = '{{ b }}' %} +{% if deferred %} +{% set bbb = {'b': '{{ bb }}'} %} +{{ bbb }} +{% endif -%} + diff --git a/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller/test.expected.jinja b/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller/test.expected.jinja new file mode 100644 index 000000000..07c749944 --- /dev/null +++ b/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller/test.expected.jinja @@ -0,0 +1,12 @@ +{% if deferred %} +{% macro foo(var) %} +{% set second_list = [] %} +{% 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/test.jinja b/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller/test.jinja new file mode 100644 index 000000000..744281882 --- /dev/null +++ b/src/test/resources/eager/reconstructs-null-variables-in-deferred-caller/test.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 %} diff --git a/src/test/resources/eager/reconstructs-types-properly.expected.jinja b/src/test/resources/eager/reconstructs-types-properly.expected.jinja deleted file mode 100644 index 760060d05..000000000 --- a/src/test/resources/eager/reconstructs-types-properly.expected.jinja +++ /dev/null @@ -1 +0,0 @@ -{{ {'bool': 'true', 'num': '1'} ~ deferred }} diff --git a/src/test/resources/eager/reconstructs-types-properly/test.expected.jinja b/src/test/resources/eager/reconstructs-types-properly/test.expected.jinja new file mode 100644 index 000000000..0d978cddd --- /dev/null +++ b/src/test/resources/eager/reconstructs-types-properly/test.expected.jinja @@ -0,0 +1 @@ +{{ {'bool': 'true', 'num': '1'} ~ deferred }} diff --git a/src/test/resources/eager/reconstructs-types-properly.jinja b/src/test/resources/eager/reconstructs-types-properly/test.jinja similarity index 100% rename from src/test/resources/eager/reconstructs-types-properly.jinja rename to src/test/resources/eager/reconstructs-types-properly/test.jinja diff --git a/src/test/resources/eager/reconstructs-value-used-in-deferred-imported-macro/test.expected.expected.jinja b/src/test/resources/eager/reconstructs-value-used-in-deferred-imported-macro/test.expected.expected.jinja new file mode 100644 index 000000000..2d6f0822a --- /dev/null +++ b/src/test/resources/eager/reconstructs-value-used-in-deferred-imported-macro/test.expected.expected.jinja @@ -0,0 +1,4 @@ +resolved 1 + + +resolved resolved diff --git a/src/test/resources/eager/reconstructs-value-used-in-deferred-imported-macro/test.expected.jinja b/src/test/resources/eager/reconstructs-value-used-in-deferred-imported-macro/test.expected.jinja new file mode 100644 index 000000000..e83a98c98 --- /dev/null +++ b/src/test/resources/eager/reconstructs-value-used-in-deferred-imported-macro/test.expected.jinja @@ -0,0 +1,27 @@ +{% do %}\ +{% set __temp_meta_current_path_528180375__,current_path = current_path,'eager/reconstructs-value-used-in-deferred-imported-macro/uses-deferred-value-in-macro.jinja' %}\ +{% set __temp_meta_import_alias_1081745881__ = {} %}\ +{% for __ignored__ in [0] %}\ +{% set value = deferred %}\ +{% do __temp_meta_import_alias_1081745881__.update({'value': value}) %} + +{% do __temp_meta_import_alias_1081745881__.update({'import_resource_path': 'eager/reconstructs-value-used-in-deferred-imported-macro/uses-deferred-value-in-macro.jinja','value': value}) %}\ +{% endfor %}\ +{% set macros = __temp_meta_import_alias_1081745881__ %}\ +{% set current_path,__temp_meta_current_path_528180375__ = __temp_meta_current_path_528180375__,null %}\ +{% enddo %} + +{% set deferred_import_resource_path = 'eager/reconstructs-value-used-in-deferred-imported-macro/uses-deferred-value-in-macro.jinja' %}\ +{% macro macros.getValueAnd(other) %}\ +{% set value = macros.value %} +{{ value ~ ' ' ~ other }} +{% endmacro %}\ +{% set deferred_import_resource_path = null %}\ +{{ macros.getValueAnd(1) }} +{% set deferred_import_resource_path = 'eager/reconstructs-value-used-in-deferred-imported-macro/uses-deferred-value-in-macro.jinja' %}\ +{% macro macros.getValueAnd(other) %}\ +{% set value = macros.value %} +{{ value ~ ' ' ~ other }} +{% endmacro %}\ +{% set deferred_import_resource_path = null %}\ +{{ macros.getValueAnd(deferred) }} diff --git a/src/test/resources/eager/reconstructs-value-used-in-deferred-imported-macro/test.jinja b/src/test/resources/eager/reconstructs-value-used-in-deferred-imported-macro/test.jinja new file mode 100644 index 000000000..d5c0b0b63 --- /dev/null +++ b/src/test/resources/eager/reconstructs-value-used-in-deferred-imported-macro/test.jinja @@ -0,0 +1,4 @@ +{% import './uses-deferred-value-in-macro.jinja' as macros %} + +{{ macros.getValueAnd(1) }} +{{ macros.getValueAnd(deferred) }} diff --git a/src/test/resources/eager/reconstructs-value-used-in-deferred-imported-macro/uses-deferred-value-in-macro.jinja b/src/test/resources/eager/reconstructs-value-used-in-deferred-imported-macro/uses-deferred-value-in-macro.jinja new file mode 100644 index 000000000..0c19fc60a --- /dev/null +++ b/src/test/resources/eager/reconstructs-value-used-in-deferred-imported-macro/uses-deferred-value-in-macro.jinja @@ -0,0 +1,4 @@ +{% set value = deferred %} +{% macro getValueAnd(other) %} +{{ value ~ ' ' ~ other }} +{% endmacro %} diff --git a/src/test/resources/eager/reconstructs-with-multiple-loops.expected.jinja b/src/test/resources/eager/reconstructs-with-multiple-loops/test.expected.jinja similarity index 86% rename from src/test/resources/eager/reconstructs-with-multiple-loops.expected.jinja rename to src/test/resources/eager/reconstructs-with-multiple-loops/test.expected.jinja index 6e45bd85e..6f81e2636 100644 --- a/src/test/resources/eager/reconstructs-with-multiple-loops.expected.jinja +++ b/src/test/resources/eager/reconstructs-with-multiple-loops/test.expected.jinja @@ -1,6 +1,8 @@ [][] [0][0] -{% set alpha,beta = [0],[0] %}{% for i in deferred %} +{% set alpha = [0] %}\ +{% set beta = [0] %}\ +{% for i in deferred %} {% if deferred %} {% for j in deferred %} {% if deferred %} diff --git a/src/test/resources/eager/reconstructs-with-multiple-loops.jinja b/src/test/resources/eager/reconstructs-with-multiple-loops/test.jinja similarity index 100% rename from src/test/resources/eager/reconstructs-with-multiple-loops.jinja rename to src/test/resources/eager/reconstructs-with-multiple-loops/test.jinja diff --git a/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions/test.expected.expected.jinja b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions/test.expected.expected.jinja new file mode 100644 index 000000000..ac18b1763 --- /dev/null +++ b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions/test.expected.expected.jinja @@ -0,0 +1,2 @@ +a +{{ foo }} diff --git a/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions/test.expected.jinja b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions/test.expected.jinja new file mode 100644 index 000000000..c64e0b1ae --- /dev/null +++ b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions/test.expected.jinja @@ -0,0 +1,5 @@ +{% 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/test.jinja b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions/test.jinja new file mode 100644 index 000000000..49db98681 --- /dev/null +++ b/src/test/resources/eager/reconstructs-words-from-inside-nested-expressions/test.jinja @@ -0,0 +1,5 @@ +{% set foo = 'a' %} +{% set bar = 'b' %} +{% do deferred.append('{{ foo }}') %} +{{ deferred[0] }} +{% print deferred[0] %} diff --git a/src/test/resources/eager/reverts-modification-with-deferred-loop.expected.jinja b/src/test/resources/eager/reverts-modification-with-deferred-loop.expected.jinja deleted file mode 100644 index 0f3b95d12..000000000 --- a/src/test/resources/eager/reverts-modification-with-deferred-loop.expected.jinja +++ /dev/null @@ -1,8 +0,0 @@ -{% set my_list = [] %}{% for i in [0, 1] %} -{% for j in deferred %} -{% if loop.first %} -{% do my_list.append(1) %} -{% endif %} -{% endfor %} -{% endfor %} -{{ my_list }} diff --git a/src/test/resources/eager/reverts-modification-with-deferred-loop/test.expected.jinja b/src/test/resources/eager/reverts-modification-with-deferred-loop/test.expected.jinja new file mode 100644 index 000000000..57568c5f2 --- /dev/null +++ b/src/test/resources/eager/reverts-modification-with-deferred-loop/test.expected.jinja @@ -0,0 +1,15 @@ +{% set my_list = [] %}\ +{% for __ignored__ in [0] %} +{% for j in deferred %} +{% if loop.first %} +{% do my_list.append(1) %} +{% endif %} +{% endfor %} + +{% for j in deferred %} +{% if loop.first %} +{% do my_list.append(1) %} +{% endif %} +{% endfor %} +{% endfor %} +{{ my_list }} diff --git a/src/test/resources/eager/reverts-modification-with-deferred-loop.jinja b/src/test/resources/eager/reverts-modification-with-deferred-loop/test.jinja similarity index 100% rename from src/test/resources/eager/reverts-modification-with-deferred-loop.jinja rename to src/test/resources/eager/reverts-modification-with-deferred-loop/test.jinja diff --git a/src/test/resources/eager/reverts-simple.expected.jinja b/src/test/resources/eager/reverts-simple/test.expected.jinja similarity index 55% rename from src/test/resources/eager/reverts-simple.expected.jinja rename to src/test/resources/eager/reverts-simple/test.expected.jinja index 946e1452d..1e69abbd1 100644 --- a/src/test/resources/eager/reverts-simple.expected.jinja +++ b/src/test/resources/eager/reverts-simple/test.expected.jinja @@ -1,9 +1,10 @@ {% for __ignored__ in [0] %} - {% set my_list = [0, 1] %}{% if deferred %} - {% set my_list = [0, 1, 2] %} + {% set my_list = [0, 1] %}\ +{% if deferred %} + {% 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 %} diff --git a/src/test/resources/eager/reverts-simple.jinja b/src/test/resources/eager/reverts-simple/test.jinja similarity index 100% rename from src/test/resources/eager/reverts-simple.jinja rename to src/test/resources/eager/reverts-simple/test.jinja diff --git a/src/test/resources/eager/runs-for-loop-inside-deferred-for-loop.expected.jinja b/src/test/resources/eager/runs-for-loop-inside-deferred-for-loop/test.expected.jinja similarity index 100% rename from src/test/resources/eager/runs-for-loop-inside-deferred-for-loop.expected.jinja rename to src/test/resources/eager/runs-for-loop-inside-deferred-for-loop/test.expected.jinja diff --git a/src/test/resources/eager/runs-for-loop-inside-deferred-for-loop.jinja b/src/test/resources/eager/runs-for-loop-inside-deferred-for-loop/test.jinja similarity index 100% rename from src/test/resources/eager/runs-for-loop-inside-deferred-for-loop.jinja rename to src/test/resources/eager/runs-for-loop-inside-deferred-for-loop/test.jinja diff --git a/src/test/resources/eager/runs-macro-function-in-deferred-execution-mode/test.expected.jinja b/src/test/resources/eager/runs-macro-function-in-deferred-execution-mode/test.expected.jinja new file mode 100644 index 000000000..d7ce85653 --- /dev/null +++ b/src/test/resources/eager/runs-macro-function-in-deferred-execution-mode/test.expected.jinja @@ -0,0 +1,12 @@ +{% 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/test.jinja b/src/test/resources/eager/runs-macro-function-in-deferred-execution-mode/test.jinja new file mode 100644 index 000000000..9670c58de --- /dev/null +++ b/src/test/resources/eager/runs-macro-function-in-deferred-execution-mode/test.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 }} diff --git a/src/test/resources/eager/scopes-resetting-bindings.expected.jinja b/src/test/resources/eager/scopes-resetting-bindings/test.expected.jinja similarity index 71% rename from src/test/resources/eager/scopes-resetting-bindings.expected.jinja rename to src/test/resources/eager/scopes-resetting-bindings/test.expected.jinja index 4c8f4b527..097e54f2d 100644 --- a/src/test/resources/eager/scopes-resetting-bindings.expected.jinja +++ b/src/test/resources/eager/scopes-resetting-bindings/test.expected.jinja @@ -1,4 +1,5 @@ -{% set my_list = [0] %}{% for i in deferred %} +{% set my_list = [0] %}\ +{% for i in deferred %} {% for j in deferred %} {% if deferred %} {% do my_list.append(1) %} diff --git a/src/test/resources/eager/scopes-resetting-bindings.jinja b/src/test/resources/eager/scopes-resetting-bindings/test.jinja similarity index 100% rename from src/test/resources/eager/scopes-resetting-bindings.jinja rename to src/test/resources/eager/scopes-resetting-bindings/test.jinja diff --git a/src/test/resources/eager/sets-multiple-vars-deferred-in-child.expected.jinja b/src/test/resources/eager/sets-multiple-vars-deferred-in-child.expected.jinja deleted file mode 100644 index 463d70d3a..000000000 --- a/src/test/resources/eager/sets-multiple-vars-deferred-in-child.expected.jinja +++ /dev/null @@ -1,2 +0,0 @@ -1 & 2{% set bar,foo = 2,3 %}{% if deferred %}{% set bar = 4 %}{% else %}{% set foo = 5 %}{% endif %} -{{ foo }} & {{ bar }} diff --git a/src/test/resources/eager/sets-multiple-vars-deferred-in-child.expected.expected.jinja b/src/test/resources/eager/sets-multiple-vars-deferred-in-child/test.expected.expected.jinja similarity index 100% rename from src/test/resources/eager/sets-multiple-vars-deferred-in-child.expected.expected.jinja rename to src/test/resources/eager/sets-multiple-vars-deferred-in-child/test.expected.expected.jinja diff --git a/src/test/resources/eager/sets-multiple-vars-deferred-in-child/test.expected.jinja b/src/test/resources/eager/sets-multiple-vars-deferred-in-child/test.expected.jinja new file mode 100644 index 000000000..5de8e1606 --- /dev/null +++ b/src/test/resources/eager/sets-multiple-vars-deferred-in-child/test.expected.jinja @@ -0,0 +1,8 @@ +1 & 2{% set bar = 2 %}\ +{% set foo = 3 %}\ +{% if deferred %}\ +{% set bar = 4 %}\ +{% else %}\ +{% set foo = 5 %}\ +{% endif %} +{{ foo }} & {{ bar }} diff --git a/src/test/resources/eager/sets-multiple-vars-deferred-in-child.jinja b/src/test/resources/eager/sets-multiple-vars-deferred-in-child/test.jinja similarity index 100% rename from src/test/resources/eager/sets-multiple-vars-deferred-in-child.jinja rename to src/test/resources/eager/sets-multiple-vars-deferred-in-child/test.jinja diff --git a/src/test/resources/tags/macrotag/deferred-modification.jinja b/src/test/resources/eager/supplements/deferred-modification.jinja similarity index 100% rename from src/test/resources/tags/macrotag/deferred-modification.jinja rename to src/test/resources/eager/supplements/deferred-modification.jinja diff --git a/src/test/resources/tags/macrotag/macro-and-set.jinja b/src/test/resources/eager/supplements/macro-and-set.jinja similarity index 100% rename from src/test/resources/tags/macrotag/macro-and-set.jinja rename to src/test/resources/eager/supplements/macro-and-set.jinja diff --git a/src/test/resources/eager/supplements/set-val.jinja b/src/test/resources/eager/supplements/set-val.jinja new file mode 100644 index 000000000..381648d46 --- /dev/null +++ b/src/test/resources/eager/supplements/set-val.jinja @@ -0,0 +1 @@ +{% set primary_line_height = 42 %} \ No newline at end of file diff --git a/src/test/resources/eager/supplements/simple-with-call.jinja b/src/test/resources/eager/supplements/simple-with-call.jinja new file mode 100644 index 000000000..243ecf68a --- /dev/null +++ b/src/test/resources/eager/supplements/simple-with-call.jinja @@ -0,0 +1,5 @@ +{% macro getPath() -%} + Hello {{ myname }} +{%- endmacro %} + +{{ getPath() }} diff --git a/src/test/resources/eager/uses-unique-macro-names/macro-with-filter.jinja b/src/test/resources/eager/uses-unique-macro-names/macro-with-filter.jinja new file mode 100644 index 000000000..fd1869569 --- /dev/null +++ b/src/test/resources/eager/uses-unique-macro-names/macro-with-filter.jinja @@ -0,0 +1,4 @@ +{% macro foo() -%} +Hello {{ myname }} +{%- endmacro %} +{% set b = foo()|upper %} diff --git a/src/test/resources/eager/uses-unique-macro-names/test.expected.expected.jinja b/src/test/resources/eager/uses-unique-macro-names/test.expected.expected.jinja new file mode 100644 index 000000000..b82681932 --- /dev/null +++ b/src/test/resources/eager/uses-unique-macro-names/test.expected.expected.jinja @@ -0,0 +1,3 @@ +GOODBYE RESOLVED + +HELLO RESOLVED diff --git a/src/test/resources/eager/uses-unique-macro-names/test.expected.jinja b/src/test/resources/eager/uses-unique-macro-names/test.expected.jinja new file mode 100644 index 000000000..54c40b809 --- /dev/null +++ b/src/test/resources/eager/uses-unique-macro-names/test.expected.jinja @@ -0,0 +1,16 @@ +{% 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____) %} +{% do %}\ +{% set __temp_meta_current_path_203114534__,current_path = current_path,'eager/uses-unique-macro-names/macro-with-filter.jinja' %} +{% set __macro_foo_1717337666_temp_variable_0__ %}\ +Hello {{ myname }}\ +{% endset %}\ +{% set b = filter:upper.filter(__macro_foo_1717337666_temp_variable_0__, ____int3rpr3t3r____) %} +{% set current_path,__temp_meta_current_path_203114534__ = __temp_meta_current_path_203114534__,null %}\ +{% enddo %} +{{ a }} +{{ b }} \ No newline at end of file diff --git a/src/test/resources/eager/uses-unique-macro-names/test.jinja b/src/test/resources/eager/uses-unique-macro-names/test.jinja new file mode 100644 index 000000000..c5c123857 --- /dev/null +++ b/src/test/resources/eager/uses-unique-macro-names/test.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/eager/wraps-certain-output-in-raw.expected.jinja b/src/test/resources/eager/wraps-certain-output-in-raw.expected.jinja deleted file mode 100644 index bbe220d27..000000000 --- a/src/test/resources/eager/wraps-certain-output-in-raw.expected.jinja +++ /dev/null @@ -1,8 +0,0 @@ -{% raw %}{{ bar }}{% endraw %} -{% raw %}{{ bar }}{% endraw %} -{% raw %}{% print foo %}{% endraw %} -{% raw %}{% print foo %}{% endraw %} -{% raw %}{% baz %}{% endraw %} -{% raw %}{% baz %}{% endraw %} -No raw -No raw diff --git a/src/test/resources/eager/wraps-certain-output-in-raw/test.expected.jinja b/src/test/resources/eager/wraps-certain-output-in-raw/test.expected.jinja new file mode 100644 index 000000000..9e748c22c --- /dev/null +++ b/src/test/resources/eager/wraps-certain-output-in-raw/test.expected.jinja @@ -0,0 +1,20 @@ +{% raw %}\ +{{ bar }}\ +{% endraw %} +{% raw %}\ +{{ bar }}\ +{% endraw %} +{% raw %}\ +{% print foo %}\ +{% endraw %} +{% raw %}\ +{% print foo %}\ +{% endraw %} +{% raw %}\ +{% baz %}\ +{% endraw %} +{% raw %}\ +{% baz %}\ +{% endraw %} +No raw +No raw diff --git a/src/test/resources/eager/wraps-certain-output-in-raw.jinja b/src/test/resources/eager/wraps-certain-output-in-raw/test.jinja similarity index 100% rename from src/test/resources/eager/wraps-certain-output-in-raw.jinja rename to src/test/resources/eager/wraps-certain-output-in-raw/test.jinja diff --git a/src/test/resources/eager/wraps-macro-that-would-change-current-path-in-child-scope/dir1/macro.jinja b/src/test/resources/eager/wraps-macro-that-would-change-current-path-in-child-scope/dir1/macro.jinja new file mode 100644 index 000000000..45fb53d08 --- /dev/null +++ b/src/test/resources/eager/wraps-macro-that-would-change-current-path-in-child-scope/dir1/macro.jinja @@ -0,0 +1,4 @@ +{% macro foo_importer() -%} + {%- include "../dir2/included.jinja" -%} + {%- print foo -%} +{%- endmacro %} \ No newline at end of file diff --git a/src/test/resources/eager/wraps-macro-that-would-change-current-path-in-child-scope/dir2/included.jinja b/src/test/resources/eager/wraps-macro-that-would-change-current-path-in-child-scope/dir2/included.jinja new file mode 100644 index 000000000..bde4ef606 --- /dev/null +++ b/src/test/resources/eager/wraps-macro-that-would-change-current-path-in-child-scope/dir2/included.jinja @@ -0,0 +1,2 @@ +{% set foo = deferred %} +{{ foo }} \ No newline at end of file diff --git a/src/test/resources/eager/wraps-macro-that-would-change-current-path-in-child-scope/test.expected.jinja b/src/test/resources/eager/wraps-macro-that-would-change-current-path-in-child-scope/test.expected.jinja new file mode 100644 index 000000000..1627e6490 --- /dev/null +++ b/src/test/resources/eager/wraps-macro-that-would-change-current-path-in-child-scope/test.expected.jinja @@ -0,0 +1,10 @@ +Starting current_path: eager/wraps-macro-that-would-change-current-path-in-child-scope/test.jinja +Intermediate current_path: eager/wraps-macro-that-would-change-current-path-in-child-scope/test.jinja +{% for __ignored__ in [0] %}\ +{% set __temp_meta_current_path_629250870__,current_path = 'eager/wraps-macro-that-would-change-current-path-in-child-scope/test.jinja', 'eager/wraps-macro-that-would-change-current-path-in-child-scope/dir2/included.jinja' %}\ +{% set foo = deferred %} +{{ foo }}\ +{% set current_path,__temp_meta_current_path_629250870__ = 'eager/wraps-macro-that-would-change-current-path-in-child-scope/test.jinja', null %}\ +{% print foo %}\ +{% endfor %} +Ending current_path: eager/wraps-macro-that-would-change-current-path-in-child-scope/test.jinja diff --git a/src/test/resources/eager/wraps-macro-that-would-change-current-path-in-child-scope/test.jinja b/src/test/resources/eager/wraps-macro-that-would-change-current-path-in-child-scope/test.jinja new file mode 100644 index 000000000..e1f64358a --- /dev/null +++ b/src/test/resources/eager/wraps-macro-that-would-change-current-path-in-child-scope/test.jinja @@ -0,0 +1,5 @@ +Starting current_path: {{ current_path }} +{% from "./dir1/macro.jinja" import foo_importer -%} +Intermediate current_path: {{ current_path }} +{{ foo_importer() }} +Ending current_path: {{ current_path }} 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 diff --git a/src/test/resources/filter/render/base.jinja b/src/test/resources/filter/render/base.jinja new file mode 100644 index 000000000..d9488b984 --- /dev/null +++ b/src/test/resources/filter/render/base.jinja @@ -0,0 +1,6 @@ +Body is: {% block body %} +Base body +{% endblock %} +Footer is: {% block footer %} +Base footer +{% endblock %} diff --git a/src/test/resources/filter/slice-filter-big.jinja b/src/test/resources/filter/slice-filter-big.jinja new file mode 100644 index 000000000..6710c8fd7 --- /dev/null +++ b/src/test/resources/filter/slice-filter-big.jinja @@ -0,0 +1,6 @@ +{%- for column in items|slice(999999999, 'hello') %} + {{ loop.index }} + {%- for item in column %} + {{ item }} + {%- endfor %} +{%- endfor %} diff --git a/src/test/resources/parse/tokenizer/tag-with-all-escaped-quotes.jinja b/src/test/resources/parse/tokenizer/tag-with-all-escaped-quotes.jinja new file mode 100644 index 000000000..5f9a709a9 --- /dev/null +++ b/src/test/resources/parse/tokenizer/tag-with-all-escaped-quotes.jinja @@ -0,0 +1,7 @@ +{# This print tag is invalid, but it should not cause the rest of the template to break #} +{% print \"foo\" %} +Start +{% if true %} +The dog says: "Woof!" +{% endif %} +End diff --git a/src/test/resources/parse/tokenizer/whitespace-comment-tags.jinja b/src/test/resources/parse/tokenizer/whitespace-comment-tags.jinja new file mode 100644 index 000000000..6cb5a61bb --- /dev/null +++ b/src/test/resources/parse/tokenizer/whitespace-comment-tags.jinja @@ -0,0 +1,8 @@ +
    + {# a comment #} + yay + {# another comment + This time, over multiple lines + #} + whoop +
    diff --git a/src/test/resources/tags/calltag/multiple.jinja b/src/test/resources/tags/calltag/multiple.jinja new file mode 100644 index 000000000..2bbd3beae --- /dev/null +++ b/src/test/resources/tags/calltag/multiple.jinja @@ -0,0 +1,4 @@ +{% macro test1() %}1{{ caller() }}{% endmacro %} +{% macro test2() %}2{{ caller() }}{% endmacro %} +{% macro test3() %}3{{ caller() }}{% endmacro %} +{% call test1() %}{% call test2() %}{% call test3() %}{% endcall %}{% endcall %}{% endcall %} \ No newline at end of file diff --git a/src/test/resources/tags/eager/extendstag/defers-block-in-extends-child.expected.jinja b/src/test/resources/tags/eager/extendstag/defers-block-in-extends-child.expected.jinja index 9ffb4d867..1e804dbee 100644 --- a/src/test/resources/tags/eager/extendstag/defers-block-in-extends-child.expected.jinja +++ b/src/test/resources/tags/eager/extendstag/defers-block-in-extends-child.expected.jinja @@ -1,8 +1,11 @@ +{% set current_path = '../eager/extendstag/base.html' %}\ diff --git a/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred-nested-interp.expected.expected.jinja b/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred-nested-interp.expected.expected.jinja new file mode 100644 index 000000000..a4e73e2b1 --- /dev/null +++ b/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred-nested-interp.expected.expected.jinja @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred-nested-interp.expected.jinja b/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred-nested-interp.expected.jinja new file mode 100644 index 000000000..41b046b1c --- /dev/null +++ b/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred-nested-interp.expected.jinja @@ -0,0 +1,13 @@ +{% set current_path = '../eager/extendstag/base-deferred.html' %}\ + + + + + diff --git a/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred-nested-interp.jinja b/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred-nested-interp.jinja new file mode 100644 index 000000000..cc301ad57 --- /dev/null +++ b/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred-nested-interp.jinja @@ -0,0 +1,6 @@ +{% extends "../eager/extendstag/base-deferred.html" %} + +{%- block sidebar -%} +

    Table Of Contents

    +{{ super() }} +{%- endblock -%} diff --git a/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred.expected.jinja b/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred.expected.jinja index 70100b641..41b046b1c 100644 --- a/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred.expected.jinja +++ b/src/test/resources/tags/eager/extendstag/defers-super-block-with-deferred.expected.jinja @@ -1,10 +1,13 @@ +{% set current_path = '../eager/extendstag/base-deferred.html' %}\ - \ No newline at end of file + 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..b01cdf9bf 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,13 +1,18 @@ -{% set __ignored__ %} +{# Start Label: ignored_output_from_extends #}{% do %} {% if deferred %} {% set foo = 'yes' %} {% else %} {% set foo = 'no' %} -{% endif %}{% endset %} +{% endif %}\ +{% enddo %}\ +{# End Label: ignored_output_from_extends #}{% set current_path = '../eager/extendstag/base.html' %}\ + diff --git a/src/test/resources/tags/eager/fortag/handles-nested-deferred-for-loop.expected.jinja b/src/test/resources/tags/eager/fortag/handles-nested-deferred-for-loop.expected.jinja index 9e53ab4fc..fd102d03e 100644 --- a/src/test/resources/tags/eager/fortag/handles-nested-deferred-for-loop.expected.jinja +++ b/src/test/resources/tags/eager/fortag/handles-nested-deferred-for-loop.expected.jinja @@ -1,13 +1,16 @@ {% for __ignored__ in [0] %} {% for bar in deferred %} -pastrami sandwich. {{ bar }}! +pastrami sandwich. {{ bar }}\ +! {% endfor %} {% for bar in deferred %} -pastrami salad. {{ bar }}! +pastrami salad. {{ bar }}\ +! {% endfor %} {% for bar in deferred %} -pastrami smoothie. {{ bar }}! +pastrami smoothie. {{ bar }}\ +! {% endfor %} {% endfor %} diff --git a/src/test/resources/tags/eager/iftag/handles-only-deferred-elif.expected.jinja b/src/test/resources/tags/eager/iftag/handles-only-deferred-elif.expected.jinja index a175246e0..ea47d3bd2 100644 --- a/src/test/resources/tags/eager/iftag/handles-only-deferred-elif.expected.jinja +++ b/src/test/resources/tags/eager/iftag/handles-only-deferred-elif.expected.jinja @@ -1,4 +1,5 @@ -{% if false %}{% elif deferred > 0 %} +{% if false %}\ +{% elif deferred > 0 %} Positive {{ deferred }} {% else %} Negative {{ deferred }} diff --git a/src/test/resources/tags/eager/importtag/layer-one.jinja b/src/test/resources/tags/eager/importtag/layer-one.jinja new file mode 100644 index 000000000..163bd0c3d --- /dev/null +++ b/src/test/resources/tags/eager/importtag/layer-one.jinja @@ -0,0 +1,2 @@ +{% set bar = 'bar val' %} +{% import 'layer-two.jinja' as double_child %} \ No newline at end of file diff --git a/src/test/resources/tags/eager/importtag/layer-two.jinja b/src/test/resources/tags/eager/importtag/layer-two.jinja new file mode 100644 index 000000000..6c9be8f6f --- /dev/null +++ b/src/test/resources/tags/eager/importtag/layer-two.jinja @@ -0,0 +1,2 @@ +{% set foo = 'foo val' %} +{% set foo_d = deferred %} 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' %} 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..945acff03 --- /dev/null +++ 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 new file mode 100644 index 000000000..2ecfd14ef --- /dev/null +++ b/src/test/resources/tags/eager/importtag/var-b.jinja @@ -0,0 +1 @@ +{% set foo = 'b' %} diff --git a/src/test/resources/tags/eager/includetag/includes-deferred.expected.jinja b/src/test/resources/tags/eager/includetag/includes-deferred.expected.jinja index 7431ee8c1..606c5930b 100644 --- a/src/test/resources/tags/eager/includetag/includes-deferred.expected.jinja +++ b/src/test/resources/tags/eager/includetag/includes-deferred.expected.jinja @@ -1,5 +1,7 @@ Foo begins as: abc -{% set current_path = 'sets-to-deferred.jinja' %}{% set foo = deferred %} -foo is now {{ deferred }}. -{% set current_path = '' %} +{% set __temp_meta_current_path_38651297__,current_path = current_path,'sets-to-deferred.jinja' %}\ +{% set foo = deferred %} +foo is now {{ deferred }}\ +. +{% set current_path,__temp_meta_current_path_38651297__ = __temp_meta_current_path_38651297__,null %} Foo ends as: {{ foo }} diff --git a/src/test/resources/tags/macrotag/from-alias-macro.jinja b/src/test/resources/tags/macrotag/from-alias-macro.jinja new file mode 100644 index 000000000..fdfb0ed21 --- /dev/null +++ b/src/test/resources/tags/macrotag/from-alias-macro.jinja @@ -0,0 +1,4 @@ +{% from "pegasus-importable.jinja" import wrap_padding as wp, spacer as sp %} + +wrap-padding: {{ wp }} +wrap-spacer: {{ sp() }} \ No newline at end of file diff --git a/src/test/resources/tags/settag/set-var-and-deferred.jinja b/src/test/resources/tags/settag/set-var-and-deferred.jinja deleted file mode 100644 index 54da34656..000000000 --- a/src/test/resources/tags/settag/set-var-and-deferred.jinja +++ /dev/null @@ -1,6 +0,0 @@ -{% 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 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 %} -{{ my_var }} -{% endif %}