Class: RuboCop::Cop::Flexport::InclusiveCode

Inherits:
RuboCop::Cop show all
Includes:
RangeHelp
Defined in:
lib/rubocop/cop/inclusive_code.rb

Overview

This cop encourages use of inclusive language to help programmers avoid using terminology that is derogatory, hurtful, or perpetuates discrimination, either directly or indirectly, in their code.

Examples:


# bad

BLACKLIST_COUNTRIES = "blacklist_countries".freeze

# good

BLOCKLIST_COUNTRIES = "blocklist_countries".freeze

Constant Summary collapse

SEVERITY =
'warning'
FLAG_ONLY_MSG =
'🚫 Use of non_inclusive word: `%<non_inclusive_word>s`.'
FULL_FLAG_MSG =
"#{FLAG_ONLY_MSG} Consider using these suggested alternatives: `%<suggestions>s`."
ALLOWED_TERM_MASK_CHAR =
'*'.freeze

Instance Method Summary collapse

Constructor Details

#initialize(config = nil, options = nil, source_file = nil) ⇒ InclusiveCode

Returns a new instance of InclusiveCode.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/rubocop/cop/inclusive_code.rb', line 35

def initialize(config = nil, options = nil, source_file = nil)
  super(config, options)

  source_file ||= YAML.load_file(cop_config['GlobalConfigPath'])
  @non_inclusive_words_alternatives_hash = source_file['flagged_terms']
  @all_non_inclusive_words = @non_inclusive_words_alternatives_hash.keys
  @non_inclusive_words_regex = concatenated_regex(@all_non_inclusive_words)
  @allowed_terms = {}
  @allowed_files = {}

  @all_non_inclusive_words.each do |word|
    @allowed_terms[word] = get_allowed_string(word)
    @allowed_files[word] = source_file['flagged_terms'][word]['allowed_files'] || []
  end
  @allowed_regex = @allowed_terms.values.reject(&:blank?).join('|')

  @allowed_regex = if @allowed_regex.blank?
                     Regexp.new(/^$/)
                   else
                     Regexp.new(@allowed_regex, Regexp::IGNORECASE)
                   end
end

Instance Method Details

#autocorrect(arg_pair) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/rubocop/cop/inclusive_code.rb', line 114

def autocorrect(arg_pair)
  return if cop_config['DisableAutoCorrect']

  word_to_correct = arg_pair.source
  correction = correction_for_word(word_to_correct)
  return if correction['suggestions'].blank?

  corrected = correction['suggestions'][0]

  # Only respects case if it is capitalized or uniform (all upper or all lower)
  to_upcase = word_to_correct == word_to_correct.upcase
  if to_upcase
    corrected = corrected.upcase
  elsif word_to_correct == word_to_correct.capitalize
    corrected = corrected.capitalize
  end

  lambda do |corrector|
    corrector.insert_before(arg_pair, corrected)
    corrector.remove(arg_pair)
  end
end

#investigate(processed_source) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/rubocop/cop/inclusive_code.rb', line 58

def investigate(processed_source)
  non_inclusive_words_for_current_file = @all_non_inclusive_words.reject do |non_inclusive_word|
    Dir.glob("{#{@allowed_files[non_inclusive_word].join(',')}}").include?(processed_source.path)
  end

  processed_source.lines.each_with_index do |line, line_number|
    next unless line.match(@non_inclusive_words_regex)

    non_inclusive_words_for_current_file.each do |non_inclusive_word|
      allowed = @allowed_terms[non_inclusive_word]
      scan_regex = /(?=#{non_inclusive_word})/i
      if allowed.present?
        line = line.gsub(/(#{allowed})/i){ |match| ALLOWED_TERM_MASK_CHAR * match.size }
      end
      locations = line.enum_for(
        :scan,
        scan_regex
      ).map { Regexp.last_match&.offset(0)&.first }

      non_inclusive_words = line.scan(/#{non_inclusive_word}/i)

      locations = locations.zip(non_inclusive_words).to_h
      next if locations.blank?

      locations.each do |location, word|
        range = source_range(
          processed_source.buffer,
          line_number + 1,
          location,
          word.length
        )
        add_offense(
          range,
          location: range,
          message: create_message(word),
          severity: SEVERITY
        )
      end
    end
  end
  # Also error for non-inclusive language in file names
  path = processed_source.path
  return if path.nil?

  non_inclusive_words_match = path.match(concatenated_regex(non_inclusive_words_for_current_file))
  return unless non_inclusive_words_match && !path.match(@allowed_regex)

  range = source_range(processed_source.buffer, 1, 0)
  add_offense(
    range,
    location: range,
    message: create_message(non_inclusive_words_match[0]),
    severity: SEVERITY
  )
end