lib/solargraph/source_map.rb



# frozen_string_literal: true


require 'yard'
require 'solargraph/yard_tags'

module Solargraph
  # An index of Pins and other ApiMap-related data for a single Source

  # that can be queried.

  #

  class SourceMap
    autoload :Mapper,        'solargraph/source_map/mapper'
    autoload :Clip,          'solargraph/source_map/clip'
    autoload :Completion,    'solargraph/source_map/completion'
    autoload :Data,          'solargraph/source_map/data'

    # @return [Source]

    attr_reader :source

    # @return [Array<Pin::Base>]

    def pins
      data.pins
    end

    # @return [Array<Pin::LocalVariable>]

    def locals
      data.locals
    end

    # @param source [Source]

    def initialize source
      @source = source

      environ.merge Convention.for_local(self) unless filename.nil?
      self.convention_pins = environ.pins
      @pin_select_cache = {}
    end

    # @param klass [Class]

    # @return [Array<Pin::Base>]

    def pins_by_class klass
      @pin_select_cache[klass] ||= pin_class_hash.select { |key, _| key <= klass }.values.flatten
    end

    # A hash representing the state of the source map's API.

    #

    # ApiMap#catalog uses this value to determine whether it needs to clear its

    # cache.

    #

    # @return [Integer]

    def api_hash
      @api_hash ||= (pins_by_class(Pin::Constant) + pins_by_class(Pin::Namespace).select { |pin| pin.namespace.to_s > '' } + pins_by_class(Pin::Reference) + pins_by_class(Pin::Method).map(&:node) + locals).hash
    end

    # @return [String]

    def filename
      source.filename
    end

    # @return [String]

    def code
      source.code
    end

    # @return [Array<Pin::Reference::Require>]

    def requires
      pins_by_class(Pin::Reference::Require)
    end

    # @return [Environ]

    def environ
      @environ ||= Environ.new
    end

    # all pins except Solargraph::Pin::Reference::Reference

    # @return [Array<Pin::Base>]

    def document_symbols
      @document_symbols ||= (pins + convention_pins).select do |pin|
        pin.path && !pin.path.empty?
      end
    end

    # @param query [String]

    # @return [Array<Pin::Base>]

    def query_symbols query
      Pin::Search.new(document_symbols, query).results
    end

    # @param position [Position]

    # @return [Source::Cursor]

    def cursor_at position
      Source::Cursor.new(source, position)
    end

    # @param path [String]

    # @return [Pin::Base]

    def first_pin path
      pins.select { |p| p.path == path }.first
    end

    # @param location [Solargraph::Location]

    # @return [Array<Solargraph::Pin::Base>]

    def locate_pins location
      # return nil unless location.start_with?("#{filename}:")

      (pins + locals).select { |pin| pin.location == location }
    end

    # @param line [Integer]

    # @param character [Integer]

    # @return [Pin::Method,Pin::Namespace]

    def locate_named_path_pin line, character
      _locate_pin line, character, Pin::Namespace, Pin::Method
    end

    # @param line [Integer]

    # @param character [Integer]

    # @return [Pin::Namespace,Pin::Method,Pin::Block]

    def locate_block_pin line, character
      _locate_pin line, character, Pin::Namespace, Pin::Method, Pin::Block
    end

    # @todo Candidate for deprecation

    #

    # @param other_map [SourceMap]

    # @return [Boolean]

    def try_merge! other_map
      return false if pins.length != other_map.pins.length || locals.length != other_map.locals.length || requires.map(&:name).uniq.sort != other_map.requires.map(&:name).uniq.sort

      pins.each_index do |i|
        return false unless pins[i].try_merge!(other_map.pins[i])
      end
      locals.each_index do |i|
        return false unless locals[i].try_merge!(other_map.locals[i])
      end
      @source = other_map.source
      true
    end

    # @param name [String]

    # @return [Array<Location>]

    def references name
      source.references name
    end

    # @param location [Location]

    # @return [Array<Pin::LocalVariable>]

    def locals_at(location)
      return [] if location.filename != filename
      closure = locate_named_path_pin(location.range.start.line, location.range.start.character)
      locals.select { |pin| pin.visible_at?(closure, location) }
    end

    class << self
      # @param filename [String]

      # @return [SourceMap]

      def load filename
        source = Solargraph::Source.load(filename)
        SourceMap.map(source)
      end

      # @param code [String]

      # @param filename [String, nil]

      # @return [SourceMap]

      def load_string code, filename = nil
        source = Solargraph::Source.load_string(code, filename)
        SourceMap.map(source)
      end

      # @deprecated

      # @param source [Source]

      # @return [SourceMap]

      def map source
        new(source)
      end
    end

    private

    def pin_class_hash
      @pin_class_hash ||= pins.to_set.classify(&:class).transform_values(&:to_a)
    end

    def data
      @data ||= Data.new(source)
    end

    # @return [Array<Pin::Base>]

    def convention_pins
      @convention_pins || []
    end

    # @param pins [Array<Pin::Base>]

    # @return [Array<Pin::Base>]

    def convention_pins=(pins)
      # unmemoizing the document_symbols in case it was called from any of conventions

      @document_symbols = nil
      @convention_pins = pins
    end

    # @param line [Integer]

    # @param character [Integer]

    # @param klasses [Array<Class>]

    # @return [Pin::Base, nil]

    def _locate_pin line, character, *klasses
      position = Position.new(line, character)
      found = nil
      pins.each do |pin|
        # @todo Attribute pins should not be treated like closures, but

        #   there's probably a better way to handle it

        next if pin.is_a?(Pin::Method) && pin.attribute?
        found = pin if (klasses.empty? || klasses.any? { |kls| pin.is_a?(kls) } ) && pin.location.range.contain?(position)
        break if pin.location.range.start.line > line
      end
      # Assuming the root pin is always valid

      found || pins.first
    end
  end
end