Skip to content

Commit c841a82

Browse files
aswamyclaude
andcommitted
Add specs for bare-bracket rejection in strict2 and self keyword
- strict2 rejects bare-bracket access (['product'], [key], etc.) - Qualified bracket access (product['title']) remains valid - `self` keyword resolves variables through the scope chain - `self` works in all parsing modes (lax, strict, strict2) - Local assigns shadow the `self` keyword Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e5dcfcf commit c841a82

1 file changed

Lines changed: 300 additions & 0 deletions

File tree

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
---
2+
# Specs for bare-bracket rejection in strict2 and the `self` keyword.
3+
#
4+
# In strict2 mode, bare-bracket variable access (e.g. {{ ['product'] }})
5+
# is rejected. The `self` keyword provides an explicit way to perform
6+
# dynamic variable lookups: {{ self[key] }}.
7+
#
8+
# `self` resolves to a SelfDrop that walks the normal variable scope
9+
# chain (local > file > global) without exposing context internals.
10+
11+
- name: strict2_rejects_bare_bracket_string_variable
12+
template: "{{ ['product'] }}"
13+
error_mode: strict2
14+
errors:
15+
parse_error:
16+
- "Bare bracket access is not allowed"
17+
hint: |
18+
In strict2 mode, bare-bracket access like ['product'] is rejected.
19+
The parser should raise a SyntaxError when it encounters an open
20+
square bracket at the start of an expression.
21+
22+
- name: strict2_rejects_bare_bracket_double_quoted
23+
template: '{{ ["product"] }}'
24+
error_mode: strict2
25+
errors:
26+
parse_error:
27+
- "Bare bracket access is not allowed"
28+
hint: |
29+
Double-quoted bare-bracket access is also rejected in strict2 mode.
30+
31+
- name: strict2_rejects_bare_bracket_dynamic_lookup
32+
template: "{{ [key] }}"
33+
error_mode: strict2
34+
errors:
35+
parse_error:
36+
- "Bare bracket access is not allowed"
37+
hint: |
38+
Dynamic variable lookup via bare brackets is rejected in strict2 mode.
39+
Use self[key] instead.
40+
41+
- name: strict2_rejects_bare_bracket_in_for
42+
template: "{% for item in ['collection'] %}{{ item }}{% endfor %}"
43+
error_mode: strict2
44+
errors:
45+
parse_error:
46+
- "Bare bracket access is not allowed"
47+
hint: |
48+
Bare brackets in for loop collections are rejected in strict2 mode.
49+
50+
- name: strict2_rejects_bare_bracket_in_if
51+
template: "{% if ['product'] == true %}hello{% endif %}"
52+
error_mode: strict2
53+
errors:
54+
parse_error:
55+
- "Bare bracket access is not allowed"
56+
hint: |
57+
Bare brackets in if conditions are rejected in strict2 mode.
58+
59+
- name: strict2_rejects_bare_bracket_in_case
60+
template: "{% case ['product'] %}{% when 'a' %}hello{% endcase %}"
61+
error_mode: strict2
62+
errors:
63+
parse_error:
64+
- "Bare bracket access is not allowed"
65+
hint: |
66+
Bare brackets in case expressions are rejected in strict2 mode.
67+
68+
- name: strict2_rejects_bare_bracket_in_assign
69+
template: "{% assign x = ['product'] %}"
70+
error_mode: strict2
71+
errors:
72+
parse_error:
73+
- "Bare bracket access is not allowed"
74+
hint: |
75+
Bare brackets in assign values are rejected in strict2 mode.
76+
77+
- name: strict2_accepts_qualified_bracket_access
78+
template: "{{ product['title'] }}"
79+
environment:
80+
product:
81+
title: Cool
82+
error_mode: strict2
83+
expected: "Cool"
84+
hint: |
85+
Bracket notation on a named variable (product['title']) is still
86+
valid in strict2 mode. Only bare brackets at the start of an
87+
expression are rejected.
88+
89+
- name: strict2_accepts_dot_notation
90+
template: "{{ product.title }}"
91+
environment:
92+
product:
93+
title: Cool
94+
error_mode: strict2
95+
expected: "Cool"
96+
hint: |
97+
Dot notation is always valid in strict2 mode.
98+
99+
- name: self_bracket_access_resolves_variable
100+
template: "{{ self['product'] }}"
101+
environment:
102+
product: shoes
103+
expected: "shoes"
104+
hint: |
105+
self['product'] resolves the variable 'product' through the normal
106+
scope chain. The `self` keyword returns a SelfDrop that provides
107+
variable-only access to the current context.
108+
109+
- name: self_bracket_access_strict2
110+
template: "{{ self['product'] }}"
111+
environment:
112+
product: shoes
113+
error_mode: strict2
114+
expected: "shoes"
115+
hint: |
116+
self['product'] is valid in strict2 mode - it's the replacement
117+
for bare-bracket access like ['product'].
118+
119+
- name: self_dynamic_lookup
120+
template: "{{ self[key] }}"
121+
environment:
122+
key: target
123+
target: found it
124+
expected: "found it"
125+
hint: |
126+
self[key] performs a dynamic variable lookup: first resolves 'key'
127+
to get 'target', then looks up 'target' in the scope chain.
128+
129+
- name: self_dynamic_lookup_strict2
130+
template: "{{ self[key] }}"
131+
environment:
132+
key: target
133+
target: found it
134+
error_mode: strict2
135+
expected: "found it"
136+
hint: |
137+
self[key] is the strict2-compatible way to do dynamic lookups.
138+
In lax mode, [key] works but is rejected in strict2.
139+
140+
- name: self_dynamic_lookup_with_assigned_key
141+
template: "{% assign key = 'greeting' %}{{ self[key] }}"
142+
environment:
143+
greeting: hello
144+
expected: "hello"
145+
hint: |
146+
The dynamic key passed to self[...] can come from a local assign,
147+
not just from the environment. self[key] reads 'key' from the
148+
local scope and uses its value to look up 'greeting'.
149+
150+
- name: self_sees_local_assigns
151+
template: "{% assign product = 'local' %}{{ self['product'] }}"
152+
environment:
153+
product: global
154+
expected: "local"
155+
hint: |
156+
self walks the normal scope chain (local > file > global).
157+
A local assign shadows the global variable, and self['product']
158+
returns the local value.
159+
160+
- name: self_can_be_assigned
161+
template: "{% assign self = 'hello' %}{{ self }}"
162+
expected: "hello"
163+
hint: |
164+
If 'self' is explicitly assigned as a local variable, the local
165+
value takes precedence over the SelfDrop. This allows templates
166+
that already use 'self' as a variable name to continue working.
167+
168+
- name: self_returns_empty_for_unknown_keys
169+
template: "{{ self['nonexistent'] }}"
170+
environment:
171+
product: shoes
172+
expected: ""
173+
hint: |
174+
self['nonexistent'] returns nil (rendered as empty string) when
175+
the key doesn't exist in any scope.
176+
177+
- name: self_returns_empty_for_unknown_keys_strict2
178+
template: "{{ self['nonexistent'] }}"
179+
error_mode: strict2
180+
expected: ""
181+
hint: |
182+
In strict2 mode, self['nonexistent'] still renders as an empty
183+
string for missing keys (it returns nil, not an error). Strict2
184+
rejects bare brackets at parse time, but self[...] is a normal
185+
drop access at runtime and follows the usual missing-variable
186+
rendering behavior.
187+
188+
- name: self_nested_property_access
189+
template: "{{ self['product'].title }}"
190+
environment:
191+
product:
192+
title: Shoes
193+
expected: "Shoes"
194+
hint: |
195+
After resolving self['product'] to the product hash, further
196+
property access (.title) works as expected.
197+
198+
- name: self_bracket_in_bracket_recursion
199+
template: "{{ a[ self[ 'b' ] ] }}"
200+
environment:
201+
b: c
202+
a:
203+
c: result
204+
expected: "result"
205+
hint: |
206+
self[...] can appear inside another bracket expression. Here
207+
self['b'] resolves to 'c', which is then used as the key into
208+
the 'a' hash, yielding 'result'. Nested resolution should work
209+
naturally because self[...] is just another expression.
210+
211+
- name: self_recursive_lookup
212+
template: "{{ self[self['key1']] }}"
213+
environment:
214+
key1: key2
215+
key2: value
216+
expected: "value"
217+
hint: |
218+
self[...] can be nested inside itself. The inner self['key1']
219+
resolves to 'key2', then the outer self[...] looks up 'key2'
220+
in the scope chain and returns 'value'.
221+
222+
- name: self_contains_present_key
223+
template: "{% if self contains 'greeting' %}yes{% else %}no{% endif %}"
224+
environment:
225+
greeting: hello
226+
expected: "no"
227+
hint: |
228+
QUIRK: `self contains 'key'` always renders the false branch,
229+
even when the key exists. The `contains` operator calls
230+
include? on its left operand, and SelfDrop does not implement
231+
include?, so the operator falls through to false. Use
232+
self['key'] and check the result instead, or rely on
233+
{% if self['key'] %} for truthiness.
234+
235+
- name: self_contains_absent_key
236+
template: "{% if self contains 'absent' %}yes{% else %}no{% endif %}"
237+
expected: "no"
238+
hint: |
239+
`self contains 'key'` returns false when the key is not defined.
240+
Note that this matches the present-key behavior - `contains`
241+
does not work on SelfDrop because Drop does not implement
242+
include?.
243+
244+
- name: self_contains_nil_valued_key
245+
template: "{% if self contains 'maybe' %}yes{% else %}no{% endif %}"
246+
environment:
247+
maybe: null
248+
expected: "no"
249+
hint: |
250+
`self contains 'key'` returns false even for keys with explicit
251+
nil values, because `contains` doesn't work on SelfDrop at all
252+
(Drop doesn't implement include?). The `contains` operator is
253+
not the right way to test variable definedness through self.
254+
255+
- name: self_inside_for_loop_body
256+
template: "{% for item in items %}{{ self[item] }}{% endfor %}"
257+
environment:
258+
items:
259+
- a
260+
- b
261+
a: "1"
262+
b: "2"
263+
expected: "12"
264+
hint: |
265+
self[...] works inside loop bodies. Each iteration of the for
266+
loop looks up the current `item` value through self, walking
267+
the normal scope chain to find variables `a` and `b`.
268+
269+
- name: self_inside_capture_body
270+
template: "{% capture x %}{{ self['k'] }}{% endcapture %}{{ x }}"
271+
environment:
272+
k: v
273+
expected: "v"
274+
hint: |
275+
self[...] works inside capture blocks. The captured value is
276+
rendered using the same variable lookup rules as anywhere else.
277+
278+
- name: self_lookup_when_environment_has_self_key
279+
template: "{{ self['key'] }}"
280+
environment:
281+
self: env_value
282+
key: value
283+
expected: "value"
284+
hint: |
285+
Even when the environment defines a 'self' key, the `self`
286+
keyword still resolves to the SelfDrop for bracket lookups.
287+
SelfDrop is returned by find_variable for the 'self' key
288+
before environment lookup occurs (unless 'self' was explicitly
289+
assigned as a local variable). So self['key'] still does the
290+
normal scope-chain lookup and finds 'key'.
291+
292+
- name: lax_allows_bare_bracket_access
293+
template: "{{ ['product'] }}"
294+
environment:
295+
product: shoes
296+
error_mode: :lax
297+
expected: "shoes"
298+
hint: |
299+
Bare-bracket access is still allowed in lax mode for backwards
300+
compatibility. Only strict2 mode rejects it.

0 commit comments

Comments
 (0)