class RuboCop::Cop::Naming::InclusiveLanguage

TeslaVehicle
# good (won’t be flagged despite containing ‘slave`)
Slave
# bad
# Specify that only terms that are full matches will be flagged.
@example FlaggedTerms: { slave: { WholeWord: true } }
# They had a master’s degree
# good
# They had a masters
# bad
# Specify allowed uses of the flagged term as a string or regexp.
@example FlaggedTerms: { master: { AllowedRegex: ‘master'?s degree’ } }
allow_list = %w(user1 user2)
# good
white_list = %w(user1 user2)
# bad
# Identify problematic terms using a Regexp
@example FlaggedTerms: { whitelist: { Regex: !ruby/regexp ‘/white?list’ } }
@primary_node = ‘node1.example.com’
# good
@master_node = ‘node1.example.com’
# bad
# Suggest replacing master in an instance variable name with main, primary, or leader
@example FlaggedTerms: { master: { Suggestions: [‘main’, ‘primary’, ‘leader’] } }
allowlist_users = %w(user1 user2)
# good
whitelist_users = %w(user1 user1)
# bad
# Suggest replacing identifier whitelist with allowlist
@example FlaggedTerms: { whitelist: { Suggestions: [‘allowlist’] } }
suggestions, the best suggestion cannot be identified and will not be autocorrected.
The cop supports autocorrection when there is only one suggestion. When there are multiple
a term matches the whole word (partial matches will not be offenses).
‘WholeWord: true` can be set on a flagged term to indicate the cop should only match when
An AllowedRegex can be specified for a flagged term to exempt allowed uses of the term.
be configured and will be displayed as part of the offense message.
Regex can be specified to identify offenses. Suggestions for replacing a flagged term can
Flagged terms are configurable for the cop. For each flagged term an optional
for example CheckIdentifiers = true/false.
Each of these locations can be individually enabled/disabled via configuration,
- file paths
- comments
- symbols
- strings
- variables
- constants
- identifiers
The cop can check the following locations for offenses:
Recommends the use of inclusive language instead of problematic terms.

def add_offenses_for_token(token, word_locations)

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']
      next unless suggestions.is_a?(String)
      corrector.replace(range, suggestions)
    end
  end
end

def add_to_flagged_term_hash(regex_string, term, term_definition)

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

def array_to_ignorecase_regex(strings)

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

def check_token?(type)

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

def create_message(word, message = MSG)

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

def create_multiple_word_message_for_file(words)

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

def create_single_word_message_for_file(word)

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

def ensure_regex_string(regex)

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

def extract_regexp(term, term_definition)

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

def find_flagged_term(word)

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

def format_suggestions(suggestions)

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

def initialize(config = nil, options = nil)

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

def investigate_filepath

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
  range = source_range(processed_source.buffer, 1, 0)
  add_offense(range, message: message)
end

def investigate_tokens

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

def mask_input(str)

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

def offense_range(token, word)

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

def on_new_investigation

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

def preprocess_check_config # rubocop:disable Metrics/AbcSize

rubocop:disable Metrics/AbcSize
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

def preprocess_flagged_terms

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

def preprocess_suggestions(suggestions)

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

def process_allowed_regex(allowed)

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

def scan_for_words(input)

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

def set_regexes(flagged_term_strings, allowed_strings)

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