Skip to content

Commit 60bafce

Browse files
hs-lsongclaude
andcommitted
Fall back to un-optimized filter chain for unknown filters
When AstFilterChain optimization is enabled and a filter chain contains an unknown filter (e.g. local_dt), fall back to the standard nested AstMethod evaluation at parse time instead of failing with an "Unknown filter" error and returning null. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5db2a86 commit 60bafce

File tree

3 files changed

+92
-0
lines changed

3 files changed

+92
-0
lines changed

src/main/java/com/hubspot/jinjava/el/ext/AstFilterChain.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ public Object eval(Bindings bindings, ELContext context) {
8888
return null;
8989
}
9090
if (filter == null) {
91+
interpreter.addError(
92+
new TemplateError(
93+
ErrorType.WARNING,
94+
ErrorReason.UNKNOWN,
95+
ErrorItem.FILTER,
96+
String.format("Unknown filter: %s", spec.getName()),
97+
spec.getName(),
98+
interpreter.getLineNumber(),
99+
-1,
100+
null
101+
)
102+
);
91103
return null;
92104
}
93105

src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.common.collect.Sets;
2525
import com.hubspot.jinjava.JinjavaConfig;
2626
import com.hubspot.jinjava.LegacyOverrides;
27+
import com.hubspot.jinjava.interpret.DisabledException;
2728
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
2829
import de.odysseus.el.tree.impl.Builder;
2930
import de.odysseus.el.tree.impl.Builder.Feature;
@@ -581,9 +582,49 @@ private AstNode parseFiltersAsChain(AstNode left) throws ScanException, ParseExc
581582
filterSpecs.add(new FilterSpec(filterName, filterParams));
582583
} while ("|".equals(getToken().getImage()));
583584

585+
if (hasUnknownFilter(filterSpecs)) {
586+
return buildUnoptimizedFromSpecs(left, filterSpecs);
587+
}
584588
return createAstFilterChain(left, filterSpecs);
585589
}
586590

591+
private boolean hasUnknownFilter(List<FilterSpec> filterSpecs) {
592+
return JinjavaInterpreter
593+
.getCurrentMaybe()
594+
.map(interp -> {
595+
for (FilterSpec spec : filterSpecs) {
596+
try {
597+
if (interp.getContext().getFilter(spec.getName()) == null) {
598+
return true;
599+
}
600+
} catch (DisabledException e) {
601+
return false;
602+
}
603+
}
604+
return false;
605+
})
606+
.orElse(false);
607+
}
608+
609+
private AstNode buildUnoptimizedFromSpecs(AstNode input, List<FilterSpec> filterSpecs) {
610+
AstNode v = input;
611+
for (FilterSpec spec : filterSpecs) {
612+
List<AstNode> filterParams = Lists.newArrayList(v, interpreter());
613+
if (spec.hasParams()) {
614+
for (int i = 0; i < spec.getParams().getCardinality(); i++) {
615+
filterParams.add(spec.getParams().getChild(i));
616+
}
617+
}
618+
AstProperty filterProperty = createAstDot(
619+
identifier(FILTER_PREFIX + spec.getName()),
620+
"filter",
621+
true
622+
);
623+
v = createAstMethod(filterProperty, createAstParameters(filterParams));
624+
}
625+
return v;
626+
}
627+
587628
protected AstNode parseFiltersAsNestedMethods(AstNode left)
588629
throws ScanException, ParseException {
589630
AstNode v = left;

src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
import com.hubspot.jinjava.Jinjava;
66
import com.hubspot.jinjava.JinjavaConfig;
7+
import com.hubspot.jinjava.interpret.RenderResult;
8+
import com.hubspot.jinjava.objects.date.PyishDate;
9+
import java.time.ZonedDateTime;
710
import java.util.HashMap;
811
import java.util.Map;
912
import org.junit.Before;
@@ -67,4 +70,40 @@ public void itHandlesFilterWithStringConversion() {
6770
String result = jinjava.render("{{ number|string|length }}", context);
6871
assertThat(result).isEqualTo("5");
6972
}
73+
74+
@Test
75+
public void itFallsBackToUnoptimizedForUnknownFilterInChain() {
76+
context.put("module", new PyishDate(ZonedDateTime.parse("2024-01-15T10:30:00Z")));
77+
RenderResult renderResult = jinjava.renderForResult(
78+
"{% set mid = module | local_dt|unixtimestamp | pprint | md5 %}{{ mid }}",
79+
context
80+
);
81+
assertThat(renderResult.getOutput())
82+
.as("Should produce MD5 output since chain continues past unknown filter")
83+
.hasSize(32);
84+
assertThat(
85+
renderResult
86+
.getErrors()
87+
.stream()
88+
.noneMatch(e -> e.getMessage().contains("Unknown filter"))
89+
)
90+
.as("Should not report 'Unknown filter' error when falling back")
91+
.isTrue();
92+
}
93+
94+
@Test
95+
public void itFallsBackToUnoptimizedForUnknownFilterParity() {
96+
String template = "{{ name | unknown_filter | lower | md5 }}";
97+
Jinjava jinjavaUnoptimized = new Jinjava(
98+
JinjavaConfig.newBuilder().withEnableFilterChainOptimization(false).build()
99+
);
100+
RenderResult optimizedResult = jinjava.renderForResult(template, context);
101+
RenderResult unoptimizedResult = jinjavaUnoptimized.renderForResult(
102+
template,
103+
context
104+
);
105+
assertThat(optimizedResult.getOutput())
106+
.as("Optimized should match un-optimized for unknown filter in chain")
107+
.isEqualTo(unoptimizedResult.getOutput());
108+
}
70109
}

0 commit comments

Comments
 (0)