mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-12 23:40:45 -07:00
Add strict2_parse to assign and capture tags
Both tags previously used only regex (VariableSignature) to validate variable names, which allowed invalid identifiers like (a(b(c) and [x.y] in all parse modes. - assign: strict2_parse uses Parser to validate the LHS as a valid identifier before delegating RHS to Variable - capture: strict2_parse uses Parser to validate the variable name as a valid identifier - Both tags now include ParserSwitching and dispatch through strict_parse_with_error_mode_fallback - Lax mode is unchanged — invalid names are still accepted Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
346166b600
commit
0d5c15a03e
@@ -18,6 +18,8 @@ module Liquid
|
|||||||
# @liquid_syntax_keyword variable_name The name of the variable being created.
|
# @liquid_syntax_keyword variable_name The name of the variable being created.
|
||||||
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
||||||
class Assign < Tag
|
class Assign < Tag
|
||||||
|
include ParserSwitching
|
||||||
|
|
||||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||||
|
|
||||||
# @api private
|
# @api private
|
||||||
@@ -29,6 +31,10 @@ module Liquid
|
|||||||
|
|
||||||
def initialize(tag_name, markup, parse_context)
|
def initialize(tag_name, markup, parse_context)
|
||||||
super
|
super
|
||||||
|
parse_with_selected_parser(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lax_parse(markup)
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@to = Regexp.last_match(1)
|
@to = Regexp.last_match(1)
|
||||||
@from = Variable.new(Regexp.last_match(2), parse_context)
|
@from = Variable.new(Regexp.last_match(2), parse_context)
|
||||||
@@ -37,6 +43,25 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def strict_parse(markup)
|
||||||
|
lax_parse(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
def strict2_parse(markup)
|
||||||
|
unless markup =~ Syntax
|
||||||
|
self.class.raise_syntax_error(parse_context)
|
||||||
|
end
|
||||||
|
|
||||||
|
lhs = Regexp.last_match(1).strip
|
||||||
|
rhs = Regexp.last_match(2)
|
||||||
|
|
||||||
|
p = @parse_context.new_parser(lhs)
|
||||||
|
@to = p.consume(:id)
|
||||||
|
p.consume(:end_of_string)
|
||||||
|
|
||||||
|
@from = Variable.new(rhs, parse_context)
|
||||||
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
val = @from.render(context)
|
val = @from.render(context)
|
||||||
context.scopes.last[@to] = val
|
context.scopes.last[@to] = val
|
||||||
|
|||||||
@@ -20,10 +20,18 @@ module Liquid
|
|||||||
# @liquid_syntax_keyword variable The name of the variable being created.
|
# @liquid_syntax_keyword variable The name of the variable being created.
|
||||||
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
||||||
class Capture < Block
|
class Capture < Block
|
||||||
|
include ParserSwitching
|
||||||
|
|
||||||
Syntax = /(#{VariableSignature}+)/o
|
Syntax = /(#{VariableSignature}+)/o
|
||||||
|
|
||||||
|
attr_reader :to
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
|
parse_with_selected_parser(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lax_parse(markup)
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@to = Regexp.last_match(1)
|
@to = Regexp.last_match(1)
|
||||||
else
|
else
|
||||||
@@ -31,6 +39,16 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def strict_parse(markup)
|
||||||
|
lax_parse(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
def strict2_parse(markup)
|
||||||
|
p = @parse_context.new_parser(markup.strip)
|
||||||
|
@to = p.consume(:id)
|
||||||
|
p.consume(:end_of_string)
|
||||||
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
context.resource_limits.with_capture do
|
context.resource_limits.with_capture do
|
||||||
capture_output = render(context)
|
capture_output = render(context)
|
||||||
|
|||||||
@@ -97,6 +97,46 @@ class AssignTest < Minitest::Test
|
|||||||
assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd'))
|
assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_assign_with_valid_identifier_in_strict2
|
||||||
|
assert_template_result("hello", "{% assign my_var = 'hello' %}{{ my_var }}", error_mode: :strict2)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_assign_with_hyphen_in_strict2
|
||||||
|
assert_template_result("hello", "{% assign my-var = 'hello' %}{{ my-var }}", error_mode: :strict2)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_assign_rejects_parentheses_in_variable_name_in_strict2
|
||||||
|
assert_raises(Liquid::SyntaxError) do
|
||||||
|
Liquid::Template.parse("{% assign (a(b(c) = 1234 %}", error_mode: :strict2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_assign_rejects_brackets_in_variable_name_in_strict2
|
||||||
|
assert_raises(Liquid::SyntaxError) do
|
||||||
|
Liquid::Template.parse("{% assign [x.y] = 'hello' %}", error_mode: :strict2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_assign_rejects_dot_in_variable_name_in_strict2
|
||||||
|
assert_raises(Liquid::SyntaxError) do
|
||||||
|
Liquid::Template.parse("{% assign a.b = 'hello' %}", error_mode: :strict2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_assign_rejects_numeric_variable_name_in_strict2
|
||||||
|
assert_raises(Liquid::SyntaxError) do
|
||||||
|
Liquid::Template.parse("{% assign 1abc = 'hello' %}", error_mode: :strict2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_assign_allows_invalid_names_in_lax
|
||||||
|
assert_template_result("1234", "{% assign (a(b(c) = 1234 %}{{ self['(a(b(c)'] }}", error_mode: :lax)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_assign_with_filter_in_strict2
|
||||||
|
assert_template_result("HELLO", "{% assign my_var = 'hello' | upcase %}{{ my_var }}", error_mode: :strict2)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
class ObjectWrapperDrop < Liquid::Drop
|
class ObjectWrapperDrop < Liquid::Drop
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ class CaptureTest < Minitest::Test
|
|||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
def test_captures_block_content_in_variable
|
def test_captures_block_content_in_variable
|
||||||
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
|
assert_template_result("test string", "{% capture var %}test string{% endcapture %}{{var}}", {})
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_captures_block_content_in_quoted_variable_in_lax
|
||||||
|
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {}, error_mode: :lax)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_capture_with_hyphen_in_variable_name
|
def test_capture_with_hyphen_in_variable_name
|
||||||
@@ -49,4 +53,35 @@ class CaptureTest < Minitest::Test
|
|||||||
t.render!
|
t.render!
|
||||||
assert_equal(9, t.resource_limits.assign_score)
|
assert_equal(9, t.resource_limits.assign_score)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_capture_with_valid_identifier_in_strict2
|
||||||
|
assert_template_result("hello", "{% capture my_var %}hello{% endcapture %}{{ my_var }}", error_mode: :strict2)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_capture_with_hyphen_in_strict2
|
||||||
|
assert_template_result("hello", "{% capture my-var %}hello{% endcapture %}{{ my-var }}", error_mode: :strict2)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_capture_rejects_parentheses_in_variable_name_in_strict2
|
||||||
|
assert_raises(Liquid::SyntaxError) do
|
||||||
|
Liquid::Template.parse("{% capture (x[y %}hello{% endcapture %}", error_mode: :strict2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_capture_rejects_dot_in_variable_name_in_strict2
|
||||||
|
assert_raises(Liquid::SyntaxError) do
|
||||||
|
Liquid::Template.parse("{% capture a.b %}hello{% endcapture %}", error_mode: :strict2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_capture_rejects_numeric_variable_name_in_strict2
|
||||||
|
assert_raises(Liquid::SyntaxError) do
|
||||||
|
Liquid::Template.parse("{% capture 1abc %}hello{% endcapture %}", error_mode: :strict2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_capture_allows_invalid_names_in_lax
|
||||||
|
t = Liquid::Template.parse("{% capture (x[y %}hello{% endcapture %}", error_mode: :lax)
|
||||||
|
assert_equal("(x[y", t.root.nodelist.first.to)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -105,10 +105,8 @@ class CycleTagTest < Minitest::Test
|
|||||||
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
|
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
|
||||||
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
|
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
|
||||||
|
|
||||||
expected_error = /Liquid syntax error: \[:dot, "."\] is not a valid expression/
|
assert_match(/Liquid syntax error:/, error1.message)
|
||||||
|
assert_match(/Liquid syntax error: \[:dot, "."\] is not a valid expression/, error2.message)
|
||||||
assert_match(expected_error, error1.message)
|
|
||||||
assert_match(expected_error, error2.message)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user