From 8fb7441de4b15593703e1257490cb16cbc7fa7e8 Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 10 Feb 2026 15:37:43 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=85=F0=9F=92=84=20Support=20highlights?= =?UTF-8?q?=20in=20parse=20error=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/net/imap/errors.rb | 42 +++++++++++++++++++++++------------- test/net/imap/test_errors.rb | 32 +++++++++++++++++++++------ 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/lib/net/imap/errors.rb b/lib/net/imap/errors.rb index a64cab41..f7751d13 100644 --- a/lib/net/imap/errors.rb +++ b/lib/net/imap/errors.rb @@ -55,6 +55,18 @@ def response_size_msg # NOTE: Parser attributes are provided for debugging and inspection only. # Their names and semantics may change incompatibly in any release. class ResponseParseError < Error + # returns "" for all highlights + ESC_NO_HL = Hash.new("").freeze + private_constant :ESC_NO_HL + + # ANSI highlights, but no colors + ESC_NO_COLOR = Hash.new("").update( + reset: "\e[m", + val: "\e[1m", # bold + alt: "\e[1;4m", # bold and underlined + ).freeze + private_constant :ESC_NO_COLOR + # Net::IMAP::ResponseParser, unless a custom parser produced the error. attr_reader :parser_class @@ -106,20 +118,21 @@ def initialize(message = "unspecified parse error", # Most parser method names are based on rules in the IMAP grammar. def detailed_message(parser_state: Net::IMAP.debug, parser_backtrace: false, + highlight: false, **) return super unless parser_state || parser_backtrace msg = super.dup + esc = highlight ? ESC_NO_COLOR : ESC_NO_HL + hl = ->str { str % esc } + val = ->str, val { val.nil? ? "nil" : str % esc % val } if parser_state && (string || pos || lex_state || token) - msg << "\n processed : %p" % processed_string - msg << "\n remaining : %p" % remaining_string - msg << "\n pos : %p" % pos - msg << "\n lex_state : %p" % lex_state - msg << "\n token : " - if token - msg << "%p => %p" % [token.symbol, token.value] - else - msg << "nil" - end + msg << "\n processed : " << val["%{val}%%p%{reset}", processed_string] + msg << "\n remaining : " << val["%{alt}%%p%{reset}", remaining_string] + msg << "\n pos : " << val["%{val}%%p%{reset}", pos] + msg << "\n lex_state : " << val["%{val}%%p%{reset}", lex_state] + msg << "\n token : " << val[ + "%{val}%%p%{reset} => %{val}%%p%{reset}", token&.to_h + ] end if parser_backtrace backtrace_locations&.each_with_index do |loc, idx| @@ -130,11 +143,10 @@ def detailed_message(parser_state: Net::IMAP.debug, else next unless loc.path&.include?("net/imap/response_parser") end - msg << "\n caller[%2d]: %-30s (%s:%d)" % [ - idx, - loc.base_label, - File.basename(loc.path, ".rb"), - loc.lineno + msg << "\n %s: %s (%s:%d)" % [ + "caller[%2d]" % idx, + hl["%{val}%%-30s%{reset}"] % loc.base_label, + File.basename(loc.path, ".rb"), loc.lineno ] end end diff --git a/test/net/imap/test_errors.rb b/test/net/imap/test_errors.rb index 3c0b70e7..b9a83a2a 100644 --- a/test/net/imap/test_errors.rb +++ b/test/net/imap/test_errors.rb @@ -74,11 +74,11 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m MSG assert_equal(<<~MSG.strip, err.detailed_message(highlight: true)) #{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET} - processed : "tag OK [Error=\\"Microsoft.Exchange.Error: foo\\"" - remaining : "] done\\r\\n" - pos : 45 - lex_state : :EXPR_BEG - token : :QUOTED => "Microsoft.Exchange.Error: foo" + processed : #{BOLD}"tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""#{RESET} + remaining : #{BOLD_UNDERLINE}"] done\\r\\n"#{RESET} + pos : #{BOLD}45#{RESET} + lex_state : #{BOLD}:EXPR_BEG#{RESET} + token : #{BOLD}:QUOTED#{RESET} => #{BOLD}"Microsoft.Exchange.Error: foo"#{RESET} MSG # `parser_state` defaults to `Net::IMAP.debug`: @@ -89,6 +89,26 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m err.detailed_message(highlight: true) ) + # with a nil token + parser_state = [string, :EXPR_BEG, 45, nil] + err = Net::IMAP::ResponseParseError.new(msg, string:, parser_state:) + assert_equal(<<~MSG.strip, err.detailed_message(parser_state: true)) + #{msg} (#{name}) + processed : "tag OK [Error=\\"Microsoft.Exchange.Error: foo\\"" + remaining : "] done\\r\\n" + pos : 45 + lex_state : :EXPR_BEG + token : nil + MSG + assert_equal(<<~MSG.strip, err.detailed_message(highlight: true, parser_state: true)) + #{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET} + processed : #{BOLD}"tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""#{RESET} + remaining : #{BOLD_UNDERLINE}"] done\\r\\n"#{RESET} + pos : #{BOLD}45#{RESET} + lex_state : #{BOLD}:EXPR_BEG#{RESET} + token : nil + MSG + # with parser_backtrace Net::IMAP.debug = false parser = Net::IMAP::ResponseParser.new @@ -96,7 +116,7 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m no_hl = error.detailed_message(parser_backtrace: true) no_color = error.detailed_message(parser_backtrace: true, highlight: true) assert_include no_hl, "caller[ 1]: %-30s (" % "msg_att" - assert_include no_color, "caller[ 1]: %-30s (" % "msg_att" + assert_include no_color, "caller[ 1]: #{BOLD}%-30s#{RESET} (" % "msg_att" end test "ResponseTooLargeError" do