Skip to content

Reject bare-bracket syntax in strict2 and introduce self keyword#2060

Merged
aswamy merged 1 commit into
mainfrom
bare-bracket-self-keyword
Apr 24, 2026
Merged

Reject bare-bracket syntax in strict2 and introduce self keyword#2060
aswamy merged 1 commit into
mainfrom
bare-bracket-self-keyword

Conversation

@aswamy
Copy link
Copy Markdown
Contributor

@aswamy aswamy commented Mar 19, 2026

Summary

Adds bare-bracket rejection to the strict2 parser and introduces a self keyword for explicit variable lookups.

Parser changes:

  • Parser#expression now raises a SyntaxError when it encounters bare-bracket access (['product']) in strict2/rigid mode
  • ParseContext#new_parser passes reject_bare_brackets: true when error_mode is :strict2 or :rigid

self keyword:

  • New Expression::SELF constant ('self')
  • New SelfDrop class — a drop that provides variable-only access to the context's scope chain (local > file > global), without exposing filters, interrupts, or other context internals
  • Context#find_variable returns a SelfDrop when the key is self and self hasn't been explicitly assigned as a local variable
  • If self is explicitly assigned (e.g. {% assign self = 'value' %}), the local value takes precedence
  • Context#variable_defined? — new method that checks key existence across scopes/environments using Hash#key?, so nil-valued variables are correctly treated as defined (used by SelfDrop#key?)
  • Annotated with YARD @liquid_public_docs tags for public documentation generation

Variable#==:

  • Added equality method comparing name and filters, used by the rewriter to detect AST equivalence when deciding whether to flag nodes for rewrite

Tophat

cd /path/to/liquid
bundle install
bin/render  # or: bundle exec irb -r liquid

Bare-bracket rejection in strict2:

# Rejected
Liquid::Template.parse("{{ ['product'] }}", error_mode: :strict2)
# => Liquid::SyntaxError: Bare bracket access is not allowed in strict2 mode. Use self['...'] instead

# Accepted
Liquid::Template.parse("{{ product }}", error_mode: :strict2)
Liquid::Template.parse("{{ self['product'] }}", error_mode: :strict2)
Liquid::Template.parse("{{ product['title'] }}", error_mode: :strict2)

self resolves through the scope chain:

# self['var'] walks local > file > global
t = Liquid::Template.parse("{{ self['product'] }}")
t.render('product' => 'shoes')
# => "shoes"

# Local assigns take precedence
t = Liquid::Template.parse("{% assign product = 'local' %}{{ self['product'] }}")
t.render('product' => 'global')
# => "local"

# Assigning self itself works
t = Liquid::Template.parse("{% assign self = 'hello' %}{{ self }}")
t.render
# => "hello"

# nil-valued variables are correctly handled under strict_variables
t = Liquid::Template.parse("{{ self['x'] }}")
t.render!({ 'x' => nil }, strict_variables: true)
# => "" (no UndefinedVariable error)

Lax mode is unaffected:

Liquid::Template.parse("{{ ['product'] }}", error_mode: :lax)
# => OK, no error

🤖 Generated with Claude Code

@aswamy aswamy force-pushed the bare-bracket-self-keyword branch 5 times, most recently from f56eb10 to 985943c Compare March 19, 2026 20:57
Copy link
Copy Markdown
Contributor

@graygilmore graygilmore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The drop looks good to me but I'd prefer to have somebody with a little more Liquid context give another approval.

Comment thread lib/liquid/self_drop.rb
Comment thread lib/liquid/context.rb
Add bare-bracket rejection to Parser#expression in strict2 mode, so that
`['var']` is disallowed and `self['var']` is the required syntax.

- Add `Expression::SELF` constant ('self')
- Add `Parser#reject_bare_brackets` option, checked in `expression`
- Add `ParseContext#reject_bare_brackets?` and `force_reject_bare_brackets`
- Add `VariableLookupDrop` for `self['var']` scope-chain lookups
- Add `Variable#==` for rewriter state comparison
- Update `Context#find_variable` to return `VariableLookupDrop` for `self`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@aswamy aswamy force-pushed the bare-bracket-self-keyword branch from e2561ed to 532b439 Compare March 23, 2026 16:14
@aswamy aswamy merged commit d0c5444 into main Apr 24, 2026
13 checks passed
EvilGenius13 added a commit that referenced this pull request Apr 30, 2026
The render tag wrote `self:` attributes directly into @scopes[0] under
key 'self', which shadowed the SelfDrop. `self[var]` lookups inside the
snippet then did strict key access on the bound object instead of
walking the scope chain, resolving to nil for any key not in the object.

SelfDrop now holds an optional `bound_self`; `[]` and `key?` consult it
first via duck typing (matches lib/liquid/variable_lookup.rb), then fall
through to the scope-chain walk on miss. Render#render_tag routes
attribute key Expression::SELF to inner_context.self_drop.bound_self=
instead of the generic scope write. Lookup order is bound-first;
existing self: users' hits stay intact, previously-nil misses now
resolve via fallthrough.

PR #2060 introduced the SelfDrop and bare-bracket prohibition but had
no coverage for the {% render 'snippet', self: obj %} interaction. Adds
8 tests including two-deep nested renders with leak detection and a
composite chain combining renders, top-level self[var], and regular
variables.

Discovered via SFR strict-parser verifier on hexclad TopPicks, where
the sanctioned `[var]` -> `self[var]` migration form (post PR #2060)
hit this edge case.
EvilGenius13 added a commit that referenced this pull request Apr 30, 2026
The render tag wrote `self:` attributes directly into @scopes[0] under
key 'self', which shadowed the SelfDrop. `self[var]` lookups inside the
snippet then did strict key access on the bound object instead of
walking the scope chain, resolving to nil for any key not in the object.

SelfDrop now holds an optional `bound_self`; `[]` and `key?` consult it
first via duck typing (matches lib/liquid/variable_lookup.rb), then fall
through to the scope-chain walk on miss. Render#render_tag routes
attribute key Expression::SELF to inner_context.self_drop.bound_self=
instead of the generic scope write. Lookup order is bound-first;
existing self: users' hits stay intact, previously-nil misses now
resolve via fallthrough.

PR #2060 introduced the SelfDrop and bare-bracket prohibition but had
no coverage for the {% render 'snippet', self: obj %} interaction.
Adds 8 tests including two-deep nested renders with leak detection
and a composite chain combining renders, top-level self[var], and
regular variables.

Discovered while investigating SFR strict-parser migration parity
diffs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

#gsd:49922 Liquid Array Literal Support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants