Skip to content

Commit 1bb5593

Browse files
hs-lsongclaude
andcommitted
feat: Add isPreserveResolvedSetTags flag for independent set tag preservation
Add a new context configuration flag that allows set tags with fully resolved RHS values to be preserved in output, independent of macro deferral behavior. This enables both: - Macros to partially evaluate (preserving only undefined vars within) - Set tags to be preserved as {% set x = 'value' %} for multi-pass rendering Also adds import/from macro tests to verify partial macro evaluation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 97eca17 commit 1bb5593

File tree

5 files changed

+69
-3
lines changed

5 files changed

+69
-3
lines changed

src/main/java/com/hubspot/jinjava/interpret/Context.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,15 @@ public TemporaryValueClosable<Boolean> withPartialMacroEvaluation(
875875
return temporaryValueClosable;
876876
}
877877

878+
public boolean isPreserveResolvedSetTags() {
879+
return contextConfiguration.isPreserveResolvedSetTags();
880+
}
881+
882+
public void setPreserveResolvedSetTags(boolean preserveResolvedSetTags) {
883+
contextConfiguration =
884+
contextConfiguration.withPreserveResolvedSetTags(preserveResolvedSetTags);
885+
}
886+
878887
public boolean isUnwrapRawOverride() {
879888
return contextConfiguration.isUnwrapRawOverride();
880889
}

src/main/java/com/hubspot/jinjava/interpret/ContextConfigurationIF.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ default boolean isPartialMacroEvaluation() {
3838
return false;
3939
}
4040

41+
@Default
42+
default boolean isPreserveResolvedSetTags() {
43+
return false;
44+
}
45+
4146
@Default
4247
default boolean isUnwrapRawOverride() {
4348
return false;

src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagStrategy.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public String run(TagNode tagNode, JinjavaInterpreter interpreter) {
5858
if (
5959
eagerExecutionResult.getResult().isFullyResolved() &&
6060
!interpreter.getContext().isDeferredExecutionMode() &&
61+
!interpreter.getContext().isPreserveResolvedSetTags() &&
6162
(Arrays
6263
.stream(variables)
6364
.noneMatch(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY::equals) ||

src/main/java/com/hubspot/jinjava/mode/PreserveUndefinedExecutionMode.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
* <li>Expressions with undefined variables are preserved: {@code {{ unknown }}} → {@code {{ unknown }}}</li>
1515
* <li>Expressions with defined variables are evaluated: {@code {{ name }}} with {name: "World"} → "World"</li>
1616
* <li>Control structures (if/for) with undefined conditions/iterables are preserved</li>
17-
* <li>Set tags with undefined RHS are preserved</li>
17+
* <li>Set tags are preserved with evaluated RHS: {@code {% set x = name %}}
18+
* with {name: "World"} → {@code {% set x = 'World' %}}</li>
19+
* <li>Macros are executed; undefined variables within macro output are preserved</li>
1820
* <li>Variables explicitly set to null are also preserved</li>
1921
* </ul>
2022
*
2123
* <p>This mode extends {@link EagerExecutionMode} to preserve control structures and tags,
2224
* but uses a custom expression strategy to preserve the original expression syntax
23-
* instead of internal representations.
25+
* instead of internal representations. It enables partial macro evaluation so that
26+
* macros can execute and produce output with undefined parts preserved.
2427
*/
2528
public class PreserveUndefinedExecutionMode extends EagerExecutionMode {
2629

@@ -37,6 +40,7 @@ public void prepareContext(Context context) {
3740
super.prepareContext(context);
3841
context.setExpressionStrategy(new PreserveUndefinedExpressionStrategy());
3942
context.setDynamicVariableResolver(varName -> DeferredValue.instance());
40-
context.setDeferredExecutionMode(true);
43+
context.setPartialMacroEvaluation(true);
44+
context.setPreserveResolvedSetTags(true);
4145
}
4246
}

src/test/java/com/hubspot/jinjava/mode/PreserveUndefinedExecutionModeTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ public void itPreservesSetTagWithKnownRHSValue() {
148148
Map<String, Object> context = new HashMap<>();
149149
context.put("name", "World");
150150
String output = render("{% set x = name %}{{ x }}", context);
151+
// Set tag is preserved with evaluated RHS for multi-pass rendering
151152
assertThat(output).isEqualTo("{% set x = 'World' %}World");
152153
}
153154

@@ -174,4 +175,50 @@ public void itPreservesForTagWithUnknownIterable() {
174175
String output = render("{% for item in items %}{{ item }}{% endfor %}");
175176
assertThat(output).isEqualTo("{% for item in items %}{{ item }}{% endfor %}");
176177
}
178+
179+
@Test
180+
public void itPreservesUndefinedInImportedMacro() {
181+
jinjava.setResourceLocator((fullName, encoding, interpreter) -> {
182+
if (fullName.equals("macros.jinja")) {
183+
return "{% macro greet(name) %}Hello {{ name }}, {{ title }}!{% endmacro %}";
184+
}
185+
return "";
186+
});
187+
188+
String template = "{% import 'macros.jinja' as m %}{{ m.greet('World') }}";
189+
String output = render(template);
190+
assertThat(output).isEqualTo("Hello World, {{ title }}!");
191+
}
192+
193+
@Test
194+
public void itEvaluatesMacroWithAllDefinedVariables() {
195+
jinjava.setResourceLocator((fullName, encoding, interpreter) -> {
196+
if (fullName.equals("macros.jinja")) {
197+
return "{% macro greet(name) %}Hello {{ name }}, {{ title }}!{% endmacro %}";
198+
}
199+
return "";
200+
});
201+
202+
Map<String, Object> context = new HashMap<>();
203+
context.put("title", "Mr");
204+
String template = "{% import 'macros.jinja' as m %}{{ m.greet('World') }}";
205+
String output = render(template, context);
206+
// When all variables are defined, macro fully evaluates
207+
assertThat(output).isEqualTo("Hello World, Mr!");
208+
}
209+
210+
@Test
211+
public void itPreservesUndefinedInFromImportMacro() {
212+
jinjava.setResourceLocator((fullName, encoding, interpreter) -> {
213+
if (fullName.equals("macros.jinja")) {
214+
return "{% macro greet() %}Hello {{ unknown }}!{% endmacro %}";
215+
}
216+
return "";
217+
});
218+
219+
String template = "{% from 'macros.jinja' import greet %}{{ greet() }}";
220+
String output = render(template);
221+
// Macro executes, but undefined variables are preserved
222+
assertThat(output).isEqualTo("Hello {{ unknown }}!");
223+
}
177224
}

0 commit comments

Comments
 (0)