mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-20 19:30:47 -07:00
Merge pull request #419 from Shopify/liquid_error_line_numbers
Optional line numbers for liquid errors
This commit is contained in:
+2
-1
@@ -54,6 +54,7 @@ require 'liquid/interrupts'
|
|||||||
require 'liquid/strainer'
|
require 'liquid/strainer'
|
||||||
require 'liquid/expression'
|
require 'liquid/expression'
|
||||||
require 'liquid/context'
|
require 'liquid/context'
|
||||||
|
require 'liquid/parser_switching'
|
||||||
require 'liquid/tag'
|
require 'liquid/tag'
|
||||||
require 'liquid/block'
|
require 'liquid/block'
|
||||||
require 'liquid/document'
|
require 'liquid/document'
|
||||||
@@ -66,11 +67,11 @@ require 'liquid/standardfilters'
|
|||||||
require 'liquid/condition'
|
require 'liquid/condition'
|
||||||
require 'liquid/module_ex'
|
require 'liquid/module_ex'
|
||||||
require 'liquid/utils'
|
require 'liquid/utils'
|
||||||
|
require 'liquid/token'
|
||||||
|
|
||||||
# Load all the tags of the standard library
|
# Load all the tags of the standard library
|
||||||
#
|
#
|
||||||
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
|
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
|
||||||
|
|
||||||
require 'liquid/profiler'
|
require 'liquid/profiler'
|
||||||
require 'liquid/profiler/token'
|
|
||||||
require 'liquid/profiler/hooks'
|
require 'liquid/profiler/hooks'
|
||||||
|
|||||||
+5
-3
@@ -32,7 +32,8 @@ module Liquid
|
|||||||
|
|
||||||
# fetch the tag from registered blocks
|
# fetch the tag from registered blocks
|
||||||
if tag = Template.tags[$1]
|
if tag = Template.tags[$1]
|
||||||
new_tag = tag.parse($1, $2, tokens, @options)
|
markup = token.is_a?(Token) ? token.child($2) : $2
|
||||||
|
new_tag = tag.parse($1, markup, tokens, @options)
|
||||||
new_tag.line_number = token.line_number if token.is_a?(Token)
|
new_tag.line_number = token.line_number if token.is_a?(Token)
|
||||||
@blank &&= new_tag.blank?
|
@blank &&= new_tag.blank?
|
||||||
@nodelist << new_tag
|
@nodelist << new_tag
|
||||||
@@ -103,7 +104,8 @@ module Liquid
|
|||||||
|
|
||||||
def create_variable(token)
|
def create_variable(token)
|
||||||
token.scan(ContentOfVariable) do |content|
|
token.scan(ContentOfVariable) do |content|
|
||||||
return Variable.new(content.first, @options)
|
markup = token.is_a?(Token) ? token.child(content.first) : content.first
|
||||||
|
return Variable.new(markup, @options)
|
||||||
end
|
end
|
||||||
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
|
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
|
||||||
end
|
end
|
||||||
@@ -144,7 +146,7 @@ module Liquid
|
|||||||
rescue MemoryError => e
|
rescue MemoryError => e
|
||||||
raise e
|
raise e
|
||||||
rescue ::StandardError => e
|
rescue ::StandardError => e
|
||||||
output << (context.handle_error(e))
|
output << (context.handle_error(e, token))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -91,17 +91,15 @@ module Liquid
|
|||||||
@interrupts.pop
|
@interrupts.pop
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_error(e)
|
|
||||||
errors.push(e)
|
|
||||||
|
|
||||||
raise if exception_handler && exception_handler.call(e)
|
def handle_error(e, token=nil)
|
||||||
|
if e.is_a?(Liquid::Error)
|
||||||
case e
|
e.set_line_number_from_token(token)
|
||||||
when SyntaxError
|
|
||||||
"Liquid syntax error: #{e.message}"
|
|
||||||
else
|
|
||||||
"Liquid error: #{e.message}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
errors.push(e)
|
||||||
|
raise if exception_handler && exception_handler.call(e)
|
||||||
|
Liquid::Error.render(e)
|
||||||
end
|
end
|
||||||
|
|
||||||
def invoke(method, *args)
|
def invoke(method, *args)
|
||||||
|
|||||||
+48
-1
@@ -1,5 +1,52 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Error < ::StandardError; end
|
class Error < ::StandardError
|
||||||
|
attr_accessor :line_number
|
||||||
|
attr_accessor :markup_context
|
||||||
|
|
||||||
|
def to_s(with_prefix=true)
|
||||||
|
str = ""
|
||||||
|
str << message_prefix if with_prefix
|
||||||
|
str << super()
|
||||||
|
|
||||||
|
if markup_context
|
||||||
|
str << " "
|
||||||
|
str << markup_context
|
||||||
|
end
|
||||||
|
|
||||||
|
str
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_line_number_from_token(token)
|
||||||
|
return unless token.respond_to?(:line_number)
|
||||||
|
self.line_number = token.line_number
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.render(e)
|
||||||
|
if e.is_a?(Liquid::Error)
|
||||||
|
e.to_s
|
||||||
|
else
|
||||||
|
"Liquid error: #{e.to_s}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def message_prefix
|
||||||
|
str = ""
|
||||||
|
if is_a?(SyntaxError)
|
||||||
|
str << "Liquid syntax error"
|
||||||
|
else
|
||||||
|
str << "Liquid error"
|
||||||
|
end
|
||||||
|
|
||||||
|
if line_number
|
||||||
|
str << " (line #{line_number})"
|
||||||
|
end
|
||||||
|
|
||||||
|
str << ": "
|
||||||
|
str
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class ArgumentError < Error; end
|
class ArgumentError < Error; end
|
||||||
class ContextError < Error; end
|
class ContextError < Error; end
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
module Liquid
|
||||||
|
module ParserSwitching
|
||||||
|
def parse_with_selected_parser(markup)
|
||||||
|
case @options[:error_mode] || Template.error_mode
|
||||||
|
when :strict then strict_parse_with_error_context(markup)
|
||||||
|
when :lax then lax_parse(markup)
|
||||||
|
when :warn
|
||||||
|
begin
|
||||||
|
return strict_parse_with_error_context(markup)
|
||||||
|
rescue SyntaxError => e
|
||||||
|
e.set_line_number_from_token(markup)
|
||||||
|
@warnings ||= []
|
||||||
|
@warnings << e
|
||||||
|
return lax_parse(markup)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def strict_parse_with_error_context(markup)
|
||||||
|
strict_parse(markup)
|
||||||
|
rescue SyntaxError => e
|
||||||
|
e.markup_context = markup_context(markup)
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
|
||||||
|
def markup_context(markup)
|
||||||
|
"in \"#{markup.strip}\""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
+1
-24
@@ -2,6 +2,7 @@ module Liquid
|
|||||||
class Tag
|
class Tag
|
||||||
attr_accessor :options, :line_number
|
attr_accessor :options, :line_number
|
||||||
attr_reader :nodelist, :warnings
|
attr_reader :nodelist, :warnings
|
||||||
|
include ParserSwitching
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def parse(tag_name, markup, tokens, options)
|
def parse(tag_name, markup, tokens, options)
|
||||||
@@ -37,29 +38,5 @@ module Liquid
|
|||||||
def blank?
|
def blank?
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_with_selected_parser(markup)
|
|
||||||
case @options[:error_mode] || Template.error_mode
|
|
||||||
when :strict then strict_parse_with_error_context(markup)
|
|
||||||
when :lax then lax_parse(markup)
|
|
||||||
when :warn
|
|
||||||
begin
|
|
||||||
return strict_parse_with_error_context(markup)
|
|
||||||
rescue SyntaxError => e
|
|
||||||
@warnings ||= []
|
|
||||||
@warnings << e
|
|
||||||
return lax_parse(markup)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def strict_parse_with_error_context(markup)
|
|
||||||
strict_parse(markup)
|
|
||||||
rescue SyntaxError => e
|
|
||||||
e.message << " in \"#{markup.strip}\""
|
|
||||||
raise e
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ module Liquid
|
|||||||
def parse(source, options = {})
|
def parse(source, options = {})
|
||||||
@options = options
|
@options = options
|
||||||
@profiling = options[:profile]
|
@profiling = options[:profile]
|
||||||
|
@line_numbers = options[:line_numbers] || @profiling
|
||||||
@root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
|
@root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
|
||||||
@warnings = nil
|
@warnings = nil
|
||||||
self
|
self
|
||||||
@@ -224,7 +225,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def calculate_line_numbers(raw_tokens)
|
def calculate_line_numbers(raw_tokens)
|
||||||
return raw_tokens unless @profiling
|
return raw_tokens unless @line_numbers
|
||||||
|
|
||||||
current_line = 1
|
current_line = 1
|
||||||
raw_tokens.map do |token|
|
raw_tokens.map do |token|
|
||||||
@@ -248,6 +249,5 @@ module Liquid
|
|||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Token < String
|
class Token < String
|
||||||
|
|
||||||
attr_reader :line_number
|
attr_reader :line_number
|
||||||
|
|
||||||
def initialize(content, line_number)
|
def initialize(content, line_number)
|
||||||
@@ -12,5 +11,8 @@ module Liquid
|
|||||||
"<raw>"
|
"<raw>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def child(string)
|
||||||
|
Token.new(string, @line_number)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
+6
-15
@@ -15,30 +15,24 @@ module Liquid
|
|||||||
EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
|
EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
|
||||||
attr_accessor :filters, :name, :warnings
|
attr_accessor :filters, :name, :warnings
|
||||||
attr_accessor :line_number
|
attr_accessor :line_number
|
||||||
|
include ParserSwitching
|
||||||
|
|
||||||
def initialize(markup, options = {})
|
def initialize(markup, options = {})
|
||||||
@markup = markup
|
@markup = markup
|
||||||
@name = nil
|
@name = nil
|
||||||
@options = options || {}
|
@options = options || {}
|
||||||
|
|
||||||
case @options[:error_mode] || Template.error_mode
|
parse_with_selected_parser(markup)
|
||||||
when :strict then strict_parse(markup)
|
|
||||||
when :lax then lax_parse(markup)
|
|
||||||
when :warn
|
|
||||||
begin
|
|
||||||
strict_parse(markup)
|
|
||||||
rescue SyntaxError => e
|
|
||||||
@warnings ||= []
|
|
||||||
@warnings << e
|
|
||||||
lax_parse(markup)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def raw
|
def raw
|
||||||
@markup
|
@markup
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def markup_context(markup)
|
||||||
|
"in \"{{#{markup}}}\""
|
||||||
|
end
|
||||||
|
|
||||||
def lax_parse(markup)
|
def lax_parse(markup)
|
||||||
@filters = []
|
@filters = []
|
||||||
if markup =~ /\s*(#{QuotedFragment})(.*)/om
|
if markup =~ /\s*(#{QuotedFragment})(.*)/om
|
||||||
@@ -74,9 +68,6 @@ module Liquid
|
|||||||
@filters << [filtername, filterargs]
|
@filters << [filtername, filterargs]
|
||||||
end
|
end
|
||||||
p.consume(:end_of_string)
|
p.consume(:end_of_string)
|
||||||
rescue SyntaxError => e
|
|
||||||
e.message << " in \"{{#{markup}}}\""
|
|
||||||
raise e
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_filterargs(p)
|
def parse_filterargs(p)
|
||||||
|
|||||||
@@ -22,6 +22,39 @@ end
|
|||||||
class ErrorHandlingTest < Minitest::Test
|
class ErrorHandlingTest < Minitest::Test
|
||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
|
def test_templates_parsed_with_line_numbers_renders_them_in_errors
|
||||||
|
template = <<-LIQUID
|
||||||
|
Hello,
|
||||||
|
|
||||||
|
{{ errors.standard_error }} will raise a standard error.
|
||||||
|
|
||||||
|
Bla bla test.
|
||||||
|
|
||||||
|
{{ errors.syntax_error }} will raise a syntax error.
|
||||||
|
|
||||||
|
This is an argument error: {{ errors.argument_error }}
|
||||||
|
|
||||||
|
Bla.
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
expected = <<-TEXT
|
||||||
|
Hello,
|
||||||
|
|
||||||
|
Liquid error (line 3): standard error will raise a standard error.
|
||||||
|
|
||||||
|
Bla bla test.
|
||||||
|
|
||||||
|
Liquid syntax error (line 7): syntax error will raise a syntax error.
|
||||||
|
|
||||||
|
This is an argument error: Liquid error (line 9): argument error
|
||||||
|
|
||||||
|
Bla.
|
||||||
|
TEXT
|
||||||
|
|
||||||
|
output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new)
|
||||||
|
assert_equal expected, output
|
||||||
|
end
|
||||||
|
|
||||||
def test_standard_error
|
def test_standard_error
|
||||||
template = Liquid::Template.parse( ' {{ errors.standard_error }} ' )
|
template = Liquid::Template.parse( ' {{ errors.standard_error }} ' )
|
||||||
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
|
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
|
||||||
@@ -59,7 +92,7 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_lax_unrecognized_operator
|
def test_lax_unrecognized_operator
|
||||||
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :lax)
|
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :lax)
|
||||||
assert_equal ' Liquid error: Unknown operator =! ', template.render
|
assert_equal ' Liquid error: Unknown operator =! ', template.render
|
||||||
@@ -71,28 +104,37 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
err = assert_raises(SyntaxError) do
|
err = assert_raises(SyntaxError) do
|
||||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict)
|
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict)
|
||||||
end
|
end
|
||||||
assert_equal 'Unexpected character = in "1 =! 2"', err.message
|
assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message
|
||||||
|
|
||||||
err = assert_raises(SyntaxError) do
|
err = assert_raises(SyntaxError) do
|
||||||
Liquid::Template.parse('{{%%%}}', :error_mode => :strict)
|
Liquid::Template.parse('{{%%%}}', :error_mode => :strict)
|
||||||
end
|
end
|
||||||
assert_equal 'Unexpected character % in "{{%%%}}"', err.message
|
assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_warnings
|
def test_warnings
|
||||||
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', :error_mode => :warn)
|
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', :error_mode => :warn)
|
||||||
assert_equal 3, template.warnings.size
|
assert_equal 3, template.warnings.size
|
||||||
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].message
|
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)
|
||||||
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].message
|
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false)
|
||||||
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
|
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false)
|
||||||
assert_equal '', template.render
|
assert_equal '', template.render
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_warning_line_numbers
|
||||||
|
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", :error_mode => :warn, :line_numbers => true)
|
||||||
|
assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message
|
||||||
|
assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message
|
||||||
|
assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
|
||||||
|
assert_equal 3, template.warnings.size
|
||||||
|
assert_equal [1,2,3], template.warnings.map(&:line_number)
|
||||||
|
end
|
||||||
|
|
||||||
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
|
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
|
||||||
def test_exceptions_propagate
|
def test_exceptions_propagate
|
||||||
assert_raises Exception do
|
assert_raises Exception do
|
||||||
template = Liquid::Template.parse( ' {{ errors.exception }} ' )
|
template = Liquid::Template.parse('{{ errors.exception }}')
|
||||||
template.render('errors' => ErrorDrop.new)
|
template.render('errors' => ErrorDrop.new)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end # ErrorHandlingTest
|
end
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class IncludeTagTest < Minitest::Test
|
|||||||
|
|
||||||
Liquid::Template.file_system = infinite_file_system.new
|
Liquid::Template.file_system = infinite_file_system.new
|
||||||
|
|
||||||
assert_raises(Liquid::StackLevelError) do
|
assert_raises(Liquid::StackLevelError, SystemStackError) do
|
||||||
Template.parse("{% include 'loop' %}").render!
|
Template.parse("{% include 'loop' %}").render!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user