Skip to content

Commit f5082d6

Browse files
aswamyclaude
andcommitted
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) <noreply@anthropic.com>
1 parent 346166b commit f5082d6

File tree

5 files changed

+121
-5
lines changed

5 files changed

+121
-5
lines changed

lib/liquid/tags/assign.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ module Liquid
1818
# @liquid_syntax_keyword variable_name The name of the variable being created.
1919
# @liquid_syntax_keyword value The value you want to assign to the variable.
2020
class Assign < Tag
21+
include ParserSwitching
22+
2123
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
2224

2325
# @api private
@@ -29,6 +31,10 @@ def self.raise_syntax_error(parse_context)
2931

3032
def initialize(tag_name, markup, parse_context)
3133
super
34+
strict_parse_with_error_mode_fallback(markup)
35+
end
36+
37+
def lax_parse(markup)
3238
if markup =~ Syntax
3339
@to = Regexp.last_match(1)
3440
@from = Variable.new(Regexp.last_match(2), parse_context)
@@ -37,6 +43,25 @@ def initialize(tag_name, markup, parse_context)
3743
end
3844
end
3945

46+
def strict_parse(markup)
47+
lax_parse(markup)
48+
end
49+
50+
def strict2_parse(markup)
51+
unless markup =~ Syntax
52+
self.class.raise_syntax_error(parse_context)
53+
end
54+
55+
lhs = Regexp.last_match(1).strip
56+
rhs = Regexp.last_match(2)
57+
58+
p = @parse_context.new_parser(lhs)
59+
@to = p.consume(:id)
60+
p.consume(:end_of_string)
61+
62+
@from = Variable.new(rhs, parse_context)
63+
end
64+
4065
def render_to_output_buffer(context, output)
4166
val = @from.render(context)
4267
context.scopes.last[@to] = val

lib/liquid/tags/capture.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,35 @@ module Liquid
2020
# @liquid_syntax_keyword variable The name of the variable being created.
2121
# @liquid_syntax_keyword value The value you want to assign to the variable.
2222
class Capture < Block
23+
include ParserSwitching
24+
2325
Syntax = /(#{VariableSignature}+)/o
2426

27+
attr_reader :to
28+
2529
def initialize(tag_name, markup, options)
2630
super
31+
strict_parse_with_error_mode_fallback(markup)
32+
end
33+
34+
def lax_parse(markup)
2735
if markup =~ Syntax
2836
@to = Regexp.last_match(1)
2937
else
3038
raise SyntaxError, options[:locale].t("errors.syntax.capture")
3139
end
3240
end
3341

42+
def strict_parse(markup)
43+
lax_parse(markup)
44+
end
45+
46+
def strict2_parse(markup)
47+
p = @parse_context.new_parser(markup.strip)
48+
@to = p.consume(:id)
49+
p.consume(:end_of_string)
50+
end
51+
3452
def render_to_output_buffer(context, output)
3553
context.resource_limits.with_capture do
3654
capture_output = render(context)

test/integration/assign_test.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,46 @@ def test_assign_score_of_hash
9797
assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd'))
9898
end
9999

100+
def test_assign_with_valid_identifier_in_strict2
101+
assert_template_result("hello", "{% assign my_var = 'hello' %}{{ my_var }}", error_mode: :strict2)
102+
end
103+
104+
def test_assign_with_hyphen_in_strict2
105+
assert_template_result("hello", "{% assign my-var = 'hello' %}{{ my-var }}", error_mode: :strict2)
106+
end
107+
108+
def test_assign_rejects_parentheses_in_variable_name_in_strict2
109+
assert_raises(Liquid::SyntaxError) do
110+
Liquid::Template.parse("{% assign (a(b(c) = 1234 %}", error_mode: :strict2)
111+
end
112+
end
113+
114+
def test_assign_rejects_brackets_in_variable_name_in_strict2
115+
assert_raises(Liquid::SyntaxError) do
116+
Liquid::Template.parse("{% assign [x.y] = 'hello' %}", error_mode: :strict2)
117+
end
118+
end
119+
120+
def test_assign_rejects_dot_in_variable_name_in_strict2
121+
assert_raises(Liquid::SyntaxError) do
122+
Liquid::Template.parse("{% assign a.b = 'hello' %}", error_mode: :strict2)
123+
end
124+
end
125+
126+
def test_assign_rejects_numeric_variable_name_in_strict2
127+
assert_raises(Liquid::SyntaxError) do
128+
Liquid::Template.parse("{% assign 1abc = 'hello' %}", error_mode: :strict2)
129+
end
130+
end
131+
132+
def test_assign_allows_invalid_names_in_lax
133+
assert_template_result("1234", "{% assign (a(b(c) = 1234 %}{{ self['(a(b(c)'] }}", error_mode: :lax)
134+
end
135+
136+
def test_assign_with_filter_in_strict2
137+
assert_template_result("HELLO", "{% assign my_var = 'hello' | upcase %}{{ my_var }}", error_mode: :strict2)
138+
end
139+
100140
private
101141

102142
class ObjectWrapperDrop < Liquid::Drop

test/integration/capture_test.rb

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ class CaptureTest < Minitest::Test
66
include Liquid
77

88
def test_captures_block_content_in_variable
9-
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
9+
assert_template_result("test string", "{% capture var %}test string{% endcapture %}{{var}}", {})
10+
end
11+
12+
def test_captures_block_content_in_quoted_variable_in_lax
13+
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {}, error_mode: :lax)
1014
end
1115

1216
def test_capture_with_hyphen_in_variable_name
@@ -49,4 +53,35 @@ def test_increment_assign_score_by_bytes_not_characters
4953
t.render!
5054
assert_equal(9, t.resource_limits.assign_score)
5155
end
56+
57+
def test_capture_with_valid_identifier_in_strict2
58+
assert_template_result("hello", "{% capture my_var %}hello{% endcapture %}{{ my_var }}", error_mode: :strict2)
59+
end
60+
61+
def test_capture_with_hyphen_in_strict2
62+
assert_template_result("hello", "{% capture my-var %}hello{% endcapture %}{{ my-var }}", error_mode: :strict2)
63+
end
64+
65+
def test_capture_rejects_parentheses_in_variable_name_in_strict2
66+
assert_raises(Liquid::SyntaxError) do
67+
Liquid::Template.parse("{% capture (x[y %}hello{% endcapture %}", error_mode: :strict2)
68+
end
69+
end
70+
71+
def test_capture_rejects_dot_in_variable_name_in_strict2
72+
assert_raises(Liquid::SyntaxError) do
73+
Liquid::Template.parse("{% capture a.b %}hello{% endcapture %}", error_mode: :strict2)
74+
end
75+
end
76+
77+
def test_capture_rejects_numeric_variable_name_in_strict2
78+
assert_raises(Liquid::SyntaxError) do
79+
Liquid::Template.parse("{% capture 1abc %}hello{% endcapture %}", error_mode: :strict2)
80+
end
81+
end
82+
83+
def test_capture_allows_invalid_names_in_lax
84+
t = Liquid::Template.parse("{% capture (x[y %}hello{% endcapture %}", error_mode: :lax)
85+
assert_equal("(x[y", t.root.nodelist.first.to)
86+
end
5287
end

test/integration/tags/cycle_tag_test.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,8 @@ def test_cycle_tag_with_error_mode
105105
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
106106
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
107107

108-
expected_error = /Liquid syntax error: \[:dot, "."\] is not a valid expression/
109-
110-
assert_match(expected_error, error1.message)
111-
assert_match(expected_error, error2.message)
108+
assert_match(/Liquid syntax error:/, error1.message)
109+
assert_match(/Liquid syntax error: \[:dot, "."\] is not a valid expression/, error2.message)
112110
end
113111
end
114112

0 commit comments

Comments
 (0)