lib/solargraph/pin/base.rb



# frozen_string_literal: true


module Solargraph
  module Pin
    # The base class for map pins.

    #

    class Base
      include Common
      include Conversions
      include Documenting

      # @return [YARD::CodeObjects::Base]

      attr_reader :code_object

      # @return [Solargraph::Location]

      attr_reader :location

      # @return [Solargraph::Location]

      attr_reader :type_location

      # @return [String]

      attr_reader :name

      # @return [String]

      attr_reader :path

      # @return [::Symbol]

      attr_accessor :source

      # @param location [Solargraph::Location, nil]

      # @param type_location [Solargraph::Location, nil]

      # @param closure [Solargraph::Pin::Closure, nil]

      # @param name [String]

      # @param comments [String]

      def initialize location: nil, type_location: nil, closure: nil, name: '', comments: ''
        @location = location
        @type_location = type_location
        @closure = closure
        @name = name
        @comments = comments
      end

      # @return [String]

      def comments
        @comments ||= ''
      end

      # @param generics_to_resolve [Enumerable<String>]

      # @param return_type_context [ComplexType, nil]

      # @param context [ComplexType]

      # @param resolved_generic_values [Hash{String => ComplexType}]

      # @return [self]

      def resolve_generics_from_context(generics_to_resolve, return_type_context = nil, resolved_generic_values: {})
        proxy return_type.resolve_generics_from_context(generics_to_resolve,
                                                        return_type_context,
                                                        resolved_generic_values: resolved_generic_values)
      end

      # @yieldparam [ComplexType]

      # @yieldreturn [ComplexType]

      # @return [self]

      def transform_types(&transform)
        proxy return_type.transform(&transform)
      end

      # Determine the concrete type for each of the generic type

      # parameters used in this method based on the parameters passed

      # into the its class and return a new method pin.

      #

      # @param definitions [Pin::Namespace] The module/class which uses generic types

      # @param context_type [ComplexType] The receiver type

      # @return [self]

      def resolve_generics definitions, context_type
        transform_types { |t| t.resolve_generics(definitions, context_type) if t }
      end

      def all_rooted?
        !return_type || return_type.all_rooted?
      end

      # @param generics_to_erase [Enumerable<String>]

      # @return [self]

      def erase_generics(generics_to_erase)
        return self if generics_to_erase.empty?
        transform_types { |t| t.erase_generics(generics_to_erase) }
      end

      # @return [String, nil]

      def filename
        return nil if location.nil?
        location.filename
      end

      # @return [Integer]

      def completion_item_kind
        LanguageServer::CompletionItemKinds::KEYWORD
      end

      # @return [Integer, nil]

      def symbol_kind
        nil
      end

      def to_s
        to_rbs
      end

      # @return [Boolean]

      def variable?
        false
      end

      # @return [Location, nil]

      def best_location
        location || type_location
      end

      # Pin equality is determined using the #nearly? method and also

      # requiring both pins to have the same location.

      #

      def == other
        return false unless nearly? other
        comments == other.comments and location == other.location
      end

      # True if the specified pin is a near match to this one. A near match

      # indicates that the pins contain mostly the same data. Any differences

      # between them should not have an impact on the API surface.

      #

      # @param other [Solargraph::Pin::Base, Object]

      # @return [Boolean]

      def nearly? other
        self.class == other.class &&
          name == other.name &&
          (closure == other.closure || (closure && closure.nearly?(other.closure))) &&
          (comments == other.comments ||
            (((maybe_directives? == false && other.maybe_directives? == false) || compare_directives(directives, other.directives)) &&
            compare_docstring_tags(docstring, other.docstring))
          )
      end

      # The pin's return type.

      #

      # @return [ComplexType]

      def return_type
        @return_type ||= ComplexType::UNDEFINED
      end

      # @return [YARD::Docstring]

      def docstring
        parse_comments unless defined?(@docstring)
        @docstring ||= Solargraph::Source.parse_docstring('').to_docstring
      end

      # @return [::Array<YARD::Tags::Directive>]

      def directives
        parse_comments unless defined?(@directives)
        @directives
      end

      # @return [::Array<YARD::Tags::MacroDirective>]

      def macros
        @macros ||= collect_macros
      end

      # Perform a quick check to see if this pin possibly includes YARD

      # directives. This method does not require parsing the comments.

      #

      # After the comments have been parsed, this method will return false if

      # no directives were found, regardless of whether it previously appeared

      # possible.

      #

      # @return [Boolean]

      def maybe_directives?
        return !@directives.empty? if defined?(@directives)
        @maybe_directives ||= comments.include?('@!')
      end

      # @return [Boolean]

      def deprecated?
        @deprecated ||= docstring.has_tag?('deprecated')
      end

      # Get a fully qualified type from the pin's return type.

      #

      # The relative type is determined from YARD documentation (@return,

      # @param, @type, etc.) and its namespaces are fully qualified using the

      # provided ApiMap.

      #

      # @param api_map [ApiMap]

      # @return [ComplexType]

      def typify api_map
        return_type.qualify(api_map, namespace)
      end

      # Infer the pin's return type via static code analysis.

      #

      # @param api_map [ApiMap]

      # @return [ComplexType]

      def probe api_map
        typify api_map
      end

      # @deprecated Use #typify and/or #probe instead

      # @param api_map [ApiMap]

      # @return [ComplexType]

      def infer api_map
        Solargraph::Logging.logger.warn "WARNING: Pin #infer methods are deprecated. Use #typify or #probe instead."
        type = typify(api_map)
        return type unless type.undefined?
        probe api_map
      end

      # Try to merge data from another pin. Merges are only possible if the

      # pins are near matches (see the #nearly? method). The changes should

      # not have any side effects on the API surface.

      #

      # @param pin [Pin::Base] The pin to merge into this one

      # @return [Boolean] True if the pins were merged

      def try_merge! pin
        return false unless nearly?(pin)
        @location = pin.location
        @closure = pin.closure
        return true if comments == pin.comments
        @comments = pin.comments
        @docstring = pin.docstring
        @return_type = pin.return_type
        @documentation = nil
        @deprecated = nil
        reset_conversions
        true
      end

      def proxied?
        @proxied ||= false
      end

      def probed?
        @probed ||= false
      end

      # @param api_map [ApiMap]

      # @return [self]

      def realize api_map
        return self if return_type.defined?
        type = typify(api_map)
        return proxy(type) if type.defined?
        type = probe(api_map)
        return self if type.undefined?
        result = proxy(type)
        result.probed = true
        result
      end

      # Return a proxy for this pin with the specified return type. Other than

      # the return type and the #proxied? setting, the proxy should be a clone

      # of the original.

      #

      # @param return_type [ComplexType]

      # @return [self]

      def proxy return_type
        result = dup
        result.return_type = return_type
        result.proxied = true
        result
      end

      # @return [String]

      def identity
        @identity ||= "#{closure.path}|#{name}"
      end

      # @return [String, nil]

      def to_rbs
        return_type.to_rbs
      end

      # @return [String, nil]

      def desc
        if path
          if to_rbs
            path + ' ' + to_rbs
          else
            path
          end
        else
          to_rbs
        end
      end

      def inspect
        "#<#{self.class} `#{self.desc}` at #{self.location.inspect}>"
      end

      protected

      # @return [Boolean]

      attr_writer :probed

      # @return [Boolean]

      attr_writer :proxied

      # @return [ComplexType]

      attr_writer :return_type

      private

      # @return [void]

      def parse_comments
        # HACK: Avoid a NoMethodError on nil with empty overload tags

        if comments.nil? || comments.empty? || comments.strip.end_with?('@overload')
          @docstring = nil
          @directives = []
        else
          # HACK: Pass a dummy code object to the parser for plugins that

          # expect it not to be nil

          parse = Solargraph::Source.parse_docstring(comments)
          @docstring = parse.to_docstring
          @directives = parse.directives
        end
      end

      # True if two docstrings have the same tags, regardless of any other

      # differences.

      #

      # @param d1 [YARD::Docstring]

      # @param d2 [YARD::Docstring]

      # @return [Boolean]

      def compare_docstring_tags d1, d2
        return false if d1.tags.length != d2.tags.length
        d1.tags.each_index do |i|
          return false unless compare_tags(d1.tags[i], d2.tags[i])
        end
        true
      end

      # @param dir1 [::Array<YARD::Tags::Directive>]

      # @param dir2 [::Array<YARD::Tags::Directive>]

      # @return [Boolean]

      def compare_directives dir1, dir2
        return false if dir1.length != dir2.length
        dir1.each_index do |i|
          return false unless compare_tags(dir1[i].tag, dir2[i].tag)
        end
        true
      end

      # @param tag1 [YARD::Tags::Tag]

      # @param tag2 [YARD::Tags::Tag]

      # @return [Boolean]

      def compare_tags tag1, tag2
        tag1.class == tag2.class &&
          tag1.tag_name == tag2.tag_name &&
          tag1.text == tag2.text &&
          tag1.name == tag2.name &&
          tag1.types == tag2.types
      end

      # @return [::Array<YARD::Tags::Handlers::Directive>]

      def collect_macros
        return [] unless maybe_directives?
        parse = Solargraph::Source.parse_docstring(comments)
        parse.directives.select{ |d| d.tag.tag_name == 'macro' }
      end
    end
  end
end