mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-16 09:20:42 -07:00
fix(render): preserve scope chain when self: bound on render tag
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.
This commit is contained in:
@@ -200,6 +200,10 @@ module Liquid
|
||||
object.respond_to?(:evaluate) ? object.evaluate(self) : object
|
||||
end
|
||||
|
||||
def self_drop
|
||||
@self_drop ||= SelfDrop.new(self)
|
||||
end
|
||||
|
||||
# Fetches an object starting at the local scope and then moving up the hierachy
|
||||
def find_variable(key, raise_on_not_found: true)
|
||||
# This was changed from find() to find_index() because this is a very hot
|
||||
@@ -208,7 +212,7 @@ module Liquid
|
||||
|
||||
# `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
|
||||
return self_drop if key == Expression::SELF && !index
|
||||
|
||||
variable = if index
|
||||
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
|
||||
|
||||
+21
-2
@@ -16,23 +16,42 @@ module Liquid
|
||||
# then the local value takes precedence over the `self` object.
|
||||
# @liquid_access global
|
||||
class SelfDrop < Drop
|
||||
attr_accessor :bound_self
|
||||
|
||||
def initialize(context)
|
||||
super()
|
||||
@context = context
|
||||
@bound_self = nil
|
||||
end
|
||||
|
||||
def [](key)
|
||||
@context.find_variable(key)
|
||||
if @bound_self && bound_has?(key)
|
||||
bound_lookup(key)
|
||||
else
|
||||
@context.find_variable(key)
|
||||
end
|
||||
rescue UndefinedVariable
|
||||
nil
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
@context.variable_defined?(key)
|
||||
(@bound_self && bound_has?(key)) || @context.variable_defined?(key)
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def bound_has?(key)
|
||||
@bound_self.respond_to?(:key?) && @bound_self.key?(key)
|
||||
end
|
||||
|
||||
def bound_lookup(key)
|
||||
return unless @bound_self.respond_to?(:[])
|
||||
|
||||
@bound_self[key]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,7 +66,12 @@ module Liquid
|
||||
inner_context['forloop'] = forloop if forloop
|
||||
|
||||
@attributes.each do |key, value|
|
||||
inner_context[key] = context.evaluate(value)
|
||||
evaluated = context.evaluate(value)
|
||||
if key == Expression::SELF
|
||||
inner_context.self_drop.bound_self = evaluated
|
||||
else
|
||||
inner_context[key] = evaluated
|
||||
end
|
||||
end
|
||||
inner_context[context_variable_name] = var unless var.nil?
|
||||
partial.render_to_output_buffer(inner_context, output)
|
||||
|
||||
Reference in New Issue
Block a user