lib/rbs/locator.rb



# frozen_string_literal: true

module RBS
  class Locator
    attr_reader :decls, :dirs, :buffer

    def initialize(buffer:, dirs:, decls:)
      @buffer = buffer
      @dirs = dirs
      @decls = decls
    end

    def find(line:, column:)
      pos = buffer.loc_to_pos([line, column])

      dirs.each do |dir|
        array = [] #: Array[component]
        find_in_directive(pos, dir, array) and return array
      end

      decls.each do |decl|
        array = [] #: Array[component]
        find_in_decl(pos, decl: decl, array: array) and return array
      end

      []
    end

    def find2(line:, column:)
      path = find(line: line, column: column)

      return if path.empty?

      hd, *tl = path
      if hd.is_a?(Symbol)
        [hd, tl]
      else
        [nil, path]
      end
    end

    def find_in_directive(pos, dir, array)
      return false unless dir.is_a?(AST::Directives::Use)

      if test_loc(pos, location: dir.location)
        array.unshift(dir)

        dir.clauses.each do |clause|
          if test_loc(pos, location: clause.location)
            array.unshift(clause)
            find_in_loc(pos, location: clause.location, array: array)
            return true
          end
        end
      end

      false
    end

    def find_in_decl(pos, decl:, array:)
      if test_loc(pos, location: decl.location)
        array.unshift(decl)

        case decl
        when AST::Declarations::Class
          decl.type_params.each do |param|
            find_in_type_param(pos, type_param: param, array: array) and return true
          end

          if super_class = decl.super_class
            if test_loc(pos, location: super_class.location)
              array.unshift(super_class)
              find_in_loc(pos, array: array, location: super_class.location)
              return true
            end
          end

          decl.each_decl do |decl_|
            find_in_decl(pos, decl: decl_, array: array) and return true
          end

          decl.each_member do |member|
            find_in_member(pos, array: array, member: member) and return true
          end

        when AST::Declarations::Module
          decl.type_params.each do |param|
            find_in_type_param(pos, type_param: param, array: array) and return true
          end

          decl.self_types.each do |self_type|
            if test_loc(pos, location: self_type.location)
              array.unshift(self_type)
              find_in_loc(pos, array: array, location: self_type.location)
              return true
            end
          end

          decl.each_decl do |decl_|
            find_in_decl(pos, decl: decl_, array: array) and return true
          end

          decl.each_member do |member|
            find_in_member(pos, array: array, member: member) and return true
          end

        when AST::Declarations::Interface
          decl.type_params.each do |param|
            find_in_type_param(pos, type_param: param, array: array) and return true
          end

          decl.members.each do |member|
            find_in_member(pos, array: array, member: member) and return true
          end

        when AST::Declarations::Constant, AST::Declarations::Global
          find_in_type(pos, array: array, type: decl.type) and return true

        when AST::Declarations::TypeAlias
          find_in_type(pos, array: array, type: decl.type) and return true
        end

        find_in_loc(pos, location: decl.location, array: array)

        true
      else
        false
      end
    end

    def find_in_member(pos, member:, array:)
      if test_loc(pos, location: member.location)
        array.unshift(member)

        case member
        when AST::Members::MethodDefinition
          member.overloads.each do |overload|
            find_in_method_type(pos, array: array, method_type: overload.method_type) and return true
          end
        when AST::Members::InstanceVariable, AST::Members::ClassInstanceVariable, AST::Members::ClassVariable
          find_in_type(pos, array: array, type: member.type) and return true
        when AST::Members::AttrReader, AST::Members::AttrWriter, AST::Members::AttrAccessor
          find_in_type(pos, array: array, type: member.type) and return true
        end

        find_in_loc(pos, location: member.location, array: array)

        true
      else
        false
      end
    end

    def find_in_method_type(pos, method_type:, array:)
      if test_loc(pos, location: method_type.location)
        array.unshift(method_type)

        method_type.type_params.each do |param|
          find_in_type_param(pos, type_param: param, array: array) and return true
        end

        method_type.each_type do |type|
          find_in_type(pos, array: array, type: type) and break
        end

        true
      else
        false
      end
    end

    def find_in_type_param(pos, type_param:, array:)
      if test_loc(pos, location: type_param.location)
        array.unshift(type_param)

        if upper_bound = type_param.upper_bound_type
          find_in_type(pos, type: upper_bound, array: array) and return true
        end

        if default_type = type_param.default_type
          find_in_type(pos, type: default_type, array: array) and return true
        end

        find_in_loc(pos, location: type_param.location, array: array)

        true
      else
        false
      end
    end

    def find_in_type(pos, type:, array:)
      if test_loc(pos, location: type.location)
        array.unshift(type)

        type.each_type do |type_|
          find_in_type(pos, array: array, type: type_) and return true
        end

        find_in_loc(pos, array: array, location: type.location)

        true
      else
        false
      end
    end

    def find_in_loc(pos, location:, array:)
      if test_loc(pos, location: location)
        if location.is_a?(Location)
          location.each_optional_key do |key|
            if loc = location[key]
              if loc.range === pos
                array.unshift(key)
                return true
              end
            end
          end

          location.each_required_key do |key|
            loc = location[key] or raise
            if loc.range === pos
              array.unshift(key)
              return true
            end
          end
        end

        true
      else
        false
      end
    end

    def test_loc(pos, location:)
      if location
        location.range === pos
      else
        false
      end
    end
  end
end