mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-12 23:40:45 -07:00
Reject bare-bracket syntax in strict2 and introduce self keyword
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) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
dd37353cca
commit
532b439063
@@ -65,6 +65,7 @@ require 'liquid/lexer'
|
|||||||
require 'liquid/parser'
|
require 'liquid/parser'
|
||||||
require 'liquid/i18n'
|
require 'liquid/i18n'
|
||||||
require 'liquid/drop'
|
require 'liquid/drop'
|
||||||
|
require 'liquid/self_drop'
|
||||||
require 'liquid/tablerowloop_drop'
|
require 'liquid/tablerowloop_drop'
|
||||||
require 'liquid/forloop_drop'
|
require 'liquid/forloop_drop'
|
||||||
require 'liquid/extensions'
|
require 'liquid/extensions'
|
||||||
|
|||||||
@@ -187,6 +187,15 @@ module Liquid
|
|||||||
find_variable(key, raise_on_not_found: false) != nil
|
find_variable(key, raise_on_not_found: false) != nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Checks whether a variable is defined in any scope, including nil-valued keys.
|
||||||
|
# Unlike #key?, this uses Hash#key? so that variables explicitly set to nil
|
||||||
|
# are still considered defined.
|
||||||
|
def variable_defined?(key)
|
||||||
|
@scopes.any? { |s| s.key?(key) } ||
|
||||||
|
@environments.any? { |e| e.key?(key) } ||
|
||||||
|
@static_environments.any? { |e| e.key?(key) }
|
||||||
|
end
|
||||||
|
|
||||||
def evaluate(object)
|
def evaluate(object)
|
||||||
object.respond_to?(:evaluate) ? object.evaluate(self) : object
|
object.respond_to?(:evaluate) ? object.evaluate(self) : object
|
||||||
end
|
end
|
||||||
@@ -197,6 +206,10 @@ module Liquid
|
|||||||
# path and find_index() is optimized in MRI to reduce object allocation
|
# path and find_index() is optimized in MRI to reduce object allocation
|
||||||
index = @scopes.find_index { |s| s.key?(key) }
|
index = @scopes.find_index { |s| s.key?(key) }
|
||||||
|
|
||||||
|
# `self` resolves to a SelfDrop (enabling `self['var']` lookups),
|
||||||
|
# but only when it hasn't been explicitly assigned as a local variable.
|
||||||
|
return SelfDrop.new(self) if key == Expression::SELF && !index
|
||||||
|
|
||||||
variable = if index
|
variable = if index
|
||||||
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
|
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
class Expression
|
class Expression
|
||||||
|
SELF = 'self'
|
||||||
|
|
||||||
LITERALS = {
|
LITERALS = {
|
||||||
nil => nil,
|
nil => nil,
|
||||||
'nil' => nil,
|
'nil' => nil,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ module Liquid
|
|||||||
|
|
||||||
def new_parser(input)
|
def new_parser(input)
|
||||||
@string_scanner.string = input
|
@string_scanner.string = input
|
||||||
Parser.new(@string_scanner)
|
Parser.new(@string_scanner, reject_bare_brackets: @error_mode == :strict2 || @error_mode == :rigid)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
|
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
class Parser
|
class Parser
|
||||||
def initialize(input)
|
def initialize(input, reject_bare_brackets: false)
|
||||||
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
|
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
|
||||||
@tokens = Lexer.tokenize(ss)
|
@tokens = Lexer.tokenize(ss)
|
||||||
@p = 0 # pointer to current location
|
@p = 0 # pointer to current location
|
||||||
|
@reject_bare_brackets = reject_bare_brackets
|
||||||
end
|
end
|
||||||
|
|
||||||
def jump(point)
|
def jump(point)
|
||||||
@@ -53,6 +54,9 @@ module Liquid
|
|||||||
str = consume
|
str = consume
|
||||||
str << variable_lookups
|
str << variable_lookups
|
||||||
when :open_square
|
when :open_square
|
||||||
|
if @reject_bare_brackets
|
||||||
|
raise SyntaxError, "Bare bracket access is not allowed in strict2 mode. Use #{Expression::SELF}['...'] instead"
|
||||||
|
end
|
||||||
str = consume.dup
|
str = consume.dup
|
||||||
str << expression
|
str << expression
|
||||||
str << consume(:close_square)
|
str << consume(:close_square)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Liquid
|
||||||
|
# @liquid_public_docs
|
||||||
|
# @liquid_type object
|
||||||
|
# @liquid_name self
|
||||||
|
# @liquid_summary
|
||||||
|
# Provides access to variables through the current scope chain.
|
||||||
|
# @liquid_description
|
||||||
|
# The `self` object resolves variables through the normal lookup hierarchy
|
||||||
|
# (local > file > global) without exposing filters, interrupts, errors,
|
||||||
|
# or other context internals. It's used when bare bracket notation
|
||||||
|
# (`['variable']`) needs to be replaced with an explicit variable lookup.
|
||||||
|
#
|
||||||
|
# If `self` is explicitly assigned as a local variable (e.g. `{% assign self = 'value' %}`),
|
||||||
|
# then the local value takes precedence over the `self` object.
|
||||||
|
# @liquid_access global
|
||||||
|
class SelfDrop < Drop
|
||||||
|
def initialize(context)
|
||||||
|
super()
|
||||||
|
@context = context
|
||||||
|
end
|
||||||
|
|
||||||
|
def [](key)
|
||||||
|
@context.find_variable(key)
|
||||||
|
rescue UndefinedVariable
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def key?(key)
|
||||||
|
@context.variable_defined?(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_liquid
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -37,6 +37,10 @@ module Liquid
|
|||||||
@markup
|
@markup
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ==(other)
|
||||||
|
self.class == other.class && name == other.name && filters == other.filters
|
||||||
|
end
|
||||||
|
|
||||||
def markup_context(markup)
|
def markup_context(markup)
|
||||||
"in \"{{#{markup}}}\""
|
"in \"{{#{markup}}}\""
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -296,8 +296,8 @@ class ContextTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_access_variable_with_hash_notation
|
def test_access_variable_with_hash_notation
|
||||||
assert_template_result('baz', '{{ ["foo"] }}', { "foo" => "baz" })
|
assert_template_result('baz', '{{ foo }}', { "foo" => "baz" })
|
||||||
assert_template_result('baz', '{{ [bar] }}', { 'foo' => 'baz', 'bar' => 'foo' })
|
assert_template_result('baz', '{{ self[bar] }}', { 'foo' => 'baz', 'bar' => 'foo' })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_access_hashes_with_hash_access_variables
|
def test_access_hashes_with_hash_access_variables
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class VariableTest < Minitest::Test
|
|||||||
|
|
||||||
def test_expression_with_whitespace_in_square_brackets
|
def test_expression_with_whitespace_in_square_brackets
|
||||||
assert_template_result('result', "{{ a[ 'b' ] }}", { 'a' => { 'b' => 'result' } })
|
assert_template_result('result', "{{ a[ 'b' ] }}", { 'a' => { 'b' => 'result' } })
|
||||||
assert_template_result('result', "{{ a[ [ 'b' ] ] }}", { 'b' => 'c', 'a' => { 'c' => 'result' } })
|
assert_template_result('result', "{{ a[ self[ 'b' ] ] }}", { 'b' => 'c', 'a' => { 'c' => 'result' } })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_ignore_unknown
|
def test_ignore_unknown
|
||||||
@@ -135,17 +135,17 @@ class VariableTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_dynamic_find_var
|
def test_dynamic_find_var
|
||||||
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
|
assert_template_result('bar', '{{ self[key] }}', { 'key' => 'foo', 'foo' => 'bar' })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_raw_value_variable
|
def test_raw_value_variable
|
||||||
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
|
assert_template_result('bar', '{{ self[key] }}', { 'key' => 'foo', 'foo' => 'bar' })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_dynamic_find_var_with_drop
|
def test_dynamic_find_var_with_drop
|
||||||
assert_template_result(
|
assert_template_result(
|
||||||
'bar',
|
'bar',
|
||||||
'{{ [list[settings.zero]] }}',
|
'{{ self[list[settings.zero]] }}',
|
||||||
{
|
{
|
||||||
'list' => ['foo'],
|
'list' => ['foo'],
|
||||||
'settings' => SettingsDrop.new("zero" => 0),
|
'settings' => SettingsDrop.new("zero" => 0),
|
||||||
@@ -155,7 +155,7 @@ class VariableTest < Minitest::Test
|
|||||||
|
|
||||||
assert_template_result(
|
assert_template_result(
|
||||||
'foo',
|
'foo',
|
||||||
'{{ [list[settings.zero]["foo"]] }}',
|
'{{ self[list[settings.zero]["foo"]] }}',
|
||||||
{
|
{
|
||||||
'list' => [{ 'foo' => 'bar' }],
|
'list' => [{ 'foo' => 'bar' }],
|
||||||
'settings' => SettingsDrop.new("zero" => 0),
|
'settings' => SettingsDrop.new("zero" => 0),
|
||||||
|
|||||||
Reference in New Issue
Block a user