From d816d61e3e5e31784b678bb5757d005f7ae765e7 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 24 Oct 2025 13:48:08 -0400 Subject: [PATCH] Retain both the original deferred words and their bases, and expose them with separate methods --- .../hubspot/jinjava/lib/fn/MacroFunction.java | 7 +- .../jinjava/lib/tag/eager/DeferredToken.java | 104 ++++++++++-------- .../util/EagerReconstructionUtils.java | 6 +- .../lib/tag/eager/EagerSetTagTest.java | 16 +++ .../jinjava/util/DeferredValueUtilsTest.java | 8 +- 5 files changed, 84 insertions(+), 57 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java index 39f2cfd5c..8b51cbc47 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java @@ -237,11 +237,8 @@ private boolean alreadyDeferredInEarlierCall( Objects.equals(importResourcePath, deferredToken.getImportResourcePath()) ) .anyMatch(deferredToken -> - deferredToken.getSetDeferredWords().contains(key) || - deferredToken - .getUsedDeferredWords() - .stream() - .anyMatch(used -> key.equals(used.split("\\.", 2)[0])) + deferredToken.getSetDeferredBases().contains(key) || + deferredToken.getUsedDeferredBases().contains(key) ); } return false; 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 31dfee7ac..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 @@ -2,6 +2,7 @@ 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; @@ -31,34 +32,18 @@ public class DeferredToken { public static class DeferredTokenBuilder { private final Token token; - private Stream usedDeferredWords; - private Stream setDeferredWords; + private ImmutableSet.Builder usedDeferredWords; + private ImmutableSet.Builder setDeferredWords; private DeferredTokenBuilder(Token token) { this.token = token; } public DeferredToken build() { - JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); return new DeferredToken( token, - usedDeferredWords != null - ? usedDeferredWords - .map(DeferredToken::splitToken) - .map(DeferredToken::getFirstNonEmptyToken) - .distinct() - .filter(word -> - interpreter == null || - !(interpreter.getContext().get(word) instanceof DeferredMacroValueImpl) - ) - .collect(Collectors.toSet()) - : Collections.emptySet(), - setDeferredWords != null - ? setDeferredWords - .map(DeferredToken::splitToken) - .map(DeferredToken::getFirstNonEmptyToken) - .collect(Collectors.toSet()) - : Collections.emptySet(), + usedDeferredWords != null ? usedDeferredWords.build() : Collections.emptySet(), + setDeferredWords != null ? setDeferredWords.build() : Collections.emptySet(), acquireImportResourcePath(), acquireMacroStack() ); @@ -67,34 +52,40 @@ public DeferredToken build() { public DeferredTokenBuilder addUsedDeferredWords( Collection usedDeferredWordsToAdd ) { - return addUsedDeferredWords(usedDeferredWordsToAdd.stream()); + if (usedDeferredWords == null) { + usedDeferredWords = ImmutableSet.builder(); + } + usedDeferredWords.addAll(usedDeferredWordsToAdd); + return this; } public DeferredTokenBuilder addUsedDeferredWords( Stream usedDeferredWordsToAdd ) { if (usedDeferredWords == null) { - usedDeferredWords = usedDeferredWordsToAdd; - } else { - usedDeferredWords = Stream.concat(usedDeferredWords, usedDeferredWordsToAdd); + usedDeferredWords = ImmutableSet.builder(); } + usedDeferredWordsToAdd.forEach(usedDeferredWords::add); return this; } public DeferredTokenBuilder addSetDeferredWords( Collection setDeferredWordsToAdd ) { - return addSetDeferredWords(setDeferredWordsToAdd.stream()); + if (setDeferredWords == null) { + setDeferredWords = ImmutableSet.builder(); + } + setDeferredWords.addAll(setDeferredWordsToAdd); + return this; } public DeferredTokenBuilder addSetDeferredWords( Stream setDeferredWordsToAdd ) { if (setDeferredWords == null) { - setDeferredWords = setDeferredWordsToAdd; - } else { - setDeferredWords = Stream.concat(setDeferredWords, setDeferredWordsToAdd); + setDeferredWords = ImmutableSet.builder(); } + setDeferredWordsToAdd.forEach(setDeferredWords::add); return this; } } @@ -103,9 +94,10 @@ public DeferredTokenBuilder addSetDeferredWords( // 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; @@ -211,8 +203,8 @@ public DeferredToken( ) { this( token, - getBases(usedDeferredWords), - getBases(setDeferredWords), + usedDeferredWords, + setDeferredWords, acquireImportResourcePath(), acquireMacroStack() ); @@ -220,14 +212,36 @@ public DeferredToken( private DeferredToken( Token token, - Set usedDeferredWordBases, - Set setDeferredWordBases, + Set usedDeferredWords, + Set setDeferredWords, String importResourcePath, CallStack macroStack ) { + JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent(); this.token = token; - this.usedDeferredWords = usedDeferredWordBases; - this.setDeferredWords = setDeferredWordBases; + 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; this.importResourcePath = importResourcePath; this.macroStack = macroStack; } @@ -240,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; } @@ -255,7 +277,7 @@ public CallStack getMacroStack() { public void addTo(Context context) { addTo( context, - usedDeferredWords + usedDeferredBases .stream() .filter(word -> { Object value = context.get(word); @@ -285,7 +307,7 @@ private void deferPropertiesOnContext( ) { if (isInSameScope(context)) { // set props are only deferred when within the scope which the variable is set in - markDeferredWordsAndFindSources(context, getSetDeferredWords(), true); + markDeferredWordsAndFindSources(context, getSetDeferredBases(), true); } wordsWithoutDeferredSource.forEach(word -> deferDuplicatePointers(context, word)); wordsWithoutDeferredSource.removeAll( @@ -424,12 +446,4 @@ private static String getFirstNonEmptyToken(List strings) { public static List splitToken(String token) { return Arrays.asList(token.split("\\.")); } - - public static Set getBases(Set original) { - return original - .stream() - .map(DeferredToken::splitToken) - .map(prop -> prop.get(0)) - .collect(Collectors.toSet()); - } } diff --git a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java index 85925b291..385fed453 100644 --- a/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/EagerReconstructionUtils.java @@ -795,13 +795,13 @@ public static Map handleDeferredTokenAndReconstructReferences( deferredToken.addTo(interpreter.getContext()); return reconstructDeferredReferences( interpreter, - deferredToken.getUsedDeferredWords() + deferredToken.getUsedDeferredBases() ); } public static Map reconstructDeferredReferences( JinjavaInterpreter interpreter, - Set usedDeferredWords + Set usedDeferredBases ) { return interpreter .getContext() @@ -815,7 +815,7 @@ public static Map reconstructDeferredReferences( .filter(entry -> // Always reconstruct the DeferredLazyReferenceSource, but only reconstruct DeferredLazyReferences when they are used entry.getValue() instanceof DeferredLazyReferenceSource || - usedDeferredWords.contains(entry.getKey()) + usedDeferredBases.contains(entry.getKey()) ) .peek(entry -> ((OneTimeReconstructible) entry.getValue()).setReconstructed(true)) .map(entry -> 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 b3a39fc9a..d86055ff2 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 @@ -159,6 +159,14 @@ public void itDefersBlockWithFilter() { .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"); } @@ -211,6 +219,14 @@ public void itDefersInDeferredExecutionModeWithFilter() { .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"); diff --git a/src/test/java/com/hubspot/jinjava/util/DeferredValueUtilsTest.java b/src/test/java/com/hubspot/jinjava/util/DeferredValueUtilsTest.java index 6303a4740..735d84361 100644 --- a/src/test/java/com/hubspot/jinjava/util/DeferredValueUtilsTest.java +++ b/src/test/java/com/hubspot/jinjava/util/DeferredValueUtilsTest.java @@ -256,9 +256,9 @@ public void itFindsFirstValidDeferredWords() { .addSetDeferredWords(ImmutableSet.of("deferred", ".attribute2")) .build(); - assertThat(deferredToken.getUsedDeferredWords()) + assertThat(deferredToken.getUsedDeferredBases()) .isEqualTo(ImmutableSet.of("deferred", "attribute1")); - assertThat(deferredToken.getSetDeferredWords()) + assertThat(deferredToken.getSetDeferredBases()) .isEqualTo(ImmutableSet.of("deferred", "attribute2")); } @@ -272,9 +272,9 @@ public void itFindsFirstValidDeferredWordsWithNestedAttributes() { .addSetDeferredWords(ImmutableSet.of("deferred", ".attribute2.ignoreme")) .build(); - assertThat(deferredToken.getUsedDeferredWords()) + assertThat(deferredToken.getUsedDeferredBases()) .isEqualTo(ImmutableSet.of("deferred", "attribute1")); - assertThat(deferredToken.getSetDeferredWords()) + assertThat(deferredToken.getSetDeferredBases()) .isEqualTo(ImmutableSet.of("deferred", "attribute2")); }