# frozen_string_literal: truemoduleNetclassIMAP<ProtocolclassResponseParser# basic utility methods for parsing.## (internal API, subject to change)moduleParserUtils# :nodoc:moduleGenerator# :nodoc:LOOKAHEAD="(@token ||= next_token)"SHIFT_TOKEN="(@token = nil)"# we can skip lexer for single character matches, as a shortcutdefdef_char_matchers(name,char,token)byte=char.ordmatch_name=name.match(/\A[A-Z]/)?"#{name}!":namechar=char.dumpclass_eval<<~RUBY,__FILE__,__LINE__+1
# frozen_string_literal: true
# force use of #next_token; no string peeking
def lookahead_#{name}?
#{LOOKAHEAD}&.symbol == #{token}
end
# use token or string peek
def peek_#{name}?
@token ? @token.symbol == #{token} : @str.getbyte(@pos) == #{byte}
end
# like accept(token_symbols); returns token or nil
def #{name}?
if @token&.symbol == #{token}#{SHIFT_TOKEN}#{char}
elsif !@token && @str.getbyte(@pos) == #{byte}
@pos += 1
#{char}
end
end
# like match(token_symbols); returns token or raises parse_error
def #{match_name}
if @token&.symbol == #{token}#{SHIFT_TOKEN}#{char}
elsif !@token && @str.getbyte(@pos) == #{byte}
@pos += 1
#{char}
else
parse_error("unexpected %s (expected %p)",
@token&.symbol || @str[@pos].inspect, #{char})
end
end
RUBYend# TODO: move coersion to the token.value method?defdef_token_matchers(name,*token_symbols,coerce: nil,send: nil)match_name=name.match(/\A[A-Z]/)?"#{name}!":nameiftoken_symbols.size==1token=token_symbols.firstmatcher="token&.symbol == %p"%[token]desc=tokenelsematcher="%p.include? token&.symbol"%[token_symbols]desc=token_symbols.join(" or ")endvalue="(token.value)"value=coerce.to_s+valueifcoercevalue=[value,send].join(".")ifsendraise_parse_error=<<~RUBY
parse_error("unexpected %s (expected #{desc})", token&.symbol)
RUBYclass_eval<<~RUBY,__FILE__,__LINE__+1
# frozen_string_literal: true
# lookahead version of match, returning the value
def lookahead_#{name}!
token = #{LOOKAHEAD}
if #{matcher}#{value}
else
#{raise_parse_error}
end
end
def #{name}?
token = #{LOOKAHEAD}
if #{matcher}#{SHIFT_TOKEN}#{value}
end
end
def #{match_name}
token = #{LOOKAHEAD}
if #{matcher}#{SHIFT_TOKEN}#{value}
else
#{raise_parse_error}
end
end
RUBYendendprivate# TODO: after checking the lookahead, use a regexp for remaining chars.# That way a loop isn't needed.defcombine_adjacent(*tokens)result="".bwhiletoken=accept(*tokens)result<<token.valueendifresult.empty?parse_error('unexpected token %s (expected %s)',lookahead.symbol,tokens.join(" or "))endresultenddefmatch(*args)token=lookaheadunlessargs.include?(token.symbol)parse_error('unexpected token %s (expected %s)',token.symbol.id2name,args.collect{|i|i.id2name}.join(" or "))endshift_tokentokenend# like match, but does not raise error on failure.## returns and shifts token on successful match# returns nil and leaves @token unshifted on no matchdefaccept(*args)token=lookaheadifargs.include?(token.symbol)shift_tokentokenendend# To be used conditionally:# assert_no_lookahead if config.debug?defassert_no_lookahead@token.nil?orparse_error("assertion failed: expected @token.nil?, actual %s: %p",@token.symbol,@token.value)end# like accept, without consuming the tokendeflookahead?(*symbols)@tokenifsymbols.include?((@token||=next_token)&.symbol)enddeflookahead@token||=next_tokenend# like match, without consuming the tokendeflookahead!(*args)ifargs.include?((@token||=next_token)&.symbol)@tokenelseparse_error('unexpected token %s (expected %s)',@token&.symbol,args.join(" or "))endenddefpeek_str?(str)assert_no_lookaheadifconfig.debug?@str[@pos,str.length]==strenddefpeek_re?(re)assert_no_lookaheadifNet::IMAP.debugre.match?(@str,@pos)enddefpeek_re(re)assert_no_lookaheadifconfig.debug?re.match(@str,@pos)enddefaccept_re(re)assert_no_lookaheadifconfig.debug?re.match(@str,@pos)and@pos=$~.end(0)$~enddefmatch_re(re,name)assert_no_lookaheadifconfig.debug?ifre.match(@str,@pos)@pos=$~.end(0)$~elseparse_error("invalid #{name}")endenddefshift_token@token=nilenddefparse_error(fmt,*args)msg=format(fmt,*args)ifconfig.debug?local_path=File.dirname(__dir__)tok=@token?"%s: %p"%[@token.symbol,@token.value]:"nil"warn"%s %s: %s"%[self.class,__method__,msg]warn" tokenized : %s"%[@str[...@pos].dump]warn" remaining : %s"%[@str[@pos..].dump]warn" @lex_state: %s"%[@lex_state]warn" @pos : %d"%[@pos]warn" @token : %s"%[tok]caller_locations(1..20).each_with_indexdo|cloc,idx|nextunlesscloc.path&.start_with?(local_path)warn" caller[%2d]: %-30s (%s:%d)"%[idx,cloc.base_label,File.basename(cloc.path,".rb"),cloc.lineno]endendraiseResponseParseError,msgendendendendend