lib/rubocop/cop/inclusivity/race.rb in rubocop-inclusivity-1.0.0 vs lib/rubocop/cop/inclusivity/race.rb in rubocop-inclusivity-1.1.0
- old
+ new
@@ -1,7 +1,11 @@
# frozen_string_literal: true
+require "active_support"
+
+# https://github.com/rubocop-hq/rubocop-ast/blob/5cd306f40e5d5ba4dacf78c698354747cdac7825/docs/modules/ROOT/pages/node_types.adoc
+
module RuboCop
module Cop
module Inclusivity
# Detects potentially insensitive langugae used in variable names and
# suggests alternatives that promote inclusivity.
@@ -14,44 +18,155 @@
# blacklist = 1
#
# # good
# banlist = 1
#
- class Race < Cop
+ class Race < Base
+ extend AutoCorrector
+ include ActiveSupport::Inflector
+
+ ALLOWLIST = "Allowlist"
+ OFFENSES = "Offenses"
+ DOUBLE_QUOTE = "\""
MSG = "`%s` may be insensitive. Consider alternatives: %s"
+ PARTIAL = "partial"
+ SINGLE_QUOTE = "'"
+ SYMBOL = ":"
+ UTF_8 = "UTF-8"
- def on_lvasgn(node)
+ def simple_substitution(node)
name, = *node
- return unless name
+ name && check(node.loc.name, name)
+ end
+ alias on_lvasgn simple_substitution
+ alias on_ivasgn simple_substitution
+ alias on_cvasgn simple_substitution
+ alias on_arg simple_substitution
+ alias on_optarg simple_substitution
+ alias on_restarg simple_substitution
+ alias on_kwoptarg simple_substitution
+ alias on_kwarg simple_substitution
+ alias on_kwrestarg simple_substitution
+ alias on_blockarg simple_substitution
+ alias on_lvar simple_substitution
+ alias on_cvar simple_substitution
- check_name(node, name, node.loc.name)
+ def on_casgn(node)
+ _parent, constant_name, _value = *node
+ check(node.loc.name, constant_name) { |alternative| alternative.upcase }
end
- alias on_ivasgn on_lvasgn
- alias on_cvasgn on_lvasgn
- alias on_arg on_lvasgn
- alias on_optarg on_lvasgn
- alias on_restarg on_lvasgn
- alias on_kwoptarg on_lvasgn
- alias on_kwarg on_lvasgn
- alias on_kwrestarg on_lvasgn
- alias on_blockarg on_lvasgn
- alias on_lvar on_lvasgn
+ def on_sym(node)
+ name, = *node
+ check(node.source_range, name) { |replacement| node.source[0] == SYMBOL ? ":#{replacement}" : replacement }
+ end
+
+ def on_str(node)
+ name, = *node
+ check(node.source_range, name) do |replacement|
+ quote = determine_quote(node.source)
+ "#{quote}#{replacement.downcase}#{quote}"
+ end
+ end
+
+ def on_def(node)
+ name, _args, _forward_args = *node
+ check(node.loc.name, name)
+ end
+
+ def on_send(node)
+ match_methods_and_variables(node) do |variable_name|
+ check(node.loc.selector, variable_name.to_s.delete_suffix("="))
+ end
+ end
+
+ def on_const(node)
+ match_consts(node) do |const_name|
+ check(node.loc.name, const_name)
+ end
+ end
+
private
- def check_name(node, name, name_range)
- if (alternatives = preferred_language(name))
- msg = message(name, alternatives)
- add_offense(node, location: name_range, message: msg)
+ def_node_matcher :match_methods_and_variables, <<~PATTERN
+ (send _ $_ ...)
+ PATTERN
+
+ def_node_matcher :match_consts, <<~PATTERN
+ (const ... $_)
+ PATTERN
+
+ def determine_quote(source)
+ return SINGLE_QUOTE if source[0] == SINGLE_QUOTE
+ DOUBLE_QUOTE if source[0] == DOUBLE_QUOTE
+ end
+
+ def check(range, input)
+ string = input.to_s.encode(Encoding.find(UTF_8), invalid: :replace, undef: :replace, replace: "")
+ alternatives = preferred_language(string)
+ return unless alternatives
+
+ add_offense(range, message: format(MSG, string, alternatives.join(", "))) do |corrector|
+ replacement = block_given? ? yield(alternatives.first) : alternatives.first
+ corrector.replace(range, replacement)
end
end
def preferred_language(word)
- cop_config["Offenses"][word.to_s.downcase]
+ exclusive_language_matcher.match(word) do |match|
+ next if allow?(word)
+
+ offense = match[0].downcase
+ alternatives = cop_config["Offenses"].fetch(offense)
+ matcher = %r{#{offense}}i
+
+ alternatives.map do |alternative|
+ word.to_s.gsub(matcher) { |match| replace_match(match, alternative) }
+ end
+ end
end
- def message(insensitive, alternatives)
- format(MSG, insensitive, alternatives.join(", "))
+ def on_new_investigation
+ processed_source.comments.each do |comment|
+ start_position = comment.location.expression.to_range.first
+ comment.text.split(" ").each do |word|
+ alternatives = preferred_language(word)
+ next unless alternatives
+
+ buffer = @processed_source.buffer
+ end_position = start_position + word.to_s.size
+ range = Parser::Source::Range.new(buffer, start_position, end_position)
+ add_offense(range, message: format(MSG, word.to_s, alternatives.join(", "))) do |corrector|
+ corrector.replace(range, alternatives.first)
+ end
+ ensure
+ start_position += word.size + 1
+ end
+ end
+ end
+
+ def replace_match(word, alternative)
+ normalized_alternative = alternative.downcase
+ return normalized_alternative if word.downcase == word
+ return normalized_alternative.upcase if word.upcase == word
+ return underscore(normalized_alternative) if underscore(word) == word
+ return camelize(normalized_alternative) if camelize(word) == word
+ return camelize(normalized_alternative, false) if camelize(word, false) == word
+ word
+ end
+
+ def allow?(text)
+ allowlist.any? do |(allow, config)|
+ config.fetch(PARTIAL, false) ? text.to_s.match(%r{#{allow}}i) : text.to_s.downcase == allow.to_s.downcase
+ end
+ end
+
+ def allowlist
+ @_allowlist ||= cop_config[ALLOWLIST] || {}
+ end
+
+ def exclusive_language_matcher
+ @_exclusive_language_matcher ||= %r{(#{cop_config[OFFENSES].keys.join("|")})}i
end
end
end
end
end