yujinakayama/transpec

View on GitHub
lib/transpec/cli/option_parser.rb

Summary

Maintainability
B
5 hrs
Test Coverage
# coding: utf-8

require 'transpec/config'
require 'transpec/git'
require 'transpec/version'
require 'optparse'
require 'rainbow'
require 'rainbow/ext/string' unless String.respond_to?(:color)

module Transpec
  class CLI
    class OptionParser # rubocop:disable ClassLength
      VALID_BOOLEAN_MATCHER_TYPES = %w(truthy,falsey truthy,falsy true,false)

      attr_reader :config

      def initialize(config = Config.new)
        @config = config
        setup_parser
      end

      def parse(args)
        args = convert_deprecated_options(args)
        @parser.parse!(args)
        args
      end

      def help
        @parser.help
      end

      private

      def setup_parser # rubocop:disable MethodLength
        @parser = create_parser

        define_option('-f', '--force') do
          config.forced = true
        end

        define_option('-c', '--rspec-command COMMAND') do |command|
          config.rspec_command = command
        end

        define_option('-k', '--keep TYPE[,TYPE...]') do |types|
          configure_conversion(types, false)
        end

        define_option('-v', '--convert TYPE[,TYPE...]') do |types|
          configure_conversion(types, true)
        end

        define_option('-o', '--convert-only TYPE[,TYPE...]') do |types|
          Config.conversion_types.each do |type|
            config.conversion[type] = false
          end

          configure_conversion(types, true)
        end

        define_option('-s', '--skip-dynamic-analysis') do
          config.skip_dynamic_analysis = true
        end

        define_option('-n', '--negative-form FORM') do |form|
          config.negative_form_of_to = form
        end

        define_option('-b', '--boolean-matcher TYPE') do |type|
          configure_boolean_matcher(type)
        end

        define_option('-e', '--explicit-spec-type') do
          config.add_explicit_type_metadata_to_example_group = true
        end

        define_option('-a', '--no-yield-any-instance') do
          config.add_receiver_arg_to_any_instance_implementation_block = false
        end

        define_option('-p', '--no-parens-matcher-arg') do
          config.parenthesize_matcher_arg = false
        end

        define_option('--no-color') do
          Rainbow.enabled = false
        end

        define_option('--version') do
          puts Version.to_s
          exit
        end
      end

      def create_parser
        banner = "Usage: transpec [options] [files or directories]\n\n"
        summary_width = 34
        indentation = ' ' * 2
        ::OptionParser.new(banner, summary_width, indentation)
      end

      def define_option(*options, &block)
        description_lines = descriptions[options.first]
        description_lines = description_lines.map { |line| highlight_text(line) }
        @parser.on(*options, *description_lines, &block)
      end

      # rubocop:disable AlignHash
      def descriptions # rubocop:disable MethodLength
        @descriptions ||= {
          '-f' => [
            'Force processing even if the current Git repository is not',
            'clean.'
          ],
          '-s' => [
            'Skip dynamic analysis and convert with only static analysis.',
            'The use of this option is basically *discouraged* since it',
            'significantly decreases the overall conversion accuracy.'
          ],
          '-c' => [
            'Specify a command to run your specs that is used for dynamic',
            'analysis.',
            'Default: "bundle exec rspec"'
          ],
          '-k' => [
            'Keep specific syntaxes by disabling conversions.',
            'Conversion Types:',
            '  *should* (to `expect(obj).to`)',
            '  *oneliner* (`it { should ... }` to `it { is_expected.to ... }`)',
            '  *should_receive* (to `expect(obj).to receive`)',
            '  *stub*  (to `allow(obj).to receive`)',
            '  *have_items* (to `expect(collection.size).to eq(n)`)',
            "  *its* (to `describe '#attr' { subject { }; it { } }`)",
            '  *pending* (to `skip`)',
            '  *deprecated* (all other deprecated syntaxes to latest syntaxes)',
            'These conversions are enabled by default.'
          ],
          '-v' => [
            'Enable specific conversions that are disabled by default.',
            'Conversion Types:',
            '  *example_group* (`describe` to `RSpec.describe`)',
            '  *hook_scope* (`before(:all)` to `before(:context)`)',
            '  *stub_with_hash* (`obj.stub(:msg => val)` to',
            '                  `allow(obj).to receive(:msg).and_return(val)`)',
            'These conversions are disabled by default.'
          ],
          '-o' => [
            'Convert specific syntaxes while keeping all other syntaxes.'
          ],
          '-n' => [
            'Specify a negative form of `to` that is used in the',
            '`expect(...).to syntax. Either *not_to* or *to_not*.',
            'Default: *not_to*'
          ],
          '-b' => [
            'Specify a matcher type that `be_true` and `be_false` will be',
            'converted to.',
            '  *truthy,falsey* (conditional semantics)',
            '  *truthy,falsy*  (alias of `falsey`)',
            '  *true,false*    (exact equality)',
            'Default: *truthy,falsey*'
          ],
          '-e' => [
            'Add explicit `:type` metadata to example groups in a project',
            'using rspec-rails.'
          ],
          '-a' => [
            'Suppress yielding receiver instances to `any_instance`',
            'implementation blocks as the first block argument.'
          ],
          '-p' => [
            'Suppress parenthesizing arguments of matchers when converting',
            '`should` with operator matcher to `expect` with non-operator',
            'matcher. Note that it will be parenthesized even if this option',
            'is specified when parentheses are necessary to keep the meaning',
            'of the expression. By default, arguments of the following',
            'operator matchers will be parenthesized.',
            '  `== 10` to `eq(10)`',
            '  `=~ /pattern/` to `match(/pattern/)`',
            '  `=~ [1, 2]` to `match_array([1, 2])`'
          ],
          '--no-color' => [
            'Disable color in the output.'
          ],
          '--version' => [
            'Show Transpec version.'
          ]
        }
      end
      # rubocop:enable AlignHash

      def highlight_text(text)
        text.gsub(/`.+?`/) { |code| code.delete('`').underline }
          .gsub(/\*.+?\*/) { |code| code.delete('*').bright }
      end

      def convert_deprecated_options(raw_args)
        raw_args.each_with_object([]) do |arg, args|
          case arg
          when '--no-parentheses-matcher-arg'
            deprecate('--no-parentheses-matcher-arg option', '--no-parens-matcher-arg')
            args << '--no-parens-matcher-arg'
          else
            args << arg
          end
        end
      end

      def deprecate(subject, alternative = nil)
        message =  "DEPRECATION: #{subject} is deprecated."
        message << " Use #{alternative} instead." if alternative
        warn message
      end

      def configure_conversion(inputted_types, boolean)
        inputted_types.split(',').each do |type|
          unless Config.valid_conversion_type?(type)
            fail ArgumentError, "Unknown syntax type #{type.inspect}"
          end

          config.conversion[type] = boolean
        end
      end

      def configure_boolean_matcher(type)
        unless VALID_BOOLEAN_MATCHER_TYPES.include?(type)
          types = VALID_BOOLEAN_MATCHER_TYPES.map(&:inspect).join(', ')
          fail ArgumentError, "Boolean matcher type must be any of #{types}"
        end

        config.boolean_matcher_type = type.include?('truthy') ? :conditional : :exact
        config.form_of_be_falsey = type.include?('falsy') ? 'be_falsy' : 'be_falsey'
      end
    end
  end
end