mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-12 23:40:45 -07:00
Decomposes the 211-line try_fast_parse monolith in variable.rb into four
named private methods -- fast_scan_name, fast_resolve_name, fast_scan_filters,
fast_scan_filter_args -- and simplifies simple_variable_markup from a 55-line
byte scanner to a 10-line regex with fast pre-checks.
Generates invoke_single/invoke_two via module_eval in strainer_template.rb
and context.rb instead of duplicating near-identical method bodies. The two
arity variants differ only in parameter lists; generating them makes the
pattern explicit and eliminates copy-paste drift.
Extracts identical 4-line text-token handling block in block_body.rb into
private append_text_token(token, parse_context). The block appeared in
both the stray-{ fallback and the plain-text branch of parse_for_document.
Extracts accessible?(object, key) predicate from the 4-line inline ternary
in variable_lookup.rb that checked hash/array key presence; extracts
liquidize(object, context) from the duplicated to_liquid + context= wiring
that appeared in both the key-found and command-method branches.
Extracts find_in_envs(envs, key, raise_on_not_found:) from
try_variable_find_in_environments in context.rb, which looped over
@environments then @static_environments with identical loop bodies.
Adds 28 fast-path equivalence tests for Variable (variable_fast_parse_test.rb).
127 lines
4.4 KiB
Ruby
127 lines
4.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'test_helper'
|
|
|
|
# Tests that the fast-path parser (try_fast_parse) produces the same result as the
|
|
# full Lexer → Parser pipeline for every input we expect it to handle.
|
|
#
|
|
# This protects against silent regressions where a change to try_fast_parse causes it
|
|
# to produce different output from the slow path (the existing test suite would still
|
|
# pass because the slow path catches it, but correctness would be silently lost).
|
|
class VariableFastParseTest < Minitest::Test
|
|
include Liquid
|
|
|
|
EQUIVALENCE_CASES = [
|
|
# Simple lookups
|
|
"product",
|
|
"product.title",
|
|
"product.variants.first.title",
|
|
# Quoted string literals
|
|
"'hello'",
|
|
'"hello"',
|
|
# Variables with no-arg filters
|
|
"product | upcase",
|
|
"product | upcase | downcase",
|
|
"product | strip | upcase | downcase",
|
|
# Variables with single-arg filters
|
|
"product | truncate: 50",
|
|
"product | plus: 1",
|
|
"product | plus: -3",
|
|
"product | round: 2",
|
|
"product | append: ' world'",
|
|
# Variables with multi-arg filters
|
|
"product | replace: 'a', 'b'",
|
|
"product | pluralize: 'item', 'items'",
|
|
"product | slice: 0, 5",
|
|
# Chained mixed filters
|
|
"product.title | truncate: 50",
|
|
"'hello' | append: ' world' | upcase",
|
|
"name | prepend: 'Dr. ' | append: ' PhD' | upcase",
|
|
# Numeric args
|
|
"count | plus: 1.5",
|
|
"price | minus: 0.99",
|
|
# No whitespace around pipe
|
|
"x|upcase",
|
|
"x|replace:'a','b'|upcase",
|
|
# Leading/trailing whitespace
|
|
" product ",
|
|
" product.title | upcase ",
|
|
].freeze
|
|
|
|
EQUIVALENCE_CASES.each_with_index do |markup, i|
|
|
define_method(:"test_fast_parse_equivalence_#{i.to_s.rjust(2, "0")}") do
|
|
lax_ctx = Liquid::ParseContext.new(error_mode: :lax)
|
|
strict_ctx = Liquid::ParseContext.new(error_mode: :strict)
|
|
|
|
lax_var = Liquid::Variable.new(markup, lax_ctx)
|
|
strict_var = Liquid::Variable.new(markup, strict_ctx)
|
|
|
|
assert_equal strict_var.name,
|
|
lax_var.name,
|
|
"Name mismatch for #{markup.inspect}: " \
|
|
"lax=#{lax_var.name.inspect} strict=#{strict_var.name.inspect}"
|
|
assert_equal strict_var.filters.length,
|
|
lax_var.filters.length,
|
|
"Filter count mismatch for #{markup.inspect}: " \
|
|
"lax=#{lax_var.filters.inspect} strict=#{strict_var.filters.inspect}"
|
|
strict_var.filters.each_with_index do |(s_name, *), i|
|
|
l_name = lax_var.filters[i][0]
|
|
assert_equal s_name,
|
|
l_name,
|
|
"Filter name mismatch at index #{i} for #{markup.inspect}"
|
|
end
|
|
end
|
|
end
|
|
|
|
# Verify the fast path is actually taken for simple variables (i.e. filters is the
|
|
# shared frozen EMPTY_ARRAY, not a newly allocated array).
|
|
def test_fast_path_taken_for_simple_variable
|
|
ctx = Liquid::ParseContext.new(error_mode: :lax)
|
|
var = Liquid::Variable.new("product.title", ctx)
|
|
assert_same(
|
|
Liquid::Const::EMPTY_ARRAY,
|
|
var.filters,
|
|
"Expected fast path (frozen EMPTY_ARRAY) for simple variable",
|
|
)
|
|
end
|
|
|
|
def test_fast_path_taken_for_no_arg_filter
|
|
ctx = Liquid::ParseContext.new(error_mode: :lax)
|
|
var = Liquid::Variable.new("product | upcase", ctx)
|
|
assert_equal(1, var.filters.length)
|
|
assert_equal("upcase", var.filters[0][0])
|
|
# The no-arg filter tuple should come from NO_ARG_FILTER_CACHE (frozen)
|
|
assert_predicate(var.filters[0], :frozen?)
|
|
end
|
|
|
|
def test_fast_path_taken_for_single_arg_filter
|
|
ctx = Liquid::ParseContext.new(error_mode: :lax)
|
|
var = Liquid::Variable.new("product | truncate: 50", ctx)
|
|
assert_equal(1, var.filters.length)
|
|
assert_equal("truncate", var.filters[0][0])
|
|
assert_equal([50], var.filters[0][1])
|
|
end
|
|
|
|
# Keyword args must fall through to the Lexer — verify the result is still correct.
|
|
def test_keyword_arg_falls_to_lexer_and_parses_correctly
|
|
ctx = Liquid::ParseContext.new(error_mode: :lax)
|
|
var = Liquid::Variable.new("img | img_tag: class: 'hero'", ctx)
|
|
assert_equal(1, var.filters.length)
|
|
assert_equal("img_tag", var.filters[0][0])
|
|
end
|
|
|
|
# Numeric filter arguments: integers and floats
|
|
def test_numeric_filter_args
|
|
ctx = Liquid::ParseContext.new(error_mode: :lax)
|
|
|
|
int_var = Liquid::Variable.new("price | plus: 3", ctx)
|
|
assert_equal([3], int_var.filters[0][1])
|
|
|
|
neg_var = Liquid::Variable.new("price | minus: -1", ctx)
|
|
assert_equal([-1], neg_var.filters[0][1])
|
|
|
|
float_var = Liquid::Variable.new("price | round: 2.5", ctx)
|
|
assert_equal([2.5], float_var.filters[0][1])
|
|
end
|
|
end
|