lib/rubocop/cop/style/special_global_vars.rb



# encoding: utf-8
# frozen_string_literal: true

module RuboCop
  module Cop
    module Style
      # This cop looks for uses of Perl-style global variables.
      class SpecialGlobalVars < Cop
        include ConfigurableEnforcedStyle

        MSG_BOTH = 'Prefer `%s` from the stdlib \'English\' module, ' \
        'or `%s` over `%s`.'.freeze
        MSG_ENGLISH = 'Prefer `%s` from the stdlib \'English\' module ' \
        'over `%s`.'.freeze
        MSG_REGULAR = 'Prefer `%s` over `%s`.'.freeze

        ENGLISH_VARS = { # rubocop:disable Style/MutableConstant
          :$: => [:$LOAD_PATH],
          :$" => [:$LOADED_FEATURES],
          :$0 => [:$PROGRAM_NAME],
          :$! => [:$ERROR_INFO],
          :$@ => [:$ERROR_POSITION],
          :$; => [:$FIELD_SEPARATOR, :$FS],
          :$, => [:$OUTPUT_FIELD_SEPARATOR, :$OFS],
          :$/ => [:$INPUT_RECORD_SEPARATOR, :$RS],
          :$\ => [:$OUTPUT_RECORD_SEPARATOR, :$ORS],
          :$. => [:$INPUT_LINE_NUMBER, :$NR],
          :$_ => [:$LAST_READ_LINE],
          :$> => [:$DEFAULT_OUTPUT],
          :$< => [:$DEFAULT_INPUT],
          :$$ => [:$PROCESS_ID, :$PID],
          :$? => [:$CHILD_STATUS],
          :$~ => [:$LAST_MATCH_INFO],
          :$= => [:$IGNORECASE],
          :$* => [:$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 { |_, v| v.freeze }.freeze
        PERL_VARS.each { |_, v| v.freeze }.freeze

        # Anything *not* in this set is provided by the English library.
        NON_ENGLISH_VARS = Set.new([
                                     :$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, :expression)
          end
        end

        def message(node)
          global_var, = *node

          if style == :use_english_names
            regular, english = ENGLISH_VARS[global_var].partition do |var|
              NON_ENGLISH_VARS.include? var
            end

            # For now, we assume that lists are 2 items or less. Easy grammar!
            regular_msg = regular.join('` or `')
            english_msg = english.join('` or `')

            if !regular.empty? && !english.empty?
              format(MSG_BOTH, english_msg, regular_msg, global_var)
            elsif !regular.empty?
              format(MSG_REGULAR, regular_msg, global_var)
            elsif !english.empty?
              format(MSG_ENGLISH, english_msg, global_var)
            else
              raise 'Bug in SpecialGlobalVars - global var w/o preferred vars!'
            end
          else
            format(MSG_REGULAR, preferred_names(global_var).first, global_var)
          end
        end

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

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

            if [:dstr, :xstr, :regexp].include?(parent_type)
              if style == :use_english_names
                corrector.replace(node.source_range,
                                  "{#{preferred_names(global_var).first}}")
              else
                corrector.replace(node.source_range,
                                  "##{preferred_names(global_var).first}")
              end
            else
              corrector.replace(node.source_range,
                                preferred_names(global_var).first.to_s)
            end
          end
        end

        private

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