class RuboCop::Cop::Naming::InclusiveLanguage
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.
@example FlaggedTerms: { whitelist: { Suggestions: [‘allowlist’] } }
# Suggest replacing identifier whitelist with allowlist # bad whitelist_users = %w(user1 user1) # good allowlist_users = %w(user1 user2)
@example 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'
@example FlaggedTerms: { whitelist: { Regex: !ruby/regexp ‘/white?list’ } }
# Identify problematic terms using a Regexp # bad white_list = %w(user1 user2) # good allow_list = %w(user1 user2)
@example 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
@example 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
Constants
- EMPTY_ARRAY
- MSG
- MSG_FOR_FILE_PATH
- WordLocation
Public Class Methods
RuboCop::Cop::Base::new
# 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
Public Instance Methods
# File lib/rubocop/cop/naming/inclusive_language.rb, line 93 def on_new_investigation investigate_filepath if cop_config['CheckFilepaths'] investigate_tokens end
Private Instance Methods
# 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'] next unless suggestions.is_a?(String) corrector.replace(range, suggestions) end end end
# File lib/rubocop/cop/naming/inclusive_language.rb, line 167 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
# File lib/rubocop/cop/naming/inclusive_language.rb, line 193 def array_to_ignorecase_regex(strings) Regexp.new(strings.join('|'), Regexp::IGNORECASE) end
# File lib/rubocop/cop/naming/inclusive_language.rb, line 126 def check_token?(type) !!@check_token[type] end
# File lib/rubocop/cop/naming/inclusive_language.rb, line 244 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
# File lib/rubocop/cop/naming/inclusive_language.rb, line 218 def create_multiple_word_message_for_file(words) format(MSG_FOR_FILE_PATH, term: words.join("', '"), suffix: ' with other terms') end
# File lib/rubocop/cop/naming/inclusive_language.rb, line 214 def create_single_word_message_for_file(word) create_message(word, MSG_FOR_FILE_PATH) end
# File lib/rubocop/cop/naming/inclusive_language.rb, line 189 def ensure_regex_string(regex) regex.is_a?(Regexp) ? regex.source : regex end
# File lib/rubocop/cop/naming/inclusive_language.rb, line 160 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
# File lib/rubocop/cop/naming/inclusive_language.rb, line 252 def find_flagged_term(word) _regexp, flagged_term = @flagged_term_hash.find do |key, _term| key.match?(word) end flagged_term end
# File lib/rubocop/cop/naming/inclusive_language.rb, line 266 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
# File lib/rubocop/cop/naming/inclusive_language.rb, line 197 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
# 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
# File lib/rubocop/cop/naming/inclusive_language.rb, line 232 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
# File lib/rubocop/cop/naming/inclusive_language.rb, line 281 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
# 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
# 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
# File lib/rubocop/cop/naming/inclusive_language.rb, line 259 def preprocess_suggestions(suggestions) return '' if suggestions.nil? || (suggestions.is_a?(String) && suggestions.strip.empty?) || suggestions.empty? format_suggestions(suggestions) end
# File lib/rubocop/cop/naming/inclusive_language.rb, line 179 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
# File lib/rubocop/cop/naming/inclusive_language.rb, line 222 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
# File lib/rubocop/cop/naming/inclusive_language.rb, line 174 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