Skip to content

Commit d6809ff

Browse files
committed
🥅💄 Support highlights in parse error details
This runs sprintf in two passes: once to apply the escape sequences and again to interpolate variables. This requires `%%` for the second pass, which _can_ be confusing. Maybe this approach is too much for the very simple highlighting in this version? But, it seems to work okay for elaborate color schemes, too. IMO it's easier to read and maintain than a bunch of conditional string appending. And it's simpler than the other templating approaches that I considered.
1 parent 2f2b3fc commit d6809ff

2 files changed

Lines changed: 53 additions & 21 deletions

File tree

lib/net/imap/errors.rb

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ def response_size_msg
5555
# NOTE: Parser attributes are provided for debugging and inspection only.
5656
# Their names and semantics may change incompatibly in any release.
5757
class ResponseParseError < Error
58+
# returns "" for all highlights
59+
ESC_NO_HL = Hash.new("").freeze
60+
private_constant :ESC_NO_HL
61+
62+
# ANSI highlights, but no colors
63+
ESC_NO_COLOR = Hash.new("").update(
64+
reset: "\e[m",
65+
val: "\e[1m", # bold
66+
alt: "\e[1;4m", # bold and underlined
67+
).freeze
68+
private_constant :ESC_NO_COLOR
69+
5870
# Net::IMAP::ResponseParser, unless a custom parser produced the error.
5971
attr_reader :parser_class
6072

@@ -106,20 +118,21 @@ def initialize(message = "unspecified parse error",
106118
# Most parser method names are based on rules in the IMAP grammar.
107119
def detailed_message(parser_state: Net::IMAP.debug,
108120
parser_backtrace: false,
121+
highlight: false,
109122
**)
110123
return super unless parser_state || parser_backtrace
111124
msg = super.dup
125+
esc = highlight ? ESC_NO_COLOR : ESC_NO_HL
126+
hl = ->str { str % esc }
127+
val = ->str, val { val.nil? ? "nil" : str % esc % val }
112128
if parser_state && (string || pos || lex_state || token)
113-
msg << "\n processed : %p" % processed_string
114-
msg << "\n remaining : %p" % remaining_string
115-
msg << "\n pos : %p" % pos
116-
msg << "\n lex_state : %p" % lex_state
117-
msg << "\n token : "
118-
if token
119-
msg << "%p => %p" % [token.symbol, token.value]
120-
else
121-
msg << "nil"
122-
end
129+
msg << "\n processed : " << val["%{val}%%p%{reset}", processed_string]
130+
msg << "\n remaining : " << val["%{alt}%%p%{reset}", remaining_string]
131+
msg << "\n pos : " << val["%{val}%%p%{reset}", pos]
132+
msg << "\n lex_state : " << val["%{val}%%p%{reset}", lex_state]
133+
msg << "\n token : " << val[
134+
"%{val}%%<symbol>p%{reset} => %{val}%%<value>p%{reset}", token&.to_h
135+
]
123136
end
124137
if parser_backtrace
125138
backtrace_locations&.each_with_index do |loc, idx|
@@ -130,11 +143,10 @@ def detailed_message(parser_state: Net::IMAP.debug,
130143
else
131144
next unless loc.path&.include?("net/imap/response_parser")
132145
end
133-
msg << "\n caller[%2d]: %-30s (%s:%d)" % [
134-
idx,
135-
loc.base_label,
136-
File.basename(loc.path, ".rb"),
137-
loc.lineno
146+
msg << "\n %s: %s (%s:%d)" % [
147+
"caller[%2d]" % idx,
148+
hl["%{val}%%-30s%{reset}"] % loc.base_label,
149+
File.basename(loc.path, ".rb"), loc.lineno
138150
]
139151
end
140152
end

test/net/imap/test_errors.rb

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,11 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m
7474
MSG
7575
assert_equal(<<~MSG.strip, err.detailed_message(highlight: true))
7676
#{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET}
77-
processed : "tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""
78-
remaining : "] done\\r\\n"
79-
pos : 45
80-
lex_state : :EXPR_BEG
81-
token : :QUOTED => "Microsoft.Exchange.Error: foo"
77+
processed : #{BOLD}"tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""#{RESET}
78+
remaining : #{BOLD_UNDERLINE}"] done\\r\\n"#{RESET}
79+
pos : #{BOLD}45#{RESET}
80+
lex_state : #{BOLD}:EXPR_BEG#{RESET}
81+
token : #{BOLD}:QUOTED#{RESET} => #{BOLD}"Microsoft.Exchange.Error: foo"#{RESET}
8282
MSG
8383

8484
# `parser_state` defaults to `Net::IMAP.debug`:
@@ -89,14 +89,34 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m
8989
err.detailed_message(highlight: true)
9090
)
9191

92+
# with a nil token
93+
parser_state = [string, :EXPR_BEG, 45, nil]
94+
err = Net::IMAP::ResponseParseError.new(msg, string:, parser_state:)
95+
assert_equal(<<~MSG.strip, err.detailed_message(parser_state: true))
96+
#{msg} (#{name})
97+
processed : "tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""
98+
remaining : "] done\\r\\n"
99+
pos : 45
100+
lex_state : :EXPR_BEG
101+
token : nil
102+
MSG
103+
assert_equal(<<~MSG.strip, err.detailed_message(highlight: true, parser_state: true))
104+
#{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET}
105+
processed : #{BOLD}"tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""#{RESET}
106+
remaining : #{BOLD_UNDERLINE}"] done\\r\\n"#{RESET}
107+
pos : #{BOLD}45#{RESET}
108+
lex_state : #{BOLD}:EXPR_BEG#{RESET}
109+
token : nil
110+
MSG
111+
92112
# with parser_backtrace
93113
Net::IMAP.debug = false
94114
parser = Net::IMAP::ResponseParser.new
95115
error = parser.parse("* 123 FETCH (UNKNOWN ...)\r\n") rescue $!
96116
no_hl = error.detailed_message(parser_backtrace: true)
97117
no_color = error.detailed_message(parser_backtrace: true, highlight: true)
98118
assert_include no_hl, "caller[ 1]: %-30s (" % "msg_att"
99-
assert_include no_color, "caller[ 1]: %-30s (" % "msg_att"
119+
assert_include no_color, "caller[ 1]: #{BOLD}%-30s#{RESET} (" % "msg_att"
100120
end
101121

102122
test "ResponseTooLargeError" do

0 commit comments

Comments
 (0)