Files
liquid/lib/liquid/variable_lookup.rb
T
Dylan Thacker-Smith e20df18eca [WIP] Add support for migrating liquid code away from several lax parse quirks
It transforms liquid code to equivalently parsed code, that doesn't depend
on lax parsing quirks.

Note that this transformation can hide the intent of the original code, so
it is best to review the changes, since removed syntax often would indicate
a bug in the original liquid code.
2022-11-16 17:35:29 -05:00

132 lines
3.9 KiB
Ruby

# frozen_string_literal: true
module Liquid
class VariableLookup
COMMAND_METHODS = ['size', 'first', 'last'].freeze
attr_reader :name, :lookups
def self.parse(markup)
new(markup)
end
LITERALS = Expression::LITERALS.keys.freeze
private_constant :LITERALS
def self.lax_migrate(markup)
new_markup = nil
last_match = nil
first_match = nil
markup.scan(VariableParser) do |lookup|
last_match = Regexp.last_match
first_match ||= last_match
if lookup&.start_with?('[') && lookup&.end_with?(']')
new_markup ||= +""
new_markup << "[" << Expression.lax_migrate(lookup[1..-2]) << "]"
else
new_markup << "." if new_markup
new_markup ||= +""
new_markup << lookup
end
end
case new_markup
when nil
# `markup.scan(VariableParser)` may skip over all characters
new_markup ||= " nil "
when Expression::INTEGERS_REGEX, Expression::RANGES_REGEX, Expression::FLOATS_REGEX, *LITERALS
# Quote variable lookups that match literals after characters are skipped by regex scanning
new_markup = "['#{new_markup}']"
else
new_markup.prepend(" ") if first_match.begin(0) > 0
new_markup << ' ' if last_match.end(0) < markup.length
end
new_markup
end
def initialize(markup)
lookups = markup.scan(VariableParser)
name = lookups.shift
if name&.start_with?('[') && name&.end_with?(']')
name = Expression.parse(name[1..-2])
end
@name = name
@lookups = lookups
@command_flags = 0
@lookups.each_index do |i|
lookup = lookups[i]
if lookup&.start_with?('[') && lookup&.end_with?(']')
lookups[i] = Expression.parse(lookup[1..-2])
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
end
end
end
def lookup_command?(lookup_index)
@command_flags & (1 << lookup_index) != 0
end
def evaluate(context)
name = context.evaluate(@name)
object = context.find_variable(name)
@lookups.each_index do |i|
key = context.evaluate(@lookups[i])
# Cast "key" to its liquid value to enable it to act as a primitive value
key = Liquid::Utils.to_liquid_value(key)
# If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it
if object.respond_to?(:[]) &&
((object.respond_to?(:key?) && object.key?(key)) ||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key)
object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls
# as commands and call them on the current object
elsif lookup_command?(i) && object.respond_to?(key)
object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil or
# raise an exception if `strict_variables` option is set to true
else
return nil unless context.strict_variables
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
# If we are dealing with a drop here we have to
object.context = context if object.respond_to?(:context=)
end
object
end
def ==(other)
self.class == other.class && state == other.state
end
protected
def state
[@name, @lookups, @command_flags]
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.lookups
end
end
end
end