From e2503e45cda92bb4d164242bbe4df99b44a8ea13 Mon Sep 17 00:00:00 2001 From: Zhihao Yao Date: Mon, 23 Mar 2026 13:49:06 -0400 Subject: [PATCH 1/5] Reject empty computed member expressions before returning slices[0] from parse_member_expression_arguments(). --- common/jinja/parser.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/jinja/parser.cpp b/common/jinja/parser.cpp index 4ae4477445b..939ad9ce639 100644 --- a/common/jinja/parser.cpp +++ b/common/jinja/parser.cpp @@ -539,6 +539,9 @@ class parser { 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()) { + throw parser_exception("Empty member expression arguments", source, peek().pos); + } return std::move(slices[0]); } From fbe1669ba3a6399bf6643953d5bacd5c7a29e00c Mon Sep 17 00:00:00 2001 From: Zhihao Yao Date: Sun, 29 Mar 2026 00:09:19 -0400 Subject: [PATCH 2/5] 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` --- common/jinja/parser.cpp | 2 +- common/jinja/runtime.cpp | 4 ++++ common/jinja/runtime.h | 2 +- tests/test-jinja.cpp | 12 ++++++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/common/jinja/parser.cpp b/common/jinja/parser.cpp index 939ad9ce639..020ba718ee3 100644 --- a/common/jinja/parser.cpp +++ b/common/jinja/parser.cpp @@ -540,7 +540,7 @@ class parser { return mk_stmt(start_pos, std::move(start), std::move(stop), std::move(step)); } if (slices.empty()) { - throw parser_exception("Empty member expression arguments", source, peek().pos); + return mk_stmt(start_pos); } return std::move(slices[0]); } diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp index af2282c5469..0bb685fc5b3 100644 --- a/common/jinja/runtime.cpp +++ b/common/jinja/runtime.cpp @@ -746,6 +746,10 @@ value member_expression::execute_impl(context & ctx) { return slice_func->invoke(args); } else { property = this->property->execute(ctx); + if (property->is_undefined()) { + JJ_DEBUG("%s", "Computed member property is undefined, returning undefined"); + return mk_val("object_property"); + } } } else { // syntax: obj.prop diff --git a/common/jinja/runtime.h b/common/jinja/runtime.h index 17a6dff5aa2..c623c95ee16 100644 --- a/common/jinja/runtime.h +++ b/common/jinja/runtime.h @@ -215,7 +215,7 @@ struct continue_statement : public statement { }; // do nothing -struct noop_statement : public statement { +struct noop_statement : public expression { std::string type() const override { return "Noop"; } value execute_impl(context &) override { return mk_val(); diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 1550627bf09..1bf34e50995 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -387,6 +387,18 @@ 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, "array access", "{{ items[1] }}", {{"items", json::array({"a", "b", "c"})}}, From 771c9567c42d109c6e4efb969afac275c9a1aa79 Mon Sep 17 00:00:00 2001 From: Zhihao Yao Date: Sun, 29 Mar 2026 17:40:48 -0400 Subject: [PATCH 3/5] Handle undefined computed member properties Move undefined-property handling to the common member access path, and add a test covering `a[undefined] is undefined`. --- common/jinja/runtime.cpp | 8 ++++---- tests/test-jinja.cpp | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp index 0bb685fc5b3..b2477878758 100644 --- a/common/jinja/runtime.cpp +++ b/common/jinja/runtime.cpp @@ -746,10 +746,6 @@ value member_expression::execute_impl(context & ctx) { return slice_func->invoke(args); } else { property = this->property->execute(ctx); - if (property->is_undefined()) { - JJ_DEBUG("%s", "Computed member property is undefined, returning undefined"); - return mk_val("object_property"); - } } } else { // syntax: obj.prop @@ -773,6 +769,10 @@ 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()); + if (property->is_undefined()) { + JJ_DEBUG("%s", "Member expression property is undefined, returning undefined"); + return mk_val("object_property"); + } ensure_key_type_allowed(property); value val = mk_val("object_property"); diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 1bf34e50995..dc1b49cb2e2 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -399,6 +399,12 @@ static void test_expressions(testing & t) { "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"})}}, From 13946a3b02fc06d5f8202b1a0eb909829a4e86d1 Mon Sep 17 00:00:00 2001 From: "Zhihao \"Zephyr\" Yao" Date: Mon, 30 Mar 2026 08:42:40 -0400 Subject: [PATCH 4/5] Use default undefined value in member access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initialize val and then return it when property is undefined. Co-authored-by: Sigbjørn Skjæret --- common/jinja/runtime.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp index b2477878758..d25fa33e327 100644 --- a/common/jinja/runtime.cpp +++ b/common/jinja/runtime.cpp @@ -769,13 +769,14 @@ 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()); + value val = mk_val("object_property"); + if (property->is_undefined()) { JJ_DEBUG("%s", "Member expression property is undefined, returning undefined"); - return mk_val("object_property"); + return val; } - ensure_key_type_allowed(property); - value val = mk_val("object_property"); + ensure_key_type_allowed(property); if (is_val(object)) { JJ_DEBUG("%s", "Accessing property on undefined object, returning undefined"); From 0643a9c7994a9e8080fb329ae8eff649d4d4050f Mon Sep 17 00:00:00 2001 From: Zhihao Yao Date: Mon, 30 Mar 2026 10:45:39 -0400 Subject: [PATCH 5/5] empty statement parses to blank_expression instead of noop_statement --- common/jinja/parser.cpp | 2 +- common/jinja/runtime.h | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/common/jinja/parser.cpp b/common/jinja/parser.cpp index 020ba718ee3..2b25654a7a0 100644 --- a/common/jinja/parser.cpp +++ b/common/jinja/parser.cpp @@ -540,7 +540,7 @@ class parser { return mk_stmt(start_pos, std::move(start), std::move(stop), std::move(step)); } if (slices.empty()) { - return mk_stmt(start_pos); + return mk_stmt(start_pos); } return std::move(slices[0]); } diff --git a/common/jinja/runtime.h b/common/jinja/runtime.h index c623c95ee16..3ca5f1754fa 100644 --- a/common/jinja/runtime.h +++ b/common/jinja/runtime.h @@ -215,7 +215,7 @@ struct continue_statement : public statement { }; // do nothing -struct noop_statement : public expression { +struct noop_statement : public statement { std::string type() const override { return "Noop"; } value execute_impl(context &) override { return mk_val(); @@ -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;