jinja : undefined should be treated as sequence/iterable (return string/array) by filters/tests (#19147)

* undefined is treated as iterable (string/array) by filters

`tojson` is not a supported `undefined` filter

* add tests

* add sequence and iterable tests

keep it DRY and fix some types
This commit is contained in:
Sigbjørn Skjæret 2026-01-28 14:40:29 +01:00 committed by GitHub
parent 88d23ad515
commit 60368e1d73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 228 additions and 7 deletions

View File

@ -114,6 +114,18 @@ static T slice(const T & array, int64_t start, int64_t stop, int64_t step = 1) {
return result;
}
template<typename T>
static value empty_value_fn(const func_args &) {
if constexpr (std::is_same_v<T, value_int>) {
return mk_val<T>(0);
} else if constexpr (std::is_same_v<T, value_float>) {
return mk_val<T>(0.0);
} else if constexpr (std::is_same_v<T, value_bool>) {
return mk_val<T>(false);
} else {
return mk_val<T>();
}
}
template<typename T>
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<value_bool>(is_type);
}
template<typename T, typename U, typename V>
static value test_type_fn(const func_args & args) {
args.ensure_count(1);
bool is_type = is_val<T>(args.get_pos(0)) || is_val<U>(args.get_pos(0)) || is_val<V>(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<value_bool>(is_type);
}
template<value_compare_op op>
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<value_int>},
{"test_is_float", test_type_fn<value_float>},
{"test_is_number", test_type_fn<value_int, value_float>},
{"test_is_iterable", test_type_fn<value_array, value_string>},
{"test_is_sequence", test_type_fn<value_array, value_string>},
{"test_is_iterable", test_type_fn<value_array, value_string, value_undefined>},
{"test_is_sequence", test_type_fn<value_array, value_string, value_undefined>},
{"test_is_mapping", test_type_fn<value_object>},
{"test_is_lower", [](const func_args & args) -> value {
args.ensure_vals<value_string>();
@ -1003,7 +1022,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<value_string>("None"); }}
{"string", [](const func_args &) -> value {
return mk_val<value_string>("None");
}},
{"safe", [](const func_args &) -> value {
return mk_val<value_string>("None");
}},
};
return builtins;
}
@ -1012,10 +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},
{"tojson", [](const func_args & args) -> value {
args.ensure_vals<value_undefined>();
return mk_val<value_string>("null");
}},
{"capitalize", empty_value_fn<value_string>},
{"first", empty_value_fn<value_undefined>},
{"items", empty_value_fn<value_array>},
{"join", empty_value_fn<value_string>},
{"last", empty_value_fn<value_undefined>},
{"length", empty_value_fn<value_int>},
{"list", empty_value_fn<value_array>},
{"lower", empty_value_fn<value_string>},
{"map", empty_value_fn<value_array>},
{"max", empty_value_fn<value_undefined>},
{"min", empty_value_fn<value_undefined>},
{"reject", empty_value_fn<value_array>},
{"rejectattr", empty_value_fn<value_array>},
{"replace", empty_value_fn<value_string>},
{"reverse", empty_value_fn<value_array>},
{"safe", empty_value_fn<value_string>},
{"select", empty_value_fn<value_array>},
{"selectattr", empty_value_fn<value_array>},
{"sort", empty_value_fn<value_array>},
{"string", empty_value_fn<value_string>},
{"strip", empty_value_fn<value_string>},
{"sum", empty_value_fn<value_int>},
{"title", empty_value_fn<value_string>},
{"truncate", empty_value_fn<value_string>},
{"unique", empty_value_fn<value_array>},
{"upper", empty_value_fn<value_string>},
{"wordcount", empty_value_fn<value_int>},
};
return builtins;
}

View File

@ -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"})}},
@ -1018,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) {
@ -1122,6 +1140,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 +1355,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 +1561,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) {