|
| 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