lib/solargraph/source/chain.rb



# frozen_string_literal: true


# HACK: Fix autoload issue

require 'solargraph/source/chain/link'

module Solargraph
  class Source
    # A chain of constants, variables, and method calls for inferring types of

    # values.

    #

    class Chain
      autoload :Link,             'solargraph/source/chain/link'
      autoload :Call,             'solargraph/source/chain/call'
      autoload :Variable,         'solargraph/source/chain/variable'
      autoload :ClassVariable,    'solargraph/source/chain/class_variable'
      autoload :Constant,         'solargraph/source/chain/constant'
      autoload :InstanceVariable, 'solargraph/source/chain/instance_variable'
      autoload :GlobalVariable,   'solargraph/source/chain/global_variable'
      autoload :Literal,          'solargraph/source/chain/literal'
      autoload :Head,             'solargraph/source/chain/head'
      autoload :Or,               'solargraph/source/chain/or'
      autoload :BlockVariable,    'solargraph/source/chain/block_variable'
      autoload :ZSuper,           'solargraph/source/chain/z_super'

      @@inference_stack = []
      @@inference_depth = 0

      UNDEFINED_CALL = Chain::Call.new('<undefined>')
      UNDEFINED_CONSTANT = Chain::Constant.new('<undefined>')

      # @return [Array<Source::Chain::Link>]

      attr_reader :links

      attr_reader :node

      # @param links [Array<Chain::Link>]

      def initialize links, node = nil, splat = false
        @links = links.clone
        @links.push UNDEFINED_CALL if @links.empty?
        head = true
        @links.map! do |link|
          result = (head ? link.clone_head : link.clone_body)
          head = false
          result
        end
        @node = node
        @splat = splat
      end

      # @return [Chain]

      def base
        @base ||= Chain.new(links[0..-2])
      end

      # @param api_map [ApiMap]

      # @param name_pin [Pin::Base]

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

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

      def define api_map, name_pin, locals
        return [] if undefined?
        working_pin = name_pin
        links[0..-2].each do |link|
          pins = link.resolve(api_map, working_pin, locals)
          # Locals are only used when resolving the first link

          # @todo There's a problem here. Call links need to resolve arguments

          #   that might refer to local variables.

          # locals = []

          type = infer_first_defined(pins, working_pin, api_map)
          return [] if type.undefined?
          working_pin = Pin::ProxyType.anonymous(type)
        end
        links.last.last_context = working_pin
        links.last.resolve(api_map, working_pin, locals)
      end

      # @param api_map [ApiMap]

      # @param name_pin [Pin::Base]

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

      # @return [ComplexType]

      def infer api_map, name_pin, locals
        pins = define(api_map, name_pin, locals)
        infer_first_defined(pins, links.last.last_context, api_map)
      end

      # @return [Boolean]

      def literal?
        links.last.is_a?(Chain::Literal)
      end

      def undefined?
        links.any?(&:undefined?)
      end

      def defined?
        !undefined?
      end

      # @return [Boolean]

      def constant?
        links.last.is_a?(Chain::Constant)
      end

      def splat?
        @splat
      end

      private

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

      # @param api_map [ApiMap]

      # @return [ComplexType]

      def infer_first_defined pins, context, api_map
        type = ComplexType::UNDEFINED
        pins.each do |pin|
          # Avoid infinite recursion

          next if @@inference_stack.include?(pin.identity)
          @@inference_stack.push pin.identity
          type = pin.typify(api_map)
          @@inference_stack.pop
          break if type.defined?
        end
        if type.undefined?
          # Limit method inference recursion

          return type if @@inference_depth >= 10 && pins.first.is_a?(Pin::BaseMethod)
          @@inference_depth += 1
          pins.each do |pin|
            # Avoid infinite recursion

            next if @@inference_stack.include?(pin.identity)
            @@inference_stack.push pin.identity
            type = pin.probe(api_map)
            @@inference_stack.pop
            break if type.defined?
          end
          @@inference_depth -= 1
        end
        return type if context.nil? || context.return_type.undefined?
        type.self_to(context.return_type.namespace)
      end
    end
  end
end