From 3703851655463e39a64d43e3b9309fc340a2da30 Mon Sep 17 00:00:00 2001 From: jasmith-hs Date: Mon, 23 Feb 2026 11:20:41 -0500 Subject: [PATCH] Use isResolvableObject instead of contains before trying to build hashcode 1. using `contains` can cause a StackOverflowError 2. If the depth grows very deep, running hashCode can be quite slow, using isResolvableObject will break out before going into too much depth and we won't use hashCode In cases where we can't use the hashCode of such objects, we just aren't able to check for changes to them --- .../hubspot/jinjava/util/EagerContextWatcher.java | 11 +++++++---- src/test/java/com/hubspot/jinjava/EagerTest.java | 7 +++++++ .../test.expected.jinja | 3 +++ .../test.jinja | 12 ++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 src/test/resources/eager/does-not-stack-overflow-trying-to-build-hashcode/test.expected.jinja create mode 100644 src/test/resources/eager/does-not-stack-overflow-trying-to-build-hashcode/test.jinja diff --git a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java index 83ed8f674..334886152 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerContextWatcher.java @@ -123,8 +123,7 @@ private static Map getInitiallyResolvedAsStrings( ? entrySet : interpreter.getContext().getCombinedScope().entrySet()).stream() .filter(entry -> initiallyResolvedHashes.containsKey(entry.getKey())) - .filter(entry -> - EagerExpressionResolver.isResolvableObject(entry.getValue(), 4, 400) // TODO make this configurable + .filter(entry -> isResolvableForContextReverting(entry.getValue()) // TODO make this configurable ); entryStream.forEach(entry -> cacheRevertibleObject( @@ -392,15 +391,19 @@ private static Object getObjectOrHashCode(Object o) { o = ((LazyExpression) o).get(); } - if (o instanceof PyList && !((PyList) o).toList().contains(o)) { + if (o instanceof PyList && isResolvableForContextReverting(o)) { return o.hashCode(); } - if (o instanceof PyMap && !((PyMap) o).toMap().containsValue(o)) { + 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; diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 0ba42fe2c..88424f8c6 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -1740,4 +1740,11 @@ public void itHandlesModifiedIncludePathSecondPass() { "handles-modified-include-path/test.expected" ); } + + @Test + public void itDoesNotStackOverflowTryingToBuildHashcode() { + expectedTemplateInterpreter.assertExpectedOutput( + "does-not-stack-overflow-trying-to-build-hashcode/test" + ); + } } 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