# frozen_string_literal: true
module YARD
module Handlers
module Ruby::Legacy
# This is the base handler for the legacy parser. To implement a legacy
# handler, subclass this class.
#
# @abstract (see Ruby::Base)
class Base < Handlers::Base
# For tokens like TkDEF, TkCLASS, etc.
include YARD::Parser::Ruby::Legacy::RubyToken
# @return [Boolean] whether or not a {Parser::Ruby::Legacy::Statement} object should be handled
# by this handler.
def self.handles?(stmt)
handlers.any? do |a_handler|
case a_handler
when String
stmt.tokens.first.text == a_handler
when Regexp
stmt.tokens.to_s =~ a_handler
else
a_handler == stmt.tokens.first.class
end
end
end
# Parses a statement's block with a set of state values. If the
# statement has no block, nothing happens. A description of state
# values can be found at {Handlers::Base#push_state}
#
# @param [Hash] opts State options
# @option opts (see Handlers::Base#push_state)
# @see Handlers::Base#push_state #push_state
def parse_block(opts = {})
push_state(opts) do
if statement.block
blk = Parser::Ruby::Legacy::StatementList.new(statement.block)
parser.process(blk)
end
end
end
def call_params
if statement.tokens.first.is_a?(TkDEF)
extract_method_details.last.map(&:first)
else
tokens = statement.tokens[1..-1]
tokval_list(tokens, :attr, :identifier, TkId).map(&:to_s)
end
end
def caller_method
if statement.tokens.first.is_a?(TkIDENTIFIER)
statement.tokens.first.text
elsif statement.tokens.first.is_a?(TkDEF)
extract_method_details.first
end
end
private
# Extracts method information for macro expansion only
#
# @todo This is a duplicate implementation of {MethodHandler}. Refactor.
# @return [Array<String,Array<Array<String>>>] the method name followed by method
# arguments (name and optional value)
def extract_method_details
if statement.tokens.to_s =~ /^def\s+(#{METHODMATCH})(?:(?:\s+|\s*\()(.*)(?:\)\s*$)?)?/m
meth = $1
args = $2
meth.gsub!(/\s+/, '')
args = tokval_list(Parser::Ruby::Legacy::TokenList.new(args), :all)
args.map! {|a| k, v = *a.split('=', 2); [k.strip, (v ? v.strip : nil)] } if args
meth = $` if meth =~ /(?:#{NSEPQ}|#{CSEPQ})([^#{NSEP}#{CSEPQ}]+)$/
[meth, args]
end
end
# The string value of a token. For example, the return value for the symbol :sym
# would be :sym. The return value for a string +"foo #{ bar}"+ would be the literal
# +"foo #{ bar}"+ without any interpolation. The return value of the identifier
# 'test' would be the same value: 'test'. Here is a list of common types and
# their return values:
#
# @example
# tokval(TokenList.new('"foo"').first) => "foo"
# tokval(TokenList.new(':foo').first) => :foo
# tokval(TokenList.new('CONSTANT').first, RubyToken::TkId) => "CONSTANT"
# tokval(TokenList.new('identifier').first, RubyToken::TkId) => "identifier"
# tokval(TokenList.new('3.25').first) => 3.25
# tokval(TokenList.new('/xyz/i').first) => /xyz/i
#
# @param [Token] token The token of the class
#
# @param [Array<Class<Token>>, Symbol] accepted_types
# The allowed token types that this token can be. Defaults to [{TkVal}].
# A list of types would be, for example, [+TkSTRING+, +TkSYMBOL+], to return
# the token's value if it is either of those types. If +TkVal+ is accepted,
# +TkNode+ is also accepted.
#
# Certain symbol keys are allowed to specify multiple types in one fell swoop.
# These symbols are:
# :string => +TkSTRING+, +TkDSTRING+, +TkDXSTRING+ and +TkXSTRING+
# :attr => +TkSYMBOL+ and +TkSTRING+
# :identifier => +TkIDENTIFIER, +TkFID+ and +TkGVAR+.
# :number => +TkFLOAT+, +TkINTEGER+
#
# @return [Object] if the token is one of the accepted types, in its real value form.
# It should be noted that identifiers and constants are kept in String form.
# @return [nil] if the token is not any of the specified accepted types
def tokval(token, *accepted_types)
accepted_types = [TkVal] if accepted_types.empty?
accepted_types.push(TkNode) if accepted_types.include? TkVal
if accepted_types.include?(:attr)
accepted_types.push(TkSTRING, TkSYMBOL)
end
if accepted_types.include?(:string)
accepted_types.push(TkSTRING, TkDSTRING, TkXSTRING, TkDXSTRING)
end
if accepted_types.include?(:identifier)
accepted_types.push(TkIDENTIFIER, TkFID, TkGVAR)
end
if accepted_types.include?(:number)
accepted_types.push(TkFLOAT, TkINTEGER)
end
return unless accepted_types.any? {|t| t === token }
case token
when TkSTRING, TkDSTRING, TkXSTRING, TkDXSTRING
token.text[1..-2]
when TkSYMBOL
token.text[1..-1].to_sym
when TkFLOAT
token.text.to_f
when TkINTEGER
token.text.to_i
when TkREGEXP
token.text =~ %r{\A/(.+)/([^/])\Z}
Regexp.new($1, $2)
when TkTRUE
true
when TkFALSE
false
when TkNIL
nil
else
token.text
end
end
# Returns a list of symbols or string values from a statement.
# The list must be a valid comma delimited list, and values
# will only be returned to the end of the list only.
#
# Example:
# attr_accessor :a, 'b', :c, :d => ['a', 'b', 'c', 'd']
# attr_accessor 'a', UNACCEPTED_TYPE, 'c' => ['a', 'c']
#
# The tokval list of a {Parser::Ruby::Legacy::TokenList} of the above
# code would be the {#tokval} value of :a, 'b',
# :c and :d.
#
# It should also be noted that this function stops immediately at
# any ruby keyword encountered:
# "attr_accessor :a, :b, :c if x == 5" => ['a', 'b', 'c']
#
# @param [TokenList] tokenlist The list of tokens to process.
# @param [Array<Class<Token>>] accepted_types passed to {#tokval}
# @return [Array<String>] the list of tokvalues in the list.
# @return [Array<EMPTY>] if there are no symbols or Strings in the list
# @see #tokval
def tokval_list(tokenlist, *accepted_types)
return [] unless tokenlist
out = [[]]
parencount = 0
beforeparen = 0
needcomma = false
seen_comma = true
tokenlist.each do |token|
tokval = accepted_types == [:all] ? token.text : tokval(token, *accepted_types)
parencond = !out.last.empty? && !tokval.nil?
# puts "#{seen_comma.inspect} #{parencount} #{token.class.class_name} #{out.inspect}"
case token
when TkCOMMA
if parencount == 0
out << [] unless out.last.empty?
needcomma = false
seen_comma = true
elsif parencond
out.last << token.text
end
when TkLPAREN
if seen_comma
beforeparen += 1
else
parencount += 1
out.last << token.text if parencond
end
when TkRPAREN
if beforeparen > 0
beforeparen -= 1
else
out.last << token.text if parencount > 0 && !tokval.nil?
parencount -= 1
end
when TkLBRACE, TkLBRACK, TkDO
parencount += 1
out.last << token.text unless tokval.nil?
when TkRBRACE, TkRBRACK, TkEND
out.last << token.text unless tokval.nil?
parencount -= 1
else
break if TkKW === token && ![TkTRUE, TkFALSE, TkSUPER, TkSELF, TkNIL].include?(token.class)
seen_comma = false unless TkWhitespace === token
if parencount == 0
next if needcomma
next if TkWhitespace === token
if !tokval.nil?
out.last << tokval
else
out.last.clear
needcomma = true
end
elsif parencond
needcomma = true
out.last << token.text
end
end
break if beforeparen == 0 && parencount < 0
end
# Flatten any single element lists
out.map {|e| e.empty? ? nil : (e.size == 1 ? e.pop : e.flatten.join) }.compact
end
end
end
end
end