123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Cop::Naming::InclusiveLanguage

Relationships & Source Files
Namespace Children
Classes:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, ::RuboCop::Cop::AutoCorrector, ::RuboCop::Cop::Base, ::RuboCop::ExcludeLimit, NodePattern::Macros, RuboCop::AST::Sexp
Instance Chain:
Inherits: RuboCop::Cop::Base
Defined in: lib/rubocop/cop/naming/inclusive_language.rb

Overview

Recommends the use of inclusive language instead of problematic terms. The cop can check the following locations for offenses:

  • identifiers

  • constants

  • variables

  • strings

  • symbols

  • comments

  • file paths

Each of these locations can be individually enabled/disabled via configuration, for example CheckIdentifiers = true/false.

Flagged terms are configurable for the cop. For each flagged term an optional Regex can be specified to identify offenses. Suggestions for replacing a flagged term can be configured and will be displayed as part of the offense message. An AllowedRegex can be specified for a flagged term to exempt allowed uses of the term. WholeWord: true can be set on a flagged term to indicate the cop should only match when a term matches the whole word (partial matches will not be offenses).

The cop supports autocorrection when there is only one suggestion. When there are multiple suggestions, the best suggestion cannot be identified and will not be autocorrected.

Examples:

FlaggedTerms: { whitelist: { Suggestions: ['allowlist'] } }

# Suggest replacing identifier whitelist with allowlist

# bad
whitelist_users = %w(user1 user1)

# good
allowlist_users = %w(user1 user2)

FlaggedTerms: { master: { Suggestions: ['main', 'primary', 'leader'] } }

# Suggest replacing master in an instance variable name with main, primary, or leader

# bad
@master_node = 'node1.example.com'

# good
@primary_node = 'node1.example.com'

FlaggedTerms: { whitelist: { Regex: !ruby/regexp '/white[-_\s]?list' } }

# Identify problematic terms using a Regexp

# bad
white_list = %w(user1 user2)

# good
allow_list = %w(user1 user2)

FlaggedTerms: { master: { AllowedRegex: 'master\'?s degree' } }

# Specify allowed uses of the flagged term as a string or regexp.

# bad
# They had a masters

# good
# They had a master's degree

FlaggedTerms: { slave: { WholeWord: true } }

# Specify that only terms that are full matches will be flagged.

# bad
Slave

# good (won't be flagged despite containing {slave})
TeslaVehicle

Constant Summary

::RuboCop::Cop::Base - Inherited

EMPTY_OFFENSES, RESTRICT_ON_SEND

::RuboCop::Cop::RangeHelp - Included

BYTE_ORDER_MARK, NOT_GIVEN

Class Attribute Summary

::RuboCop::Cop::AutoCorrector - Extended

::RuboCop::Cop::Base - Inherited

.gem_requirements, .lint?,
.support_autocorrect?

Returns if class supports autocorrect.

.support_multiple_source?

Override if your cop should be called repeatedly for multiple investigations Between calls to #on_new_investigation and on_investigation_end, the result of processed_source will remain constant.

Class Method Summary

::RuboCop::Cop::Base - Inherited

.autocorrect_incompatible_with

List of cops that should not try to autocorrect at the same time as this cop.

.badge

Naming.

.callbacks_needed, .cop_name, .department,
.documentation_url

Returns a url to view this cops documentation online.

.exclude_from_registry

Call for abstract Cop classes.

.inherited,
.joining_forces

Override and return the Force class(es) you need to join.

.match?

Returns true if the cop name or the cop namespace matches any of the given names.

.new,
.requires_gem

Register a version requirement for the given gem name.

.restrict_on_send

::RuboCop::ExcludeLimit - Extended

exclude_limit

Sets up a configuration option to have an exclude limit tracked.

transform

Instance Attribute Summary

Instance Method Summary

::RuboCop::Cop::RangeHelp - Included

#add_range, #column_offset_between,
#contents_range

A range containing only the contents of a literal with delimiters (e.g.

#directions,
#effective_column

Returns the column attribute of the range, except if the range is on the first line and there’s a byte order mark at the beginning of that line, in which case 1 is subtracted from the column value.

#final_pos, #move_pos, #move_pos_str, #range_between, #range_by_whole_lines, #range_with_comments, #range_with_comments_and_lines, #range_with_surrounding_comma, #range_with_surrounding_space, #source_range

::RuboCop::Cop::Base - Inherited

#add_global_offense

Adds an offense that has no particular location.

#add_offense

Adds an offense on the specified range (or node with an expression) Unless that offense is disabled for this range, a corrector will be yielded to provide the cop the opportunity to autocorrect the offense.

#begin_investigation

Called before any investigation.

#callbacks_needed,
#cop_config

Configuration Helpers.

#cop_name, #excluded_file?,
#external_dependency_checksum

This method should be overridden when a cop’s behavior depends on state that lives outside of these locations:

#inspect,
#message

Gets called if no message is specified when calling add_offense or add_global_offense Cops are discouraged to override this; instead pass your message directly.

#name

Alias for Base#cop_name.

#offenses,
#on_investigation_end

Called after all on_…​

#on_new_investigation

Called before all on_…​

#on_other_file

Called instead of all on_…​

#parse

There should be very limited reasons for a Cop to do it’s own parsing.

#parser_engine,
#ready

Called between investigations.

#relevant_file?,
#target_gem_version

Returns a gems locked versions (i.e.

#target_rails_version, #target_ruby_version, #annotate, #apply_correction, #attempt_correction,
#callback_argument

Reserved for Cop::Cop.

#complete_investigation

Called to complete an investigation.

#correct, #current_corrector,
#current_offense_locations

Reserved for Commissioner:

#current_offenses, #currently_disabled_lines, #custom_severity, #default_severity, #disable_uncorrectable, #enabled_line?, #file_name_matches_any?, #find_message, #find_severity, #range_for_original, #range_from_node_or_range,
#reset_investigation

Actually private methods.

#use_corrector

::RuboCop::Cop::AutocorrectLogic - Included

::RuboCop::Cop::IgnoredNode - Included

Constructor Details

.new(config = nil, options = nil) ⇒ InclusiveLanguage

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 84

def initialize(config = nil, options = nil)
  super
  @flagged_term_hash = {}
  @flagged_terms_regex = nil
  @allowed_regex = nil
  @check_token = preprocess_check_config
  preprocess_flagged_terms
end

Instance Method Details

#add_offenses_for_token(token, word_locations) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 111

def add_offenses_for_token(token, word_locations)
  word_locations.each do |word_location|
    word = word_location.word
    range = offense_range(token, word)

    add_offense(range, message: create_message(word)) do |corrector|
      suggestions = find_flagged_term(word)['Suggestions']

      if (preferred_term = preferred_sole_term(suggestions))
        corrector.replace(range, preferred_term)
      end
    end
  end
end

#add_to_flagged_term_hash(regex_string, term, term_definition) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 176

def add_to_flagged_term_hash(regex_string, term, term_definition)
  @flagged_term_hash[Regexp.new(regex_string, Regexp::IGNORECASE)] =
    term_definition.merge('Term' => term,
                          'SuggestionString' =>
                            preprocess_suggestions(term_definition['Suggestions']))
end

#array_to_ignorecase_regex(strings) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 202

def array_to_ignorecase_regex(strings)
  Regexp.new(strings.join('|'), Regexp::IGNORECASE)
end

#check_token?(type) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 126

def check_token?(type)
  !!@check_token[type]
end

#create_message(word, message = MSG) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 252

def create_message(word, message = MSG)
  flagged_term = find_flagged_term(word)
  suggestions = flagged_term['SuggestionString']
  suggestions = ' with another term' if suggestions.blank?

  format(message, term: word, suffix: suggestions)
end

#create_multiple_word_message_for_file(words) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 226

def create_multiple_word_message_for_file(words)
  format(MSG_FOR_FILE_PATH, term: words.join("', '"), suffix: ' with other terms')
end

#create_single_word_message_for_file(word) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 222

def create_single_word_message_for_file(word)
  create_message(word, MSG_FOR_FILE_PATH)
end

#ensure_regex_string(regex) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 198

def ensure_regex_string(regex)
  regex.is_a?(Regexp) ? regex.source : regex
end

#extract_regexp(term, term_definition) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 169

def extract_regexp(term, term_definition)
  return term_definition['Regex'] if term_definition['Regex']
  return /(?:\b|(?<=[\W_]))#{term}(?:\b|(?=[\W_]))/ if term_definition['WholeWord']

  term
end

#find_flagged_term(word) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 260

def find_flagged_term(word)
  _regexp, flagged_term = @flagged_term_hash.find do |key, _term|
    key.match?(word)
  end
  flagged_term
end

#format_suggestions(suggestions) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 274

def format_suggestions(suggestions)
  quoted_suggestions = Array(suggestions).map { |word| "'#{word}'" }
  suggestion_str = case quoted_suggestions.size
                   when 1
                     quoted_suggestions.first
                   when 2
                     quoted_suggestions.join(' or ')
                   else
                     last_quoted = quoted_suggestions.pop
                     quoted_suggestions << "or #{last_quoted}"
                     quoted_suggestions.join(', ')
                   end
  " with #{suggestion_str}"
end

#investigate_filepath (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 206

def investigate_filepath
  word_locations = scan_for_words(processed_source.file_path)

  case word_locations.length
  when 0
    return
  when 1
    message = create_single_word_message_for_file(word_locations.first.word)
  else
    words = word_locations.map(&:word)
    message = create_multiple_word_message_for_file(words)
  end

  add_global_offense(message)
end

#investigate_tokens (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 100

def investigate_tokens
  processed_source.tokens.each do |token|
    next unless check_token?(token.type)

    word_locations = scan_for_words(token.text)
    next if word_locations.empty?

    add_offenses_for_token(token, word_locations)
  end
end

#mask_input(str) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 240

def mask_input(str)
  safe_str = if str.valid_encoding?
               str
             else
               str.encode('UTF-8', invalid: :replace, undef: :replace)
             end

  return safe_str if @allowed_regex.nil?

  safe_str.gsub(@allowed_regex) { |match| '*' * match.size }
end

#offense_range(token, word) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 289

def offense_range(token, word)
  start_position = token.pos.begin_pos + token.pos.source.index(word)

  range_between(start_position, start_position + word.length)
end

#on_new_investigation

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 93

def on_new_investigation
  investigate_filepath if cop_config['CheckFilepaths']
  investigate_tokens
end

#preferred_sole_term(suggestions) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 160

def preferred_sole_term(suggestions)
  case suggestions
  when Array
    suggestions.one? && preferred_sole_term(suggestions.first)
  when String
    suggestions
  end
end

#preprocess_check_config (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 130

def preprocess_check_config # rubocop:disable Metrics/AbcSize
  {
    tIDENTIFIER: cop_config['CheckIdentifiers'],
    tCONSTANT: cop_config['CheckConstants'],
    tIVAR: cop_config['CheckVariables'],
    tCVAR: cop_config['CheckVariables'],
    tGVAR: cop_config['CheckVariables'],
    tSYMBOL: cop_config['CheckSymbols'],
    tSTRING: cop_config['CheckStrings'],
    tSTRING_CONTENT: cop_config['CheckStrings'],
    tCOMMENT: cop_config['CheckComments']
  }.freeze
end

#preprocess_flagged_terms (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 144

def preprocess_flagged_terms
  allowed_strings = []
  flagged_term_strings = []
  cop_config['FlaggedTerms'].each do |term, term_definition|
    next if term_definition.nil?

    allowed_strings.concat(process_allowed_regex(term_definition['AllowedRegex']))
    regex_string = ensure_regex_string(extract_regexp(term, term_definition))
    flagged_term_strings << regex_string

    add_to_flagged_term_hash(regex_string, term, term_definition)
  end

  set_regexes(flagged_term_strings, allowed_strings)
end

#preprocess_suggestions(suggestions) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 267

def preprocess_suggestions(suggestions)
  return '' if suggestions.nil? ||
               (suggestions.is_a?(String) && suggestions.strip.empty?) || suggestions.empty?

  format_suggestions(suggestions)
end

#process_allowed_regex(allowed) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 188

def process_allowed_regex(allowed)
  return EMPTY_ARRAY if allowed.nil?

  Array(allowed).map do |allowed_term|
    next if allowed_term.is_a?(String) && allowed_term.strip.empty?

    ensure_regex_string(allowed_term)
  end
end

#scan_for_words(input) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 230

def scan_for_words(input)
  masked_input = mask_input(input)
  return EMPTY_ARRAY unless masked_input.match?(@flagged_terms_regex)

  masked_input.enum_for(:scan, @flagged_terms_regex).map do
    match = Regexp.last_match
    WordLocation.new(match.to_s, match.offset(0).first)
  end
end

#set_regexes(flagged_term_strings, allowed_strings) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/inclusive_language.rb', line 183

def set_regexes(flagged_term_strings, allowed_strings)
  @flagged_terms_regex = array_to_ignorecase_regex(flagged_term_strings)
  @allowed_regex = array_to_ignorecase_regex(allowed_strings) unless allowed_strings.empty?
end