1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
|
module Parslet
module ErrorReporter
# A reporter that tries to improve on the deepest error reporter by
# using heuristics to find the most relevant error and provide more
# context.
# The heuristic chooses the deepest error when parsing a sequence for which
# no alternative parsed successfully.
#
# Given the following parser:
#
# root(:call)
#
# rule(:call, label: 'call') {
# identifier >> str('.') >> method
# }
#
# rule(:method, label: 'method call') {
# identifier >> str('(') >> arguments.maybe >> str(')')
# }
#
# rule(:identifier, label: 'identifier') {
# match['[:alnum:]'].repeat(1)
# }
#
# rule(:arguments, label: 'method call arguments') {
# argument >> str(',') >> arguments | argument
# }
#
# rule(:argument) {
# call | identifier
# }
#
# and the following source:
#
# foo.bar(a,goo.baz(),c,)
#
# The contextual reporter returns the following causes:
#
# 0: Failed to match sequence (identifier '.' method call) at line 1 char 5
# when parsing method call arguments.
# 1: Failed to match sequence (identifier '(' method call arguments? ')') at
# line 1 char 22 when parsing method call arguments.
# 2: Failed to match [[:alnum:]] at line 1 char 23 when parsing method call
# arguments.
#
# (where 2 is a child cause of 1 and 1 a child cause of 0)
#
# The last piece used by the reporter is the (newly introduced) ability
# to attach a label to rules that describe a sequence in the grammar. The
# labels are used in two places:
# - In the "to_s" of Atom::Base so that any error message uses labels to
# refer to atoms
# - In the cause error messages to give information about which expression
# failed to parse
#
class Contextual < Deepest
def initialize
@last_reset_pos = 0
reset
end
# A sequence expression successfully parsed, reset all errors reported
# for previous expressions in the sequence (an alternative matched)
# Only reset errors if the position of the source that matched is higher
# than the position of the source that was last successful (so we keep
# errors that are the "deepest" but for which no alternative succeeded)
#
def succ(source)
source_pos = source.pos.bytepos
return if source_pos < @last_reset_pos
@last_reset_pos = source_pos
reset
end
# Reset deepest error and its position and sequence index
#
def reset
@deepest_cause = nil
@label_pos = -1
end
# Produces an error cause that combines the message at the current level
# with the errors that happened at a level below (children).
# Compute and set label used by Cause to produce error message.
#
# @param atom [Parslet::Atoms::Base] parslet that failed
# @param source [Source] Source that we're using for this parse. (line
# number information...)
# @param message [String, Array] Error message at this level.
# @param children [Array] A list of errors from a deeper level (or nil).
# @return [Cause] An error tree combining children with message.
#
def err(atom, source, message, children=nil)
cause = super(atom, source, message, children)
if (label = atom.respond_to?(:label) && atom.label)
update_label(label, source.pos.bytepos)
cause.set_label(@label)
end
cause
end
# Update error message label if given label is more relevant.
# A label is more relevant if the position of the matched source is
# bigger.
#
# @param label [String] label to apply if more relevant
# @param bytepos [Integer] position in source code of matched source
#
def update_label(label, bytepos)
if bytepos >= @label_pos
@label_pos = bytepos
@label = label
end
end
end
end
end
|