From 60bafcef853fe7466101454ae42e9c5b4e820531 Mon Sep 17 00:00:00 2001 From: Libo Song Date: Thu, 5 Feb 2026 16:26:26 -0500 Subject: [PATCH] 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 --- .../jinjava/el/ext/AstFilterChain.java | 12 ++++++ .../jinjava/el/ext/ExtendedParser.java | 41 +++++++++++++++++++ .../jinjava/el/ext/AstFilterChainTest.java | 39 ++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AstFilterChain.java b/src/main/java/com/hubspot/jinjava/el/ext/AstFilterChain.java index 30dfc4435..34cae1689 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/AstFilterChain.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/AstFilterChain.java @@ -88,6 +88,18 @@ public Object eval(Bindings bindings, ELContext context) { return null; } if (filter == null) { + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.UNKNOWN, + ErrorItem.FILTER, + String.format("Unknown filter: %s", spec.getName()), + spec.getName(), + interpreter.getLineNumber(), + -1, + null + ) + ); return null; } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java index 543726a7b..164a7f331 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java @@ -24,6 +24,7 @@ import com.google.common.collect.Sets; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; +import com.hubspot.jinjava.interpret.DisabledException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import de.odysseus.el.tree.impl.Builder; import de.odysseus.el.tree.impl.Builder.Feature; @@ -581,9 +582,49 @@ private AstNode parseFiltersAsChain(AstNode left) throws ScanException, ParseExc filterSpecs.add(new FilterSpec(filterName, filterParams)); } while ("|".equals(getToken().getImage())); + if (hasUnknownFilter(filterSpecs)) { + return buildUnoptimizedFromSpecs(left, filterSpecs); + } return createAstFilterChain(left, filterSpecs); } + private boolean hasUnknownFilter(List filterSpecs) { + return JinjavaInterpreter + .getCurrentMaybe() + .map(interp -> { + for (FilterSpec spec : filterSpecs) { + try { + if (interp.getContext().getFilter(spec.getName()) == null) { + return true; + } + } catch (DisabledException e) { + return false; + } + } + return false; + }) + .orElse(false); + } + + private AstNode buildUnoptimizedFromSpecs(AstNode input, List filterSpecs) { + AstNode v = input; + for (FilterSpec spec : filterSpecs) { + List filterParams = Lists.newArrayList(v, interpreter()); + if (spec.hasParams()) { + for (int i = 0; i < spec.getParams().getCardinality(); i++) { + filterParams.add(spec.getParams().getChild(i)); + } + } + AstProperty filterProperty = createAstDot( + identifier(FILTER_PREFIX + spec.getName()), + "filter", + true + ); + v = createAstMethod(filterProperty, createAstParameters(filterParams)); + } + return v; + } + protected AstNode parseFiltersAsNestedMethods(AstNode left) throws ScanException, ParseException { AstNode v = left; diff --git a/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainTest.java b/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainTest.java index f0a0931a3..ae05cec6f 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainTest.java @@ -4,6 +4,9 @@ import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.interpret.RenderResult; +import com.hubspot.jinjava.objects.date.PyishDate; +import java.time.ZonedDateTime; import java.util.HashMap; import java.util.Map; import org.junit.Before; @@ -67,4 +70,40 @@ public void itHandlesFilterWithStringConversion() { String result = jinjava.render("{{ number|string|length }}", context); assertThat(result).isEqualTo("5"); } + + @Test + public void itFallsBackToUnoptimizedForUnknownFilterInChain() { + context.put("module", new PyishDate(ZonedDateTime.parse("2024-01-15T10:30:00Z"))); + RenderResult renderResult = jinjava.renderForResult( + "{% set mid = module | local_dt|unixtimestamp | pprint | md5 %}{{ mid }}", + context + ); + assertThat(renderResult.getOutput()) + .as("Should produce MD5 output since chain continues past unknown filter") + .hasSize(32); + assertThat( + renderResult + .getErrors() + .stream() + .noneMatch(e -> e.getMessage().contains("Unknown filter")) + ) + .as("Should not report 'Unknown filter' error when falling back") + .isTrue(); + } + + @Test + public void itFallsBackToUnoptimizedForUnknownFilterParity() { + String template = "{{ name | unknown_filter | lower | md5 }}"; + Jinjava jinjavaUnoptimized = new Jinjava( + JinjavaConfig.newBuilder().withEnableFilterChainOptimization(false).build() + ); + RenderResult optimizedResult = jinjava.renderForResult(template, context); + RenderResult unoptimizedResult = jinjavaUnoptimized.renderForResult( + template, + context + ); + assertThat(optimizedResult.getOutput()) + .as("Optimized should match un-optimized for unknown filter in chain") + .isEqualTo(unoptimizedResult.getOutput()); + } }