bbatsov/rubocop

View on GitHub
lib/rubocop/cop/gemspec/require_mfa.rb

Summary

Maintainability
A
35 mins
Test Coverage
# frozen_string_literal: true

module RuboCop
  module Cop
    module Gemspec
      # Requires a gemspec to have `rubygems_mfa_required` metadata set.
      #
      # This setting tells RubyGems that MFA (Multi-Factor Authentication) is
      # required for accounts to be able perform privileged operations, such as
      # (see RubyGems' documentation for the full list of privileged
      # operations):
      #
      # * `gem push`
      # * `gem yank`
      # * `gem owner --add/remove`
      # * adding or removing owners using gem ownership page
      #
      # This helps make your gem more secure, as users can be more
      # confident that gem updates were pushed by maintainers.
      #
      # @example
      #   # bad
      #   Gem::Specification.new do |spec|
      #     # no `rubygems_mfa_required` metadata specified
      #   end
      #
      #   # good
      #   Gem::Specification.new do |spec|
      #     spec.metadata = {
      #       'rubygems_mfa_required' => 'true'
      #     }
      #   end
      #
      #   # good
      #   Gem::Specification.new do |spec|
      #     spec.metadata['rubygems_mfa_required'] = 'true'
      #   end
      #
      #   # bad
      #   Gem::Specification.new do |spec|
      #     spec.metadata = {
      #       'rubygems_mfa_required' => 'false'
      #     }
      #   end
      #
      #   # good
      #   Gem::Specification.new do |spec|
      #     spec.metadata = {
      #       'rubygems_mfa_required' => 'true'
      #     }
      #   end
      #
      #   # bad
      #   Gem::Specification.new do |spec|
      #     spec.metadata['rubygems_mfa_required'] = 'false'
      #   end
      #
      #   # good
      #   Gem::Specification.new do |spec|
      #     spec.metadata['rubygems_mfa_required'] = 'true'
      #   end
      #
      class RequireMFA < Base
        include GemspecHelp
        extend AutoCorrector

        MSG = "`metadata['rubygems_mfa_required']` must be set to `'true'`."

        # @!method metadata(node)
        def_node_matcher :metadata, <<~PATTERN
          `{
            (send _ :metadata= $_)
            (send (send _ :metadata) :[]= (str "rubygems_mfa_required") $_)
          }
        PATTERN

        # @!method rubygems_mfa_required(node)
        def_node_search :rubygems_mfa_required, <<~PATTERN
          (pair (str "rubygems_mfa_required") $_)
        PATTERN

        # @!method true_string?(node)
        def_node_matcher :true_string?, <<~PATTERN
          (str "true")
        PATTERN

        def on_block(node) # rubocop:disable Metrics/MethodLength, InternalAffairs/NumblockHandler
          gem_specification(node) do |block_var|
            metadata_value = metadata(node)
            mfa_value = mfa_value(metadata_value)

            if mfa_value
              unless true_string?(mfa_value)
                add_offense(mfa_value) do |corrector|
                  change_value(corrector, mfa_value)
                end
              end
            else
              add_offense(node) do |corrector|
                autocorrect(corrector, node, block_var, metadata_value)
              end
            end
          end
        end

        private

        def mfa_value(metadata_value)
          return unless metadata_value
          return metadata_value if metadata_value.str_type?

          rubygems_mfa_required(metadata_value).first
        end

        def autocorrect(corrector, node, block_var, metadata)
          if metadata
            return unless metadata.hash_type?

            correct_metadata(corrector, metadata)
          else
            insert_mfa_required(corrector, node, block_var)
          end
        end

        def correct_metadata(corrector, metadata)
          if metadata.pairs.any?
            corrector.insert_after(metadata.pairs.last, ",\n'rubygems_mfa_required' => 'true'")
          else
            corrector.insert_before(metadata.loc.end, "'rubygems_mfa_required' => 'true'")
          end
        end

        def insert_mfa_required(corrector, node, block_var)
          corrector.insert_before(node.loc.end, <<~RUBY)
            #{block_var}.metadata['rubygems_mfa_required'] = 'true'
          RUBY
        end

        def change_value(corrector, value)
          corrector.replace(value, "'true'")
        end
      end
    end
  end
end