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).
@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 78 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 87 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 105 def add_offenses_for_token(token, word_locations) word_locations.each do |word_location| start_position = token.pos.begin_pos + token.pos.source.index(word_location.word) range = range_between(start_position, start_position + word_location.word.length) add_offense(range, message: create_message(word_location.word)) end end
# File lib/rubocop/cop/naming/inclusive_language.rb, line 154 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 180 def array_to_ignorecase_regex(strings) Regexp.new(strings.join('|'), Regexp::IGNORECASE) end
# File lib/rubocop/cop/naming/inclusive_language.rb, line 113 def check_token?(type) !!@check_token[type] end
# File lib/rubocop/cop/naming/inclusive_language.rb, line 228 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 205 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 201 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 176 def ensure_regex_string(regex) regex.is_a?(Regexp) ? regex.source : regex end
# File lib/rubocop/cop/naming/inclusive_language.rb, line 147 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 236 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 250 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 184 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 94 def investigate_tokens processed_source.each_token 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 216 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 117 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 131 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 243 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 166 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 209 def scan_for_words(input) mask_input(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 161 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