inclusive-code 0.1.0 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 495cf47e5d18f5dad603026d5314bdfeeaeae01a045884444e2eb6316fc1ae68
4
- data.tar.gz: 774b4de9e09abae3ec9c80373a9a6d3af1f63593df0ddd18a4922ad2deb92c46
3
+ metadata.gz: 7fc5f8f716dd932818068d5b282bc52d1f7e93ad38eec1f16e100076347c42f8
4
+ data.tar.gz: 372a8b68115654589caf6ec9a6c03e94c96d5f2cced613065c15b0454214076a
5
5
  SHA512:
6
- metadata.gz: c6cf747c66926af9c1bc9d6c7fcdc0cf52ee3ee9b3c20c17238989a5a2983c569bea8929dfcade6cf5f60333fb604a5a6ff961e8abc7d9b842c0692e4d07e281
7
- data.tar.gz: e850f2f2cfae0a2ece5efd6368c8d8e45a3f31544aff933b4fccd802bcbb4bce420854472dc83c609c01aeaa6b1c8fc0a43060ebb52fa9a1af34897fc40aec88
6
+ metadata.gz: 6781133cb6fad6ef8bb373de9d7359a0d121901f58863adcf5f0498c0550059af3b3b6787cab808ca104392ef790e22e6fe4529805362c5c0c3c5332269004d5
7
+ data.tar.gz: aa2a97adf00529dbdf3fb6f525aaaa994e7988fe0242d47aeb934cd063fc448e0c8723ee22f91da792727c914b68b6fbbea78142fe271641e3242818450d64da
data/README.md CHANGED
@@ -1 +1,120 @@
1
- ## TODO: add usage instructions for rubocop gem
1
+ ## Installation
2
+
3
+ Add this line to your application's Gemfile:
4
+
5
+ ```ruby
6
+ gem 'inclusive-code'
7
+ ```
8
+
9
+ And then execute:
10
+
11
+ $ bundle
12
+
13
+ Or install it yourself as:
14
+
15
+ $ gem install inclusive-code
16
+
17
+ ## Usage
18
+ Configure your own set of flagged terms following the structure of the `inclusive_code_flagged_terms.yml` file at the top level of this repo. We recommend storing it in `'app/constants/inclusive_code/inclusive_code_flagged_terms.yml'`.
19
+
20
+ Put this into your .rubocop.yml:
21
+
22
+ ```yaml
23
+ require:
24
+ - inclusive-code
25
+
26
+ Flexport/InclusiveCode:
27
+ Enabled: true
28
+ GlobalConfigPath: 'app/constants/inclusive_code/inclusive_code_flagged_terms.yml' # or your path
29
+ DisableAutoCorrect: false
30
+ ```
31
+
32
+ You can run the cop on your entire codebase with `rubocop --only Flexport/InclusiveCode`.
33
+
34
+ You can run the cop on a specific file with `rubocop --only Flexport/InclusiveCode file_path`.
35
+
36
+ If you want to add inline `rubocop:disable` or `rubocop:todo` comments on all offenses, set `DisableAutoCorrect: true` in your .rubocop.yml, and run `rubocop --only Flexport/InclusiveCode --auto-correct --disable-uncorrectable`.
37
+
38
+ ## Configuration
39
+
40
+ The inclusive-code gem includes an initial configuration to get your project started. You can customize this configuration to fit your needs.
41
+
42
+ ### Flagging harmful terms
43
+
44
+ Rules are added under the key `flagged_term` as follows:
45
+
46
+ ```yaml
47
+ ---
48
+ flagged_terms:
49
+ some_harmful_term: {}
50
+ ```
51
+
52
+ ### Specifying harmful terminology
53
+
54
+ You can specify harmful terminology using basic keys (`another_harmful_term:`), string keys (`" his ":`), or using [regular expressions](https://rubular.com/r/HvcomHUBZ3KFCz) like `"white[-_\\s]*list":`. Please note that when specifying a regular expression, some characters (`\`) may need to be escaped. Examples:
55
+
56
+ ```yaml
57
+ ---
58
+ flagged_terms:
59
+ "white[-_\\s]*list": {}
60
+ "black[-_\\s]*list": {}
61
+ " him ": {}
62
+ master: {}
63
+ ```
64
+
65
+ ### Suggestions and Autocorrect
66
+
67
+ In a document titled [Terminology, Power and Offensive Language](https://tools.ietf.org/id/draft-knodel-terminology-01.html), the Internet Engineering Task Force (IETF) recommends that an editor or reviewer *should* "offer alternatives for offensive terminology as an important act of correcting larger editorial issues and clarifying technical concepts."
68
+
69
+ As this gem does some of the work of an editor or reviewer, it is appropriate that it should allow for communicating better alternatives when it finds harmful technology.
70
+
71
+ Here's how you can offer alternative suggestions:
72
+
73
+ ```yaml
74
+ ---
75
+ flagged_terms:
76
+ some_harmful_term:
77
+ suggestions:
78
+ - some_thoughtful_alternative
79
+ - some_other_thoughtful_alternative
80
+ ```
81
+
82
+ When using autocorrect, the first item in the suggestions array will be used as the autocorrect term.
83
+
84
+ ### Allowing exceptions
85
+
86
+ This gem supports two ways to specify exceptions to your rules: allowing specific terms, allowing specific files.
87
+
88
+ #### Allowing specific terms
89
+
90
+ You might want to do this to allow for an [Industry Term Exception](../README.md#industry-term-exemption). Here's how to allow certain terms using the `allowed:` key:
91
+
92
+ ```yaml
93
+ ---
94
+ flagged_terms:
95
+ master:
96
+ suggestions:
97
+ - main
98
+ allowed:
99
+ - master bill
100
+ - master air waybill
101
+ - master consol
102
+ - master shipment
103
+ ```
104
+
105
+ #### Allowing specific files
106
+
107
+ You might want to do this when you wish to disallow some term, but you need to allow it in certain files. Perhaps you rely on some library which requires you to configure it using some harmful terminology. Here's how to allow occurrences of a harmful term when they occur within some file using the `allowed_files:` key:
108
+
109
+ ```yaml
110
+ ---
111
+ flagged_terms:
112
+ whitelist:
113
+ suggestions:
114
+ - allowlist
115
+ allowed_files:
116
+ - config/initializers/some_gem_config.rb
117
+ - .some_gem/*
118
+ ```
119
+
120
+ This will result in allowing offenses in any files returned by `Dir.glob("{config/initializers/some_gem_config.rb,.some_gem/*}")`
data/config/default.yml CHANGED
@@ -2,3 +2,5 @@ Flexport/InclusiveCode:
2
2
  Description: "Warns on usage of flagged terms, suggests alternatives and allows for exceptions as configured."
3
3
  Enabled: true
4
4
  VersionAdded: "0.1.0"
5
+ GlobalConfigPath: ""
6
+ DisableAutoCorrect: false
@@ -2,10 +2,10 @@
2
2
 
3
3
  require 'rubocop'
4
4
 
5
- require_relative 'inclusive_code/flexport'
6
- require_relative 'inclusive_code/flexport/version'
7
- require_relative 'inclusive_code/flexport/inject'
5
+ require_relative 'rubocop/inclusive_code'
6
+ require_relative 'rubocop/inclusive_code/version'
7
+ require_relative 'rubocop/inclusive_code/inject'
8
8
 
9
- InclusiveCode::Flexport::Inject.defaults!
9
+ RuboCop::InclusiveCode::Inject.defaults!
10
10
 
11
- require_relative 'inclusive_code/cop/inclusive_code'
11
+ require_relative 'rubocop/cop/inclusive_code'
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Flexport
8
+ # This cop encourages use of inclusive language to help programmers avoid
9
+ # using terminology that is derogatory, hurtful, or perpetuates discrimination,
10
+ # either directly or indirectly, in their code.
11
+ #
12
+ # @example
13
+ #
14
+ # # bad
15
+ #
16
+ # BLACKLIST_COUNTRIES = "blacklist_countries".freeze
17
+ #
18
+ # @example
19
+ #
20
+ # # good
21
+ #
22
+ # BLOCKLIST_COUNTRIES = "blocklist_countries".freeze
23
+ #
24
+ #
25
+ class InclusiveCode < Cop
26
+ include RangeHelp
27
+
28
+ SEVERITY = 'warning'
29
+
30
+ FLAG_ONLY_MSG = '🚫 Use of non_inclusive word: `%<non_inclusive_word>s`.'
31
+ FULL_FLAG_MSG = "#{FLAG_ONLY_MSG} Consider using these suggested alternatives: `%<suggestions>s`."
32
+
33
+ ALLOWED_TERM_MASK_CHAR = '*'.freeze
34
+
35
+ def initialize(config = nil, options = nil, source_file = nil)
36
+ super(config, options)
37
+
38
+ source_file ||= YAML.load_file(cop_config['GlobalConfigPath'])
39
+ @non_inclusive_words_alternatives_hash = source_file['flagged_terms']
40
+ @all_non_inclusive_words = @non_inclusive_words_alternatives_hash.keys
41
+ @non_inclusive_words_regex = concatenated_regex(@all_non_inclusive_words)
42
+ @allowed_terms = {}
43
+ @allowed_files = {}
44
+
45
+ @all_non_inclusive_words.each do |word|
46
+ @allowed_terms[word] = get_allowed_string(word)
47
+ @allowed_files[word] = source_file['flagged_terms'][word]['allowed_files'] || []
48
+ end
49
+ @allowed_regex = @allowed_terms.values.reject(&:blank?).join('|')
50
+
51
+ @allowed_regex = if @allowed_regex.blank?
52
+ Regexp.new(/^$/)
53
+ else
54
+ Regexp.new(@allowed_regex, Regexp::IGNORECASE)
55
+ end
56
+ end
57
+
58
+ def investigate(processed_source)
59
+ non_inclusive_words_for_current_file = @all_non_inclusive_words.reject do |non_inclusive_word|
60
+ Dir.glob("{#{@allowed_files[non_inclusive_word].join(',')}}").include?(processed_source.path)
61
+ end
62
+
63
+ processed_source.lines.each_with_index do |line, line_number|
64
+ next unless line.match(@non_inclusive_words_regex)
65
+
66
+ non_inclusive_words_for_current_file.each do |non_inclusive_word|
67
+ allowed = @allowed_terms[non_inclusive_word]
68
+ scan_regex = /(?=#{non_inclusive_word})/i
69
+ if allowed.present?
70
+ line = line.gsub(/(#{allowed})/i){ |match| ALLOWED_TERM_MASK_CHAR * match.size }
71
+ end
72
+ locations = line.enum_for(
73
+ :scan,
74
+ scan_regex
75
+ ).map { Regexp.last_match&.offset(0)&.first }
76
+
77
+ non_inclusive_words = line.scan(/#{non_inclusive_word}/i)
78
+
79
+ locations = locations.zip(non_inclusive_words).to_h
80
+ next if locations.blank?
81
+
82
+ locations.each do |location, word|
83
+ range = source_range(
84
+ processed_source.buffer,
85
+ line_number + 1,
86
+ location,
87
+ word.length
88
+ )
89
+ add_offense(
90
+ range,
91
+ location: range,
92
+ message: create_message(word),
93
+ severity: SEVERITY
94
+ )
95
+ end
96
+ end
97
+ end
98
+ # Also error for non-inclusive language in file names
99
+ path = processed_source.path
100
+ return if path.nil?
101
+
102
+ non_inclusive_words_match = path.match(concatenated_regex(non_inclusive_words_for_current_file))
103
+ return unless non_inclusive_words_match && !path.match(@allowed_regex)
104
+
105
+ range = source_range(processed_source.buffer, 1, 0)
106
+ add_offense(
107
+ range,
108
+ location: range,
109
+ message: create_message(non_inclusive_words_match[0]),
110
+ severity: SEVERITY
111
+ )
112
+ end
113
+
114
+ def autocorrect(arg_pair)
115
+ return if cop_config['DisableAutoCorrect']
116
+
117
+ word_to_correct = arg_pair.source
118
+ correction = correction_for_word(word_to_correct)
119
+ return if correction['suggestions'].blank?
120
+
121
+ corrected = correction['suggestions'][0]
122
+
123
+ # Only respects case if it is capitalized or uniform (all upper or all lower)
124
+ to_upcase = word_to_correct == word_to_correct.upcase
125
+ if to_upcase
126
+ corrected = corrected.upcase
127
+ elsif word_to_correct == word_to_correct.capitalize
128
+ corrected = corrected.capitalize
129
+ end
130
+
131
+ lambda do |corrector|
132
+ corrector.insert_before(arg_pair, corrected)
133
+ corrector.remove(arg_pair)
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ def concatenated_regex(non_inclusive_words)
140
+ Regexp.new(
141
+ non_inclusive_words.join('|'),
142
+ Regexp::IGNORECASE
143
+ )
144
+ end
145
+
146
+ def get_allowed_string(non_inclusive_word)
147
+ allowed = @non_inclusive_words_alternatives_hash[non_inclusive_word].fetch('allowed') { [] }
148
+ snake_case = allowed.map { |e| e.tr(' ', '_').underscore }
149
+ pascal_case = snake_case.map(&:camelize)
150
+ (allowed + snake_case + pascal_case).join('|')
151
+ end
152
+
153
+ def correction_for_word(word_to_correct)
154
+ _, correction = @non_inclusive_words_alternatives_hash.detect do |correction_key, _|
155
+ Regexp.new(correction_key, Regexp::IGNORECASE).match?(word_to_correct.downcase)
156
+ end
157
+
158
+ correction || {}
159
+ end
160
+
161
+ def create_message(non_inclusive_word)
162
+ correction = correction_for_word(non_inclusive_word)
163
+ suggestions = correction.fetch('suggestions') { [] }.join(', ')
164
+
165
+ format(
166
+ suggestions.present? ? FULL_FLAG_MSG : FLAG_ONLY_MSG,
167
+ non_inclusive_word: non_inclusive_word,
168
+ suggestions: suggestions
169
+ )
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'inclusive_code/flexport/version'
3
+ require 'rubocop/inclusive_code/version'
4
4
 
5
- module InclusiveCode
6
- module Flexport
5
+ module RuboCop
6
+ module InclusiveCode
7
7
  class Error < StandardError; end
8
8
  PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze
9
9
  CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
@@ -11,4 +11,4 @@ module InclusiveCode
11
11
 
12
12
  private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
13
13
  end
14
- end
14
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  # The original code is from https://github.com/rubocop-hq/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
4
4
  # See https://github.com/rubocop-hq/rubocop-rspec/blob/master/MIT-LICENSE.md
5
- module InclusiveCode
6
- module Flexport
5
+ module RuboCop
6
+ module InclusiveCode
7
7
  # Because RuboCop doesn't yet support plugins, we have to monkey patch in a
8
8
  # bit of our configuration.
9
9
  module Inject
@@ -17,4 +17,4 @@ module InclusiveCode
17
17
  end
18
18
  end
19
19
  end
20
- end
20
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module InclusiveCode
5
+ VERSION = '0.1.5'
6
+ end
7
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inclusive-code
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Flexport Engineering
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-29 00:00:00.000000000 Z
11
+ date: 2021-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -48,14 +48,15 @@ files:
48
48
  - README.md
49
49
  - config/default.yml
50
50
  - lib/inclusive-code.rb
51
- - lib/inclusive_code/cop/inclusive_code.rb
52
- - lib/inclusive_code/flexport.rb
53
- - lib/inclusive_code/flexport/inject.rb
54
- - lib/inclusive_code/flexport/version.rb
51
+ - lib/rubocop/cop/inclusive_code.rb
52
+ - lib/rubocop/inclusive_code.rb
53
+ - lib/rubocop/inclusive_code/inject.rb
54
+ - lib/rubocop/inclusive_code/version.rb
55
55
  homepage: https://github.com/flexport/rubocop-flexport
56
56
  licenses:
57
57
  - MIT
58
- metadata: {}
58
+ metadata:
59
+ source_code_uri: https://github.com/flexport/inclusive-code
59
60
  post_install_message:
60
61
  rdoc_options: []
61
62
  require_paths:
@@ -71,8 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
71
72
  - !ruby/object:Gem::Version
72
73
  version: '0'
73
74
  requirements: []
74
- rubyforge_project:
75
- rubygems_version: 2.7.7
75
+ rubygems_version: 3.1.4
76
76
  signing_key:
77
77
  specification_version: 4
78
78
  summary: Inclusive Language RuboCop.
@@ -1,139 +0,0 @@
1
- module InclusiveCode
2
- module Cop
3
- module Flexport
4
- # This cop encourages use of inclusive language to help programmers avoid
5
- # using terminology that is derogatory, hurtful, or perpetuates discrimination,
6
- # either directly or indirectly, in their code.
7
- #
8
- # @example
9
- #
10
- # # bad
11
- #
12
- # BLACKLIST_COUNTRIES = "blacklist_countries".freeze
13
- #
14
- # @example
15
- #
16
- # # good
17
- #
18
- # BLOCKLIST_COUNTRIES = "blocklist_countries".freeze
19
- #
20
- #
21
- class InclusiveCode < Cop
22
- include RangeHelp
23
-
24
- SEVERITY = "warning".freeze
25
-
26
- MSG = "🚫 Use of non_inclusive word: `%<non_inclusive_word>s`. Consider using these suggested alternatives: `%<suggestions>s`.".freeze
27
-
28
- NON_INCLUSIVE_WORDS_ALTERNATIVES_HASH = YAML.load_file(global_config_path)["flagged_terms"]
29
-
30
- ALL_NON_INCLUSIVE_WORDS = NON_INCLUSIVE_WORDS_ALTERNATIVES_HASH.keys
31
-
32
- NON_INCLUSIVE_WORDS_REGEX = Regexp.new(
33
- ALL_NON_INCLUSIVE_WORDS.join("|"),
34
- Regexp::IGNORECASE,
35
- )
36
-
37
- def self.get_allowed_string(non_inclusive_word)
38
- allowed = NON_INCLUSIVE_WORDS_ALTERNATIVES_HASH[non_inclusive_word]["allowed"]
39
- snake_case = allowed.map { |e| e.tr(" ", "_").underscore }
40
- pascal_case = snake_case.map(&:camelize)
41
- (allowed + snake_case + pascal_case).join("|")
42
- end
43
-
44
- ALLOWED = ALL_NON_INCLUSIVE_WORDS.collect do |word|
45
- [
46
- word,
47
- get_allowed_string(word),
48
- ]
49
- end.to_h
50
-
51
- ALLOWED_REGEX = Regexp.new(ALLOWED.values.reject(&:blank?).join("|"), Regexp::IGNORECASE)
52
-
53
- def investigate(processed_source)
54
- processed_source.lines.each_with_index do |line, line_number|
55
- if line.match(NON_INCLUSIVE_WORDS_REGEX)
56
- ALL_NON_INCLUSIVE_WORDS.each do |non_inclusive_word|
57
- allowed = ALLOWED[non_inclusive_word]
58
- if allowed.blank?
59
- scan_regex = /(?=#{non_inclusive_word})/i
60
- else
61
- scan_regex = /(?=#{non_inclusive_word})(?!(#{ALLOWED[non_inclusive_word]}))/i
62
- end
63
- locations = line.enum_for(
64
- :scan,
65
- scan_regex,
66
- ).map { Regexp.last_match&.offset(0)&.first }
67
- next if locations.blank?
68
- locations.each do |location|
69
- range = source_range(
70
- processed_source.buffer,
71
- line_number + 1,
72
- location,
73
- non_inclusive_word.length,
74
- )
75
- add_offense(
76
- range,
77
- location: range,
78
- message: create_message(non_inclusive_word),
79
- severity: SEVERITY,
80
- )
81
- end
82
- end
83
- end
84
- end
85
- # Also error for non-inclusive language in file names
86
- path = processed_source.path
87
- if path.nil?
88
- return
89
- end
90
- non_inclusive_words_match = path.match(NON_INCLUSIVE_WORDS_REGEX)
91
- if non_inclusive_words_match && !path.match(ALLOWED_REGEX)
92
- range = source_range(processed_source.buffer, 1, 0)
93
- add_offense(
94
- range,
95
- location: range,
96
- message: create_message(non_inclusive_words_match[0]),
97
- severity: SEVERITY,
98
- )
99
- end
100
- end
101
-
102
- def autocorrect(arg_pair)
103
- word_to_correct = arg_pair.source
104
- word_to_correct_downcase = word_to_correct.downcase
105
- return if !NON_INCLUSIVE_WORDS_ALTERNATIVES_HASH.key?(word_to_correct_downcase)
106
- return if NON_INCLUSIVE_WORDS_ALTERNATIVES_HASH[word_to_correct_downcase]["suggestions"].blank?
107
- corrected = NON_INCLUSIVE_WORDS_ALTERNATIVES_HASH[word_to_correct_downcase]["suggestions"][0]
108
-
109
- # Only respects case if it is capitalized or uniform (all upper or all lower)
110
- to_upcase = word_to_correct == word_to_correct.upcase
111
- if to_upcase
112
- corrected = corrected.upcase
113
- elsif word_to_correct == word_to_correct.capitalize
114
- corrected = corrected.capitalize
115
- end
116
-
117
- ->(corrector) do
118
- corrector.insert_before(arg_pair, corrected)
119
- corrector.remove(arg_pair)
120
- end
121
- end
122
-
123
- private
124
-
125
- def create_message(non_inclusive_word)
126
- format(
127
- MSG,
128
- non_inclusive_word: non_inclusive_word,
129
- suggestions: NON_INCLUSIVE_WORDS_ALTERNATIVES_HASH[non_inclusive_word]["suggestions"].join(", "),
130
- )
131
- end
132
-
133
- def global_config_path
134
- cop_config['GlobalConfigPath']
135
- end
136
- end
137
- end
138
- end
139
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module InclusiveCode
4
- module Flexport
5
- VERSION = '0.1.0'
6
- end
7
- end