From 508c867fa8c1284e24772de00f7f23f2b352453a Mon Sep 17 00:00:00 2001 From: jasmith-hs Date: Fri, 26 Jun 2026 11:43:40 -0400 Subject: [PATCH 1/2] fix: reconstruct current_path when base template defers before blocks When a base template defers a variable before its block definitions, the eager first pass produced output without {% set current_path = '...' %}, leaving the second pass without a base-template path reference. This would break relative path resolution for any base template using includes or other path-dependent tags. The pathSetter was only written when resolveBlockStubs() added deferred tokens (deferred content inside block bodies). It was not written when the base template itself produced deferred tokens before its blocks (preserveBlocks = true). Fix: also set pathSetter when preserveBlocks is true. Co-Authored-By: Claude Sonnet 4.6 --- .../jinjava/interpret/JinjavaInterpreter.java | 4 +++- .../lib/tag/eager/EagerExtendsTagTest.java | 15 +++++++++++++++ .../base-with-deferred-before-block.html | 4 ++++ ...t-in-base-before-block.expected.expected.jinja | 4 ++++ ...defers-set-in-base-before-block.expected.jinja | 6 ++++++ .../defers-set-in-base-before-block.jinja | 4 ++++ 6 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/tags/eager/extendstag/base-with-deferred-before-block.html create mode 100644 src/test/resources/tags/eager/extendstag/defers-set-in-base-before-block.expected.expected.jinja create mode 100644 src/test/resources/tags/eager/extendstag/defers-set-in-base-before-block.expected.jinja create mode 100644 src/test/resources/tags/eager/extendstag/defers-set-in-base-before-block.jinja diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index 4034ee43c..0e6f90533 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -513,7 +513,9 @@ private String render(Node root, boolean processExtendRoots, long renderLimit) { ); } } - if (context.getDeferredTokens().size() > numDeferredTokensBefore) { + if ( + preserveBlocks || context.getDeferredTokens().size() > numDeferredTokensBefore + ) { pathSetter.setValue( EagerReconstructionUtils.buildBlockOrInlineSetTag( RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, 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 93d6c4916..f9a38ce13 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 @@ -124,6 +124,21 @@ public void itReconstructsDeferredOutsideBlockSecondPass() { ); } + @Test + public void itDefersSetInBaseBeforeBlock() { + expectedTemplateInterpreter.assertExpectedOutputNonIdempotent( + "defers-set-in-base-before-block" + ); + } + + @Test + public void itDefersSetInBaseBeforeBlockSecondPass() { + context.put("deferred", "Resolved now"); + expectedTemplateInterpreter.assertExpectedOutput( + "defers-set-in-base-before-block.expected" + ); + } + @Test public void itThrowsWhenDeferredExtendsTag() { interpreter.render( diff --git a/src/test/resources/tags/eager/extendstag/base-with-deferred-before-block.html b/src/test/resources/tags/eager/extendstag/base-with-deferred-before-block.html new file mode 100644 index 000000000..a95db89f1 --- /dev/null +++ b/src/test/resources/tags/eager/extendstag/base-with-deferred-before-block.html @@ -0,0 +1,4 @@ +{% set foo = deferred %} +{{ foo }} +{% block body %} +{% endblock body %} diff --git a/src/test/resources/tags/eager/extendstag/defers-set-in-base-before-block.expected.expected.jinja b/src/test/resources/tags/eager/extendstag/defers-set-in-base-before-block.expected.expected.jinja new file mode 100644 index 000000000..03c953931 --- /dev/null +++ b/src/test/resources/tags/eager/extendstag/defers-set-in-base-before-block.expected.expected.jinja @@ -0,0 +1,4 @@ + +Resolved now + +Hi diff --git a/src/test/resources/tags/eager/extendstag/defers-set-in-base-before-block.expected.jinja b/src/test/resources/tags/eager/extendstag/defers-set-in-base-before-block.expected.jinja new file mode 100644 index 000000000..bb5f90f14 --- /dev/null +++ b/src/test/resources/tags/eager/extendstag/defers-set-in-base-before-block.expected.jinja @@ -0,0 +1,6 @@ +{% set current_path = '../eager/extendstag/base-with-deferred-before-block.html' %}\ +{% set foo = deferred %} +{{ foo }} +{% block body %} +Hi +{% endblock body %} diff --git a/src/test/resources/tags/eager/extendstag/defers-set-in-base-before-block.jinja b/src/test/resources/tags/eager/extendstag/defers-set-in-base-before-block.jinja new file mode 100644 index 000000000..795d43e39 --- /dev/null +++ b/src/test/resources/tags/eager/extendstag/defers-set-in-base-before-block.jinja @@ -0,0 +1,4 @@ +{% extends "../eager/extendstag/base-with-deferred-before-block.html" %} +{% block body %} +Hi +{% endblock body %} From 92454e3fda0588d0c855c6d3fb1dfb2e0350f01e Mon Sep 17 00:00:00 2001 From: jasmith-hs Date: Tue, 30 Jun 2026 09:46:54 -0400 Subject: [PATCH 2/2] fix: only reconstruct current_path when an extends actually deferred The prior commit widened the path-setter condition to fire whenever preserveBlocks was true. But preserveBlocks was unsound outside the extends loop: numDeferredTokensBefore is initialized to 0 and only advanced inside the `while (!extendParentRoots.isEmpty())` loop, so when a template had no extend parents the loop never ran and preserveBlocks collapsed to `deferredTokens.size() > 0`. Any deferred template (for, import, set, include, ...) then injected a spurious `{% set current_path = null %}`, breaking 279 tests. Gate preserveBlocks on an extends having actually been processed (processedExtendRoots) so the path setter only fires in the base-template-defers-before-blocks case it was meant for. The no-extends path falls back to the original resolveBlockStubs growth check, restoring prior behavior. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../com/hubspot/jinjava/interpret/JinjavaInterpreter.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index 0e6f90533..c7c93d369 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -432,6 +432,7 @@ private String render(Node root, boolean processExtendRoots, long renderLimit) { if (processExtendRoots) { Set extendPaths = new HashSet<>(); Optional extendPath = context.getExtendPathStack().peek(); + boolean processedExtendRoots = false; int numDeferredTokensBefore = 0; while (!extendParentRoots.isEmpty()) { if (extendPaths.contains(extendPath.orElse(""))) { @@ -494,9 +495,12 @@ private String render(Node root, boolean processExtendRoots, long renderLimit) { extendPath = hasNestedExtends ? currentExtendPath : context.getExtendPathStack().peek(); basePath = Optional.of(currentPath); + processedExtendRoots = true; } } - preserveBlocks = (context.getDeferredTokens().size() > numDeferredTokensBefore); + preserveBlocks = + processedExtendRoots && + (context.getDeferredTokens().size() > numDeferredTokensBefore); } int numDeferredTokensBefore = context.getDeferredTokens().size();