lib/steep/annotation_parser.rb



module Steep
  class AnnotationParser
    VAR_NAME = /[a-z][A-Za-z0-9_]*/
    METHOD_NAME = Regexp.union(
      /[^.:\s]+/
    )
    CONST_NAME = Regexp.union(
      /(::)?([A-Z][A-Za-z0-9_]*::)*[A-Z][A-Za-z0-9_]*/
    )
    DYNAMIC_NAME = /(self\??\.)?#{METHOD_NAME}/
    IVAR_NAME = /@[^:\s]+/

    attr_reader :factory

    def initialize(factory:)
      @factory = factory
    end

    class SyntaxError < StandardError
      attr_reader :source
      attr_reader :location

      def initialize(source:, location:, exn: nil, message: nil)
        @source = source
        @location = location

        if exn
          message =
            case exn
            when RBS::ParsingError
              Diagnostic::Signature::SyntaxError.parser_syntax_error_message(exn)
            else
              exn.message
            end
        end

        super message
      end
    end

    TYPE = /(?<type>.*)/
    COLON = /\s*:\s*/

    PARAM = /[A-Z][A-Za-z0-9_]*/
    TYPE_PARAMS = /(\[(?<params>#{PARAM}(,\s*#{PARAM})*)\])?/

    def parse_type(match, name = :type, location:)
      string = match[name] or raise
      st, en = match.offset(name)
      st or raise
      en or raise
      loc = RBS::Location.new(location.buffer, location.start_pos + st, location.start_pos + en)

      type =
        begin
          RBS::Parser.parse_type(string)
        rescue RBS::ParsingError => exn
          raise SyntaxError.new(source: string, location: loc, exn: exn)
        end or raise

      unless (type.location || raise).source == string.strip
        raise SyntaxError.new(source: string, location: loc, message: "Failed to parse a type in annotation")
      end

      factory.type(type)
    end

    def keyword_subject_type(keyword, name)
      /@type\s+#{keyword}\s+(?<name>#{name})#{COLON}#{TYPE}/
    end

    def keyword_and_type(keyword)
      /@type\s+#{keyword}#{COLON}#{TYPE}/
    end

    def parse(src, location:)
      case src
      when keyword_subject_type("var", VAR_NAME)
        Regexp.last_match.yield_self do |match|
          match or raise
          name = match[:name] or raise

          AST::Annotation::VarType.new(name: name.to_sym,
                                       type: parse_type(match, location: location),
                                       location: location)
        end

      when keyword_subject_type("method", METHOD_NAME)
        Regexp.last_match.yield_self do |match|
          match or raise
          name = match[:name] or raise
          type = match[:type] or raise

          method_type = factory.method_type(RBS::Parser.parse_method_type(type) || raise)

          AST::Annotation::MethodType.new(name: name.to_sym,
                                          type: method_type,
                                          location: location)
        end

      when keyword_subject_type("const", CONST_NAME)
        Regexp.last_match.yield_self do |match|
          match or raise
          name = match[:name] or raise
          type = parse_type(match, location: location)

          AST::Annotation::ConstType.new(name: TypeName(name), type: type, location: location)
        end

      when keyword_subject_type("ivar", IVAR_NAME)
        Regexp.last_match.yield_self do |match|
          match or raise
          name = match[:name] or raise
          type = parse_type(match, location: location)

          AST::Annotation::IvarType.new(name: name.to_sym,
                                         type: type,
                                         location: location)
        end

      when keyword_and_type("return")
        Regexp.last_match.yield_self do |match|
          match or raise
          type = parse_type(match, location: location)
          AST::Annotation::ReturnType.new(type: type, location: location)
        end

      when keyword_and_type("block")
        Regexp.last_match.yield_self do |match|
          match or raise
          type = parse_type(match, location: location)
          AST::Annotation::BlockType.new(type: type, location: location)
        end

      when keyword_and_type("self")
        Regexp.last_match.yield_self do |match|
          match or raise
          type = parse_type(match, location: location)
          AST::Annotation::SelfType.new(type: type, location: location)
        end

      when keyword_and_type("instance")
        Regexp.last_match.yield_self do |match|
          match or raise
          type = parse_type(match, location: location)
          AST::Annotation::InstanceType.new(type: type, location: location)
        end

      when keyword_and_type("module")
        Regexp.last_match.yield_self do |match|
          match or raise
          type = parse_type(match, location: location)
          AST::Annotation::ModuleType.new(type: type, location: location)
        end

      when keyword_and_type("break")
        Regexp.last_match.yield_self do |match|
          match or raise
          type = parse_type(match, location: location)

          AST::Annotation::BreakType.new(type: type, location: location)
        end

      when /@dynamic\s+(?<names>(#{DYNAMIC_NAME}\s*,\s*)*#{DYNAMIC_NAME})/
        Regexp.last_match.yield_self do |match|
          match or raise
          names = (match[:names] || raise).split(/\s*,\s*/)

          AST::Annotation::Dynamic.new(
            names: names.map {|name|
              case
              when name.delete_prefix!("self.")
                AST::Annotation::Dynamic::Name.new(name: name.to_sym, kind: :module)
              when name.delete_prefix!("self?.")
                AST::Annotation::Dynamic::Name.new(name: name.to_sym, kind: :module_instance)
              else
                AST::Annotation::Dynamic::Name.new(name: name.to_sym, kind: :instance)
              end
            },
            location: location
          )
        end

      when /@implements\s+(?<name>#{CONST_NAME})#{TYPE_PARAMS}$/
        Regexp.last_match.yield_self do |match|
          match or raise
          type_name = TypeName(match[:name] || raise)
          params = match[:params]&.yield_self {|params| params.split(/,/).map {|param| param.strip.to_sym } } || []

          name = AST::Annotation::Implements::Module.new(name: type_name, args: params)
          AST::Annotation::Implements.new(name: name, location: location)
        end
      end

    rescue RBS::ParsingError => exn
      raise SyntaxError.new(source: src, location: location, exn: exn)
    end
  end
end