lib/rubocop/cop/gemspec/dependency_version.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Gemspec
      # Enforce that gem dependency version specifications or a commit reference (branch,
      # ref, or tag) are either required or forbidden.
      #
      # @example EnforcedStyle: required (default)
      #
      #   # bad
      #   Gem::Specification.new do |spec|
      #     spec.add_dependency 'parser'
      #   end
      #
      #   # bad
      #   Gem::Specification.new do |spec|
      #     spec.add_development_dependency 'parser'
      #   end
      #
      #   # good
      #   Gem::Specification.new do |spec|
      #     spec.add_dependency 'parser', '>= 2.3.3.1', '< 3.0'
      #   end
      #
      #   # good
      #   Gem::Specification.new do |spec|
      #     spec.add_development_dependency 'parser', '>= 2.3.3.1', '< 3.0'
      #   end
      #
      # @example EnforcedStyle: forbidden
      #
      #   # bad
      #   Gem::Specification.new do |spec|
      #     spec.add_dependency 'parser', '>= 2.3.3.1', '< 3.0'
      #   end
      #
      #   # bad
      #   Gem::Specification.new do |spec|
      #     spec.add_development_dependency 'parser', '>= 2.3.3.1', '< 3.0'
      #   end
      #
      #   # good
      #   Gem::Specification.new do |spec|
      #     spec.add_dependency 'parser'
      #   end
      #
      #   # good
      #   Gem::Specification.new do |spec|
      #     spec.add_development_dependency 'parser'
      #   end
      #
      class DependencyVersion < Base
        include ConfigurableEnforcedStyle
        include GemspecHelp

        REQUIRED_MSG = 'Dependency version specification is required.'
        FORBIDDEN_MSG = 'Dependency version specification is forbidden.'
        VERSION_SPECIFICATION_REGEX = /^\s*[~<>=]*\s*[0-9.]+/.freeze

        # @!method add_dependency_method_declarations(node)
        def_node_search :add_dependency_method_declarations, <<~PATTERN
          (send
            (lvar #match_block_variable_name?) #add_dependency_method? ...)
        PATTERN

        # @!method includes_version_specification?(node)
        def_node_matcher :includes_version_specification?, <<~PATTERN
          (send _ #add_dependency_method? <(str #version_specification?) ...>)
        PATTERN

        # @!method includes_commit_reference?(node)
        def_node_matcher :includes_commit_reference?, <<~PATTERN
          (send _ #add_dependency_method? <(hash <(pair (sym {:branch :ref :tag}) (str _)) ...>) ...>)
        PATTERN

        def on_new_investigation
          return if processed_source.blank?

          add_dependency_method_nodes.each do |node|
            next if allowed_gem?(node)

            if offense?(node)
              add_offense(node)
              opposite_style_detected
            else
              correct_style_detected
            end
          end
        end

        private

        def allowed_gem?(node)
          allowed_gems.include?(node.first_argument.value)
        end

        def allowed_gems
          Array(cop_config['AllowedGems'])
        end

        def message(range)
          gem_specification = range.source

          if required_style?
            format(REQUIRED_MSG, gem_specification: gem_specification)
          elsif forbidden_style?
            format(FORBIDDEN_MSG, gem_specification: gem_specification)
          end
        end

        def match_block_variable_name?(receiver_name)
          gem_specification(processed_source.ast) do |block_variable_name|
            return block_variable_name == receiver_name
          end
        end

        def add_dependency_method?(method_name)
          method_name.to_s.end_with?('_dependency')
        end

        def add_dependency_method_nodes
          add_dependency_method_declarations(processed_source.ast)
        end

        def offense?(node)
          required_offense?(node) || forbidden_offense?(node)
        end

        def required_offense?(node)
          return unless required_style?

          !includes_version_specification?(node) && !includes_commit_reference?(node)
        end

        def forbidden_offense?(node)
          return unless forbidden_style?

          includes_version_specification?(node) || includes_commit_reference?(node)
        end

        def forbidden_style?
          style == :forbidden
        end

        def required_style?
          style == :required
        end

        def version_specification?(expression)
          expression.match?(VERSION_SPECIFICATION_REGEX)
        end
      end
    end
  end
end