lib/rubocop/cop/style/special_global_vars.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Style
      #
      # This cop looks for uses of Perl-style global variables.
      #
      # @example EnforcedStyle: use_english_names (default)
      #   # good
      #   puts $LOAD_PATH
      #   puts $LOADED_FEATURES
      #   puts $PROGRAM_NAME
      #   puts $ERROR_INFO
      #   puts $ERROR_POSITION
      #   puts $FIELD_SEPARATOR # or $FS
      #   puts $OUTPUT_FIELD_SEPARATOR # or $OFS
      #   puts $INPUT_RECORD_SEPARATOR # or $RS
      #   puts $OUTPUT_RECORD_SEPARATOR # or $ORS
      #   puts $INPUT_LINE_NUMBER # or $NR
      #   puts $LAST_READ_LINE
      #   puts $DEFAULT_OUTPUT
      #   puts $DEFAULT_INPUT
      #   puts $PROCESS_ID # or $PID
      #   puts $CHILD_STATUS
      #   puts $LAST_MATCH_INFO
      #   puts $IGNORECASE
      #   puts $ARGV # or ARGV
      #   puts $MATCH
      #   puts $PREMATCH
      #   puts $POSTMATCH
      #   puts $LAST_PAREN_MATCH
      #
      # @example EnforcedStyle: use_perl_names
      #   # good
      #   puts $:
      #   puts $"
      #   puts $0
      #   puts $!
      #   puts $@
      #   puts $;
      #   puts $,
      #   puts $/
      #   puts $\
      #   puts $.
      #   puts $_
      #   puts $>
      #   puts $<
      #   puts $$
      #   puts $?
      #   puts $~
      #   puts $=
      #   puts $*
      #   puts $&
      #   puts $`
      #   puts $'
      #   puts $+
      #
      class SpecialGlobalVars < Cop
        include ConfigurableEnforcedStyle

        MSG_BOTH = 'Prefer `%<prefer>s` from the stdlib \'English\' ' \
        'module (don\'t forget to require it) or `%<regular>s` over ' \
        '`%<global>s`.'
        MSG_ENGLISH = 'Prefer `%<prefer>s` from the stdlib \'English\' ' \
        'module (don\'t forget to require it) over `%<global>s`.'
        MSG_REGULAR = 'Prefer `%<prefer>s` over `%<global>s`.'

        ENGLISH_VARS = { # rubocop:disable Style/MutableConstant
          :$: => [:$LOAD_PATH],
          :$" => [:$LOADED_FEATURES],
          :$0 => [:$PROGRAM_NAME],
          :$! => [:$ERROR_INFO],
          :$@ => [:$ERROR_POSITION],
          :$; => %i[$FIELD_SEPARATOR $FS],
          :$, => %i[$OUTPUT_FIELD_SEPARATOR $OFS],
          :$/ => %i[$INPUT_RECORD_SEPARATOR $RS],
          :$\ => %i[$OUTPUT_RECORD_SEPARATOR $ORS],
          :$. => %i[$INPUT_LINE_NUMBER $NR],
          :$_ => [:$LAST_READ_LINE],
          :$> => [:$DEFAULT_OUTPUT],
          :$< => [:$DEFAULT_INPUT],
          :$$ => %i[$PROCESS_ID $PID],
          :$? => [:$CHILD_STATUS],
          :$~ => [:$LAST_MATCH_INFO],
          :$= => [:$IGNORECASE],
          :$* => %i[$ARGV ARGV],
          :$& => [:$MATCH],
          :$` => [:$PREMATCH],
          :$' => [:$POSTMATCH],
          :$+ => [:$LAST_PAREN_MATCH]
        }

        PERL_VARS =
          Hash[ENGLISH_VARS.flat_map { |k, vs| vs.map { |v| [v, [k]] } }]

        ENGLISH_VARS.merge!(
          Hash[ENGLISH_VARS.flat_map { |_, vs| vs.map { |v| [v, [v]] } }]
        )
        PERL_VARS.merge!(
          Hash[PERL_VARS.flat_map { |_, vs| vs.map { |v| [v, [v]] } }]
        )
        ENGLISH_VARS.each_value(&:freeze).freeze
        PERL_VARS.each_value(&:freeze).freeze

        # Anything *not* in this set is provided by the English library.
        NON_ENGLISH_VARS = Set.new(%i[
                                     $LOAD_PATH
                                     $LOADED_FEATURES
                                     $PROGRAM_NAME
                                     ARGV
                                   ]).freeze

        def on_gvar(node)
          global_var, = *node

          return unless (preferred = preferred_names(global_var))

          if preferred.include?(global_var)
            correct_style_detected
          else
            opposite_style_detected
            add_offense(node)
          end
        end

        def message(node)
          global_var, = *node

          if style == :use_english_names
            format_english_message(global_var)
          else
            format(MSG_REGULAR,
                   prefer: preferred_names(global_var).first,
                   global: global_var)
          end
        end

        def autocorrect(node)
          lambda do |corrector|
            global_var, = *node

            while node.parent&.begin_type? &&
                  node.parent.children.one?
              node = node.parent
            end

            corrector.replace(node.source_range, replacement(node, global_var))
          end
        end

        private

        def format_english_message(global_var)
          regular, english = ENGLISH_VARS[global_var].partition do |var|
            NON_ENGLISH_VARS.include? var
          end

          format_message(english, regular, global_var)
        end

        def format_message(english, regular, global)
          if regular.empty?
            format(MSG_ENGLISH, prefer: format_list(english), global: global)
          elsif english.empty?
            format(MSG_REGULAR, prefer: format_list(regular), global: global)
          else
            format(MSG_BOTH,
                   prefer: format_list(english),
                   regular: format_list(regular),
                   global: global)
          end
        end

        # For now, we assume that lists are 2 items or less. Easy grammar!
        def format_list(items)
          items.join('` or `')
        end

        def replacement(node, global_var)
          parent_type = node.parent&.type
          preferred_name = preferred_names(global_var).first

          unless %i[dstr xstr regexp].include?(parent_type)
            return preferred_name.to_s
          end

          if style == :use_english_names
            return english_name_replacement(preferred_name, node)
          end

          "##{preferred_name}"
        end

        def preferred_names(global)
          if style == :use_english_names
            ENGLISH_VARS[global]
          else
            PERL_VARS[global]
          end
        end

        def english_name_replacement(preferred_name, node)
          return "\#{#{preferred_name}}" if node.begin_type?

          "{#{preferred_name}}"
        end
      end
    end
  end
end