From ead417f01ce9769249e26fd1aaf700de83003a5c Mon Sep 17 00:00:00 2001 From: "Zhihao \"Zephyr\" Yao" Date: Mon, 30 Mar 2026 14:08:46 -0400 Subject: [PATCH] jinja : handle empty expressions correctly (#20913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Reject empty computed member expressions before returning slices[0] from parse_member_expression_arguments(). * Treat empty computed member expressions with Jinja2 undefined semantics Treat empty computed member expressions like `a[]` as undefined instead of raising a parser error, to match Jinja2 behavior. - return a noop expression for empty computed member arguments - return undefined when a computed member key evaluates to undefined - add Jinja tests covering `a[]|default('fallback')` and `a[] is undefined` * Handle undefined computed member properties Move undefined-property handling to the common member access path, and add a test covering `a[undefined] is undefined`. * Use default undefined value in member access Initialize val and then return it when property is undefined. Co-authored-by: Sigbjørn Skjæret * empty statement parses to blank_expression instead of noop_statement --------- Co-authored-by: Sigbjørn Skjæret --- common/jinja/parser.cpp | 3 +++ common/jinja/runtime.cpp | 9 +++++++-- common/jinja/runtime.h | 8 ++++++++ tests/test-jinja.cpp | 18 ++++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/common/jinja/parser.cpp b/common/jinja/parser.cpp index 4ae4477445..2b25654a7a 100644 --- a/common/jinja/parser.cpp +++ b/common/jinja/parser.cpp @@ -539,6 +539,9 @@ private: statement_ptr step = slices.size() > 2 ? std::move(slices[2]) : nullptr; return mk_stmt(start_pos, std::move(start), std::move(stop), std::move(step)); } + if (slices.empty()) { + return mk_stmt(start_pos); + } return std::move(slices[0]); } diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp index dce5bbae30..2232790c31 100644 --- a/common/jinja/runtime.cpp +++ b/common/jinja/runtime.cpp @@ -771,10 +771,15 @@ value member_expression::execute_impl(context & ctx) { } JJ_DEBUG("Member expression on object type %s, property type %s", object->type().c_str(), property->type().c_str()); - ensure_key_type_allowed(property); - value val = mk_val("object_property"); + if (property->is_undefined()) { + JJ_DEBUG("%s", "Member expression property is undefined, returning undefined"); + return val; + } + + ensure_key_type_allowed(property); + if (is_val(object)) { JJ_DEBUG("%s", "Accessing property on undefined object, returning undefined"); return val; diff --git a/common/jinja/runtime.h b/common/jinja/runtime.h index 17a6dff5aa..3ca5f1754f 100644 --- a/common/jinja/runtime.h +++ b/common/jinja/runtime.h @@ -263,6 +263,14 @@ struct comment_statement : public statement { // Expressions +// Represents an omitted expression in a computed member, e.g. `a[]`. +struct blank_expression : public expression { + std::string type() const override { return "BlankExpression"; } + value execute_impl(context &) override { + return mk_val(); + } +}; + struct member_expression : public expression { statement_ptr object; statement_ptr property; diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 2cac38f02a..5d4b2806ac 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -387,6 +387,24 @@ static void test_expressions(testing & t) { "Bob" ); + test_template(t, "empty computed member defaults to undefined", + "{{ a[]|default('fallback') }}", + {{"a", {{"name", "Bob"}}}}, + "fallback" + ); + + test_template(t, "empty computed member is undefined", + "{{ a[] is undefined }}", + {{"a", {{"name", "Bob"}}}}, + "True" + ); + + test_template(t, "undefined computed member is undefined", + "{{ a[undefined] is undefined }}", + {{"a", {{"name", "Bob"}}}}, + "True" + ); + test_template(t, "array access", "{{ items[1] }}", {{"items", json::array({"a", "b", "c"})}},