From 2805d4ee21274b8253a4cd87d47ee8f0879951c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigbj=C3=B8rn=20Skj=C3=A6ret?= Date: Tue, 27 Jan 2026 21:16:23 +0100 Subject: [PATCH 1/3] undefined is treated as iterable (string/array) by filters `tojson` is not a supported `undefined` filter --- common/jinja/value.cpp | 90 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp index 2d7706814358..fd1acdfefebe 100644 --- a/common/jinja/value.cpp +++ b/common/jinja/value.cpp @@ -1003,7 +1003,12 @@ const func_builtins & value_none_t::get_builtins() const { static const func_builtins builtins = { {"default", default_value}, {"tojson", tojson}, - {"string", [](const func_args &) -> value { return mk_val("None"); }} + {"string", [](const func_args &) -> value { + return mk_val("None"); + }}, + {"safe", [](const func_args &) -> value { + return mk_val("None"); + }}, }; return builtins; } @@ -1012,9 +1017,86 @@ const func_builtins & value_none_t::get_builtins() const { const func_builtins & value_undefined_t::get_builtins() const { static const func_builtins builtins = { {"default", default_value}, - {"tojson", [](const func_args & args) -> value { - args.ensure_vals(); - return mk_val("null"); + {"capitalize", [](const func_args &) -> value { + return mk_val(); + }}, + {"first", [](const func_args &) -> value { + return mk_val(); + }}, + {"items", [](const func_args &) -> value { + return mk_val(); + }}, + {"join", [](const func_args &) -> value { + return mk_val(); + }}, + {"last", [](const func_args &) -> value { + return mk_val(); + }}, + {"length", [](const func_args &) -> value { + return mk_val(0); + }}, + {"list", [](const func_args &) -> value { + return mk_val(); + }}, + {"lower", [](const func_args &) -> value { + return mk_val(); + }}, + {"map", [](const func_args &) -> value { + return mk_val(); + }}, + {"max", [](const func_args &) -> value { + return mk_val(); + }}, + {"min", [](const func_args &) -> value { + return mk_val(); + }}, + {"reject", [](const func_args &) -> value { + return mk_val(); + }}, + {"rejectattr", [](const func_args &) -> value { + return mk_val(); + }}, + {"replace", [](const func_args &) -> value { + return mk_val(); + }}, + {"reverse", [](const func_args &) -> value { + return mk_val(); + }}, + {"safe", [](const func_args &) -> value { + return mk_val(); + }}, + {"select", [](const func_args &) -> value { + return mk_val(); + }}, + {"selectattr", [](const func_args &) -> value { + return mk_val(); + }}, + {"sort", [](const func_args &) -> value { + return mk_val(); + }}, + {"string", [](const func_args &) -> value { + return mk_val(); + }}, + {"strip", [](const func_args &) -> value { + return mk_val(); + }}, + {"sum", [](const func_args &) -> value { + return mk_val(0); + }}, + {"title", [](const func_args &) -> value { + return mk_val(); + }}, + {"truncate", [](const func_args &) -> value { + return mk_val(); + }}, + {"unique", [](const func_args &) -> value { + return mk_val(); + }}, + {"upper", [](const func_args &) -> value { + return mk_val(); + }}, + {"wordcount", [](const func_args &) -> value { + return mk_val(0); }}, }; return builtins; From 22ab7c8ff627bbb582cac10bf96287906c1262b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigbj=C3=B8rn=20Skj=C3=A6ret?= Date: Tue, 27 Jan 2026 21:16:56 +0100 Subject: [PATCH 2/3] add tests --- tests/test-jinja.cpp | 162 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 7c6eeb311ce4..03b6b87b37e3 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -329,6 +329,12 @@ static void test_loops(testing & t) { "empty" ); + test_template(t, "for undefined empty", + "{% for i in items %}{{ i }}{% else %}empty{% endfor %}", + json::object(), + "empty" + ); + test_template(t, "nested for", "{% for i in a %}{% for j in b %}{{ i }}{{ j }}{% endfor %}{% endfor %}", {{"a", json::array({1, 2})}, {"b", json::array({"x", "y"})}}, @@ -1122,6 +1128,54 @@ static void test_string_methods(testing & t) { {{"s", "banana"}}, "bXnXna" ); + + test_template(t, "undefined|capitalize", + "{{ arr|capitalize }}", + json::object(), + "" + ); + + test_template(t, "undefined|title", + "{{ arr|title }}", + json::object(), + "" + ); + + test_template(t, "undefined|truncate", + "{{ arr|truncate(9) }}", + json::object(), + "" + ); + + test_template(t, "undefined|upper", + "{{ arr|upper }}", + json::object(), + "" + ); + + test_template(t, "undefined|lower", + "{{ arr|lower }}", + json::object(), + "" + ); + + test_template(t, "undefined|replace", + "{{ arr|replace('a', 'b') }}", + json::object(), + "" + ); + + test_template(t, "undefined|trim", + "{{ arr|trim }}", + json::object(), + "" + ); + + test_template(t, "undefined|wordcount", + "{{ arr|wordcount }}", + json::object(), + "0" + ); } static void test_array_methods(testing & t) { @@ -1289,6 +1343,108 @@ static void test_array_methods(testing & t) { // {{"arr", json::array({"a", "b", "c"})}}, // "a,x,b,c" // ); + + test_template(t, "undefined|select", + "{% for item in items|select('odd') %}{{ item.name }} {% endfor %}", + json::object(), + "" + ); + + test_template(t, "undefined|selectattr", + "{% for item in items|selectattr('active') %}{{ item.name }} {% endfor %}", + json::object(), + "" + ); + + test_template(t, "undefined|reject", + "{% for item in items|reject('even') %}{{ item.name }} {% endfor %}", + json::object(), + "" + ); + + test_template(t, "undefined|rejectattr", + "{% for item in items|rejectattr('active') %}{{ item.name }} {% endfor %}", + json::object(), + "" + ); + + test_template(t, "undefined|list", + "{{ arr|list|string }}", + json::object(), + "[]" + ); + + test_template(t, "undefined|string", + "{{ arr|string }}", + json::object(), + "" + ); + + test_template(t, "undefined|first", + "{{ arr|first }}", + json::object(), + "" + ); + + test_template(t, "undefined|last", + "{{ arr|last }}", + json::object(), + "" + ); + + test_template(t, "undefined|length", + "{{ arr|length }}", + json::object(), + "0" + ); + + test_template(t, "undefined|join", + "{{ arr|join }}", + json::object(), + "" + ); + + test_template(t, "undefined|sort", + "{{ arr|sort|string }}", + json::object(), + "[]" + ); + + test_template(t, "undefined|reverse", + "{{ arr|reverse|join }}", + json::object(), + "" + ); + + test_template(t, "undefined|map", + "{% for v in arr|map(attribute='age') %}{{ v }} {% endfor %}", + json::object(), + "" + ); + + test_template(t, "undefined|min", + "{{ arr|min }}", + json::object(), + "" + ); + + test_template(t, "undefined|max", + "{{ arr|max }}", + json::object(), + "" + ); + + test_template(t, "undefined|unique", + "{{ arr|unique|join }}", + json::object(), + "" + ); + + test_template(t, "undefined|sum", + "{{ arr|sum }}", + json::object(), + "0" + ); } static void test_object_methods(testing & t) { @@ -1393,6 +1549,12 @@ static void test_object_methods(testing & t) { json::object(), "True" ); + + test_template(t, "undefined|items", + "{{ arr|items|join }}", + json::object(), + "" + ); } static void test_hasher(testing & t) { From 69646c188c4a922b0e55461fcf36d4812b40e678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigbj=C3=B8rn=20Skj=C3=A6ret?= Date: Wed, 28 Jan 2026 09:38:29 +0100 Subject: [PATCH 3/3] add sequence and iterable tests keep it DRY and fix some types --- common/jinja/value.cpp | 131 +++++++++++++++-------------------------- tests/test-jinja.cpp | 12 ++++ 2 files changed, 60 insertions(+), 83 deletions(-) diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp index fd1acdfefebe..17d43826c0b3 100644 --- a/common/jinja/value.cpp +++ b/common/jinja/value.cpp @@ -114,6 +114,18 @@ static T slice(const T & array, int64_t start, int64_t stop, int64_t step = 1) { return result; } +template +static value empty_value_fn(const func_args &) { + if constexpr (std::is_same_v) { + return mk_val(0); + } else if constexpr (std::is_same_v) { + return mk_val(0.0); + } else if constexpr (std::is_same_v) { + return mk_val(false); + } else { + return mk_val(); + } +} template static value test_type_fn(const func_args & args) { args.ensure_count(1); @@ -128,6 +140,13 @@ static value test_type_fn(const func_args & args) { JJ_DEBUG("test_type_fn: type=%s or %s result=%d", typeid(T).name(), typeid(U).name(), is_type ? 1 : 0); return mk_val(is_type); } +template +static value test_type_fn(const func_args & args) { + args.ensure_count(1); + bool is_type = is_val(args.get_pos(0)) || is_val(args.get_pos(0)) || is_val(args.get_pos(0)); + JJ_DEBUG("test_type_fn: type=%s, %s or %s result=%d", typeid(T).name(), typeid(U).name(), typeid(V).name(), is_type ? 1 : 0); + return mk_val(is_type); +} template static value test_compare_fn(const func_args & args) { args.ensure_count(2, 2); @@ -347,8 +366,8 @@ const func_builtins & global_builtins() { {"test_is_integer", test_type_fn}, {"test_is_float", test_type_fn}, {"test_is_number", test_type_fn}, - {"test_is_iterable", test_type_fn}, - {"test_is_sequence", test_type_fn}, + {"test_is_iterable", test_type_fn}, + {"test_is_sequence", test_type_fn}, {"test_is_mapping", test_type_fn}, {"test_is_lower", [](const func_args & args) -> value { args.ensure_vals(); @@ -1017,87 +1036,33 @@ const func_builtins & value_none_t::get_builtins() const { const func_builtins & value_undefined_t::get_builtins() const { static const func_builtins builtins = { {"default", default_value}, - {"capitalize", [](const func_args &) -> value { - return mk_val(); - }}, - {"first", [](const func_args &) -> value { - return mk_val(); - }}, - {"items", [](const func_args &) -> value { - return mk_val(); - }}, - {"join", [](const func_args &) -> value { - return mk_val(); - }}, - {"last", [](const func_args &) -> value { - return mk_val(); - }}, - {"length", [](const func_args &) -> value { - return mk_val(0); - }}, - {"list", [](const func_args &) -> value { - return mk_val(); - }}, - {"lower", [](const func_args &) -> value { - return mk_val(); - }}, - {"map", [](const func_args &) -> value { - return mk_val(); - }}, - {"max", [](const func_args &) -> value { - return mk_val(); - }}, - {"min", [](const func_args &) -> value { - return mk_val(); - }}, - {"reject", [](const func_args &) -> value { - return mk_val(); - }}, - {"rejectattr", [](const func_args &) -> value { - return mk_val(); - }}, - {"replace", [](const func_args &) -> value { - return mk_val(); - }}, - {"reverse", [](const func_args &) -> value { - return mk_val(); - }}, - {"safe", [](const func_args &) -> value { - return mk_val(); - }}, - {"select", [](const func_args &) -> value { - return mk_val(); - }}, - {"selectattr", [](const func_args &) -> value { - return mk_val(); - }}, - {"sort", [](const func_args &) -> value { - return mk_val(); - }}, - {"string", [](const func_args &) -> value { - return mk_val(); - }}, - {"strip", [](const func_args &) -> value { - return mk_val(); - }}, - {"sum", [](const func_args &) -> value { - return mk_val(0); - }}, - {"title", [](const func_args &) -> value { - return mk_val(); - }}, - {"truncate", [](const func_args &) -> value { - return mk_val(); - }}, - {"unique", [](const func_args &) -> value { - return mk_val(); - }}, - {"upper", [](const func_args &) -> value { - return mk_val(); - }}, - {"wordcount", [](const func_args &) -> value { - return mk_val(0); - }}, + {"capitalize", empty_value_fn}, + {"first", empty_value_fn}, + {"items", empty_value_fn}, + {"join", empty_value_fn}, + {"last", empty_value_fn}, + {"length", empty_value_fn}, + {"list", empty_value_fn}, + {"lower", empty_value_fn}, + {"map", empty_value_fn}, + {"max", empty_value_fn}, + {"min", empty_value_fn}, + {"reject", empty_value_fn}, + {"rejectattr", empty_value_fn}, + {"replace", empty_value_fn}, + {"reverse", empty_value_fn}, + {"safe", empty_value_fn}, + {"select", empty_value_fn}, + {"selectattr", empty_value_fn}, + {"sort", empty_value_fn}, + {"string", empty_value_fn}, + {"strip", empty_value_fn}, + {"sum", empty_value_fn}, + {"title", empty_value_fn}, + {"truncate", empty_value_fn}, + {"unique", empty_value_fn}, + {"upper", empty_value_fn}, + {"wordcount", empty_value_fn}, }; return builtins; } diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 03b6b87b37e3..f6114f1e2f2e 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -1024,6 +1024,18 @@ static void test_tests(testing & t) { {{"x", {{"a", 1}}}}, "yes" ); + + test_template(t, "undefined is sequence", + "{{ 'yes' if x is sequence }}", + json::object(), + "yes" + ); + + test_template(t, "undefined is iterable", + "{{ 'yes' if x is iterable }}", + json::object(), + "yes" + ); } static void test_string_methods(testing & t) {