lib/yard/handlers/ruby/class_handler.rb



# frozen_string_literal: true
# Handles class declarations
class YARD::Handlers::Ruby::ClassHandler < YARD::Handlers::Ruby::Base
  include YARD::Handlers::Ruby::StructHandlerMethods
  handles :class, :sclass
  namespace_only

  process do
    classname = statement[0].source.gsub(/\s/, '')
    if statement.type == :class
      superclass = parse_superclass(statement[1])
      if superclass == "Struct"
        is_a_struct = true
        superclass = struct_superclass_name(statement[1]) # refine the superclass if possible
        create_struct_superclass(superclass, statement[1])
      end
      undocsuper = statement[1] && superclass.nil?
      klass = register ClassObject.new(namespace, classname) do |o|
        o.superclass = superclass if superclass
        o.superclass.type = :class if o.superclass.is_a?(Proxy)
      end
      if is_a_struct
        parse_struct_superclass(klass, statement[1])
      elsif klass
        create_attributes(klass, members_from_tags(klass))
      end
      parse_block(statement[2], :namespace => klass)

      if undocsuper
        raise YARD::Parser::UndocumentableError, 'superclass (class was added without superclass)'
      end
    elsif statement.type == :sclass
      if statement[0] == s(:var_ref, s(:kw, "self"))
        parse_block(statement[1], :namespace => namespace, :scope => :class)
      else
        proxy = Proxy.new(namespace, classname)

        # Allow constants to reference class names
        if ConstantObject === proxy
          if proxy.value =~ /\A#{NAMESPACEMATCH}\Z/
            proxy = Proxy.new(namespace, proxy.value)
          else
            raise YARD::Parser::UndocumentableError, "constant class reference '#{classname}'"
          end
        end

        if classname[0, 1] =~ /[A-Z]/
          register ClassObject.new(namespace, classname) if Proxy === proxy
          parse_block(statement[1], :namespace => proxy, :scope => :class)
        else
          raise YARD::Parser::UndocumentableError, "class '#{classname}'"
        end
      end
    else
      sig_end = (statement[1] ? statement[1].source_end : statement[0].source_end) - statement.source_start
      raise YARD::Parser::UndocumentableError, "class: #{statement.source[0..sig_end]}"
    end
  end

  private

  # Extract the parameters from the Struct.new AST node, returning them as a list
  # of strings
  #
  # @param [MethodCallNode] superclass the AST node for the Struct.new call
  # @return [Array<String>] the member names to generate methods for
  def extract_parameters(superclass)
    members = superclass.parameters.select {|x| x && x.type == :symbol_literal }
    members.map! {|x| x.source.strip[1..-1] }
    members
  end

  def create_struct_superclass(superclass, superclass_def)
    return if superclass == "Struct"
    the_super = register ClassObject.new(P("Struct"), superclass[8..-1]) do |o|
      o.superclass = "Struct"
    end
    parse_struct_superclass(the_super, superclass_def)
    the_super
  end

  def struct_superclass_name(superclass)
    if superclass.call?
      first = superclass.parameters.first
      if first.type == :string_literal && first[0].type == :string_content && first[0].size == 1
        return "Struct::#{first[0][0][0]}"
      end
    end
    "Struct"
  end

  def parse_struct_superclass(klass, superclass)
    return unless superclass.call? && superclass.parameters
    members = extract_parameters(superclass)
    create_attributes(klass, members)
  end

  def parse_superclass(superclass)
    return nil unless superclass

    case superclass.type
    when :var_ref
      return namespace.path if superclass.first == s(:kw, "self")
      return superclass.source if superclass.first.type == :const
    when :const, :const_ref, :const_path_ref, :top_const_ref
      return superclass.source
    when :fcall, :command
      methname = superclass.method_name.source
      return superclass.parameters.first.source if methname == "DelegateClass"
      return methname if superclass.method_name.type == :const
    when :call, :command_call
      cname = superclass.namespace.source
      if cname =~ /^O?Struct$/ && superclass.method_name(true) == :new
        return cname
      end
    end
    nil
  end
end