keep the expression parse caching per document parsing

This commit is contained in:
Michael Go
2025-01-07 14:32:46 -04:00
parent 40191022e8
commit 91be3dd75e
4 changed files with 23 additions and 16 deletions
+12 -8
View File
@@ -22,7 +22,7 @@ module Liquid
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
def self.parse(markup, _ss = nil)
def self.parse(markup, _ss = nil, _cache = nil)
return nil unless markup
markup = markup.strip
@@ -72,10 +72,8 @@ module Liquid
INTEGER_REGEX = /\A(-?\d+)\z/
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
CACHE = LruRedux::Cache.new(10_000) # most themes would have less than 2,000 unique expression
class << self
def parse(markup, ss = StringScanner.new(""))
def parse(markup, ss = StringScanner.new(""), cache = nil)
return unless markup
markup = markup.strip # markup can be a frozen string
@@ -87,24 +85,30 @@ module Liquid
return LITERALS[markup]
end
return CACHE[markup] if CACHE.key?(markup)
# Cache only exists during parsing
if cache
return cache[markup] if cache.key?(markup)
CACHE[markup] = inner_parse(markup, ss)
cache[markup] = inner_parse(markup, ss, cache)
else
inner_parse(markup, ss, nil)
end
end
def inner_parse(markup, ss)
def inner_parse(markup, ss, cache)
if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
return RangeLookup.parse(
Regexp.last_match(1),
Regexp.last_match(2),
ss,
cache,
)
end
if (num = parse_number(markup, ss))
num
else
VariableLookup.parse(markup, ss)
VariableLookup.parse(markup, ss, cache)
end
end
+3 -2
View File
@@ -3,7 +3,7 @@
module Liquid
class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth
attr_reader :partial, :warnings, :error_mode, :environment, :string_scanner
attr_reader :partial, :warnings, :error_mode, :environment, :string_scanner, :expression_cache
def initialize(options = Const::EMPTY_HASH)
@environment = options.fetch(:environment, Environment.default)
@@ -15,6 +15,7 @@ module Liquid
# constructing new StringScanner in Lexer, Tokenizer, etc is expensive
# This StringScanner will be shared by all of them
@string_scanner = StringScanner.new("")
@expression_cache = LruRedux::Cache.new(10_000)
self.depth = 0
self.partial = false
@@ -42,7 +43,7 @@ module Liquid
end
def parse_expression(markup)
Expression.parse(markup, string_scanner)
Expression.parse(markup, string_scanner, expression_cache)
end
def partial=(value)
+3 -3
View File
@@ -2,9 +2,9 @@
module Liquid
class RangeLookup
def self.parse(start_markup, end_markup, string_scanner)
start_obj = Expression.parse(start_markup, string_scanner)
end_obj = Expression.parse(end_markup, string_scanner)
def self.parse(start_markup, end_markup, string_scanner, cache = nil)
start_obj = Expression.parse(start_markup, string_scanner, cache)
end_obj = Expression.parse(end_markup, string_scanner, cache)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj)
else
+5 -3
View File
@@ -6,11 +6,11 @@ module Liquid
attr_reader :name, :lookups
def self.parse(markup, string_scanner)
new(markup, string_scanner)
def self.parse(markup, string_scanner, cache = nil)
new(markup, string_scanner, cache)
end
def initialize(markup, string_scanner = StringScanner.new(""))
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
lookups = markup.scan(VariableParser)
name = lookups.shift
@@ -18,6 +18,7 @@ module Liquid
name = Expression.parse(
name[1..-2],
string_scanner,
cache,
)
end
@name = name
@@ -31,6 +32,7 @@ module Liquid
lookups[i] = Expression.parse(
lookup[1..-2],
string_scanner,
cache,
)
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i