Skip to content

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

Open
aswamy wants to merge 1 commit intomainfrom
bare-bracket-self-keyword
Open

Reject bare-bracket syntax in strict2 and introduce self keyword#2060
aswamy wants to merge 1 commit intomainfrom
bare-bracket-self-keyword

Conversation

@aswamy
Copy link
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 4 times, most recently from d77e5d3 to f56eb10 Compare March 19, 2026 20:55
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 f56eb10 to 985943c Compare March 19, 2026 20:57
Copy link
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.

end

def to_liquid
self
Copy link
Contributor

Choose a reason for hiding this comment

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

Image

Copy link
Contributor

Choose a reason for hiding this comment

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

Would love to see some additional tests for the new logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants