rubocop 1.17.0 → 1.18.4

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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +70 -29
  4. data/lib/rubocop.rb +2 -0
  5. data/lib/rubocop/cli/command/suggest_extensions.rb +3 -3
  6. data/lib/rubocop/config_loader.rb +1 -1
  7. data/lib/rubocop/config_loader_resolver.rb +1 -1
  8. data/lib/rubocop/config_validator.rb +23 -10
  9. data/lib/rubocop/cop/base.rb +2 -2
  10. data/lib/rubocop/cop/bundler/duplicated_gem.rb +1 -1
  11. data/lib/rubocop/cop/bundler/gem_version.rb +38 -4
  12. data/lib/rubocop/cop/corrector.rb +4 -4
  13. data/lib/rubocop/cop/generator.rb +1 -1
  14. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +1 -1
  15. data/lib/rubocop/cop/layout/argument_alignment.rb +1 -1
  16. data/lib/rubocop/cop/layout/array_alignment.rb +2 -2
  17. data/lib/rubocop/cop/layout/block_alignment.rb +1 -1
  18. data/lib/rubocop/cop/layout/class_structure.rb +5 -1
  19. data/lib/rubocop/cop/layout/closing_parenthesis_indentation.rb +7 -1
  20. data/lib/rubocop/cop/layout/comment_indentation.rb +1 -1
  21. data/lib/rubocop/cop/layout/end_alignment.rb +8 -1
  22. data/lib/rubocop/cop/layout/first_argument_indentation.rb +1 -1
  23. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +2 -2
  24. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +2 -2
  25. data/lib/rubocop/cop/layout/first_parameter_indentation.rb +1 -1
  26. data/lib/rubocop/cop/layout/hash_alignment.rb +25 -24
  27. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +1 -1
  28. data/lib/rubocop/cop/layout/indentation_style.rb +2 -2
  29. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +122 -0
  30. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +6 -6
  31. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +2 -2
  32. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +6 -6
  33. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +6 -6
  34. data/lib/rubocop/cop/layout/multiline_method_definition_brace_layout.rb +6 -6
  35. data/lib/rubocop/cop/layout/multiline_operation_indentation.rb +3 -3
  36. data/lib/rubocop/cop/layout/parameter_alignment.rb +2 -2
  37. data/lib/rubocop/cop/layout/space_around_operators.rb +5 -1
  38. data/lib/rubocop/cop/lint/duplicate_branch.rb +2 -1
  39. data/lib/rubocop/cop/lint/nested_percent_literal.rb +1 -1
  40. data/lib/rubocop/cop/lint/percent_string_array.rb +1 -1
  41. data/lib/rubocop/cop/lint/percent_symbol_array.rb +1 -1
  42. data/lib/rubocop/cop/lint/symbol_conversion.rb +1 -1
  43. data/lib/rubocop/cop/lint/unused_block_argument.rb +1 -1
  44. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -1
  45. data/lib/rubocop/cop/lint/useless_times.rb +1 -1
  46. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  47. data/lib/rubocop/cop/mixin/check_line_breakable.rb +12 -3
  48. data/lib/rubocop/cop/mixin/hash_transform_method.rb +6 -1
  49. data/lib/rubocop/cop/naming/inclusive_language.rb +249 -0
  50. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +2 -2
  51. data/lib/rubocop/cop/style/block_delimiters.rb +15 -0
  52. data/lib/rubocop/cop/style/class_and_module_children.rb +14 -0
  53. data/lib/rubocop/cop/style/comment_annotation.rb +50 -6
  54. data/lib/rubocop/cop/style/double_cop_disable_directive.rb +1 -7
  55. data/lib/rubocop/cop/style/eval_with_location.rb +1 -1
  56. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +8 -2
  57. data/lib/rubocop/cop/style/hash_syntax.rb +1 -1
  58. data/lib/rubocop/cop/style/multiple_comparison.rb +1 -1
  59. data/lib/rubocop/cop/style/mutable_constant.rb +6 -8
  60. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +1 -1
  61. data/lib/rubocop/cop/style/quoted_symbols.rb +2 -2
  62. data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +1 -1
  63. data/lib/rubocop/cop/style/regexp_literal.rb +3 -2
  64. data/lib/rubocop/cop/style/single_line_methods.rb +25 -15
  65. data/lib/rubocop/cop/style/special_global_vars.rb +3 -3
  66. data/lib/rubocop/cop/style/string_concatenation.rb +32 -5
  67. data/lib/rubocop/cop/style/string_literals.rb +2 -2
  68. data/lib/rubocop/cop/style/swap_values.rb +1 -1
  69. data/lib/rubocop/cop/style/unpack_first.rb +1 -1
  70. data/lib/rubocop/cop/variable_force/variable_table.rb +1 -1
  71. data/lib/rubocop/formatter/git_hub_actions_formatter.rb +1 -1
  72. data/lib/rubocop/options.rb +4 -4
  73. data/lib/rubocop/rspec/cop_helper.rb +1 -1
  74. data/lib/rubocop/rspec/expect_offense.rb +1 -1
  75. data/lib/rubocop/version.rb +1 -1
  76. metadata +11 -9
@@ -93,18 +93,18 @@ module RuboCop
93
93
  extend AutoCorrector
94
94
 
95
95
  SAME_LINE_MESSAGE = 'The closing array brace must be on the same ' \
96
- 'line as the last array element when the opening brace is on the ' \
97
- 'same line as the first array element.'
96
+ 'line as the last array element when the opening brace is on the ' \
97
+ 'same line as the first array element.'
98
98
 
99
99
  NEW_LINE_MESSAGE = 'The closing array brace must be on the line ' \
100
- 'after the last array element when the opening brace is on a ' \
101
- 'separate line from the first array element.'
100
+ 'after the last array element when the opening brace is on a ' \
101
+ 'separate line from the first array element.'
102
102
 
103
103
  ALWAYS_NEW_LINE_MESSAGE = 'The closing array brace must be on the ' \
104
- 'line after the last array element.'
104
+ 'line after the last array element.'
105
105
 
106
106
  ALWAYS_SAME_LINE_MESSAGE = 'The closing array brace must be on the ' \
107
- 'same line as the last array element.'
107
+ 'same line as the last array element.'
108
108
 
109
109
  def on_array(node)
110
110
  check_brace_layout(node)
@@ -64,10 +64,10 @@ module RuboCop
64
64
  extend AutoCorrector
65
65
 
66
66
  NEW_LINE_OFFENSE = 'Right hand side of multi-line assignment is on ' \
67
- 'the same line as the assignment operator `=`.'
67
+ 'the same line as the assignment operator `=`.'
68
68
 
69
69
  SAME_LINE_OFFENSE = 'Right hand side of multi-line assignment is not ' \
70
- 'on the same line as the assignment operator `=`.'
70
+ 'on the same line as the assignment operator `=`.'
71
71
 
72
72
  def check_assignment(node, rhs)
73
73
  return if node.send_type? && node.loc.operator&.source != '='
@@ -93,18 +93,18 @@ module RuboCop
93
93
  extend AutoCorrector
94
94
 
95
95
  SAME_LINE_MESSAGE = 'Closing hash brace must be on the same line as ' \
96
- 'the last hash element when opening brace is on the same line as ' \
97
- 'the first hash element.'
96
+ 'the last hash element when opening brace is on the same line as ' \
97
+ 'the first hash element.'
98
98
 
99
99
  NEW_LINE_MESSAGE = 'Closing hash brace must be on the line after ' \
100
- 'the last hash element when opening brace is on a separate line ' \
101
- 'from the first hash element.'
100
+ 'the last hash element when opening brace is on a separate line ' \
101
+ 'from the first hash element.'
102
102
 
103
103
  ALWAYS_NEW_LINE_MESSAGE = 'Closing hash brace must be on the line ' \
104
- 'after the last hash element.'
104
+ 'after the last hash element.'
105
105
 
106
106
  ALWAYS_SAME_LINE_MESSAGE = 'Closing hash brace must be on the same ' \
107
- 'line as the last hash element.'
107
+ 'line as the last hash element.'
108
108
 
109
109
  def on_hash(node)
110
110
  check_brace_layout(node)
@@ -93,18 +93,18 @@ module RuboCop
93
93
  extend AutoCorrector
94
94
 
95
95
  SAME_LINE_MESSAGE = 'Closing method call brace must be on the ' \
96
- 'same line as the last argument when opening brace is on the same ' \
97
- 'line as the first argument.'
96
+ 'same line as the last argument when opening brace is on the same ' \
97
+ 'line as the first argument.'
98
98
 
99
99
  NEW_LINE_MESSAGE = 'Closing method call brace must be on the ' \
100
- 'line after the last argument when opening brace is on a separate ' \
101
- 'line from the first argument.'
100
+ 'line after the last argument when opening brace is on a separate ' \
101
+ 'line from the first argument.'
102
102
 
103
103
  ALWAYS_NEW_LINE_MESSAGE = 'Closing method call brace must be on ' \
104
- 'the line after the last argument.'
104
+ 'the line after the last argument.'
105
105
 
106
106
  ALWAYS_SAME_LINE_MESSAGE = 'Closing method call brace must be on ' \
107
- 'the same line as the last argument.'
107
+ 'the same line as the last argument.'
108
108
 
109
109
  def on_send(node)
110
110
  check_brace_layout(node)
@@ -105,18 +105,18 @@ module RuboCop
105
105
  extend AutoCorrector
106
106
 
107
107
  SAME_LINE_MESSAGE = 'Closing method definition brace must be on the ' \
108
- 'same line as the last parameter when opening brace is on the same ' \
109
- 'line as the first parameter.'
108
+ 'same line as the last parameter when opening brace is on the same ' \
109
+ 'line as the first parameter.'
110
110
 
111
111
  NEW_LINE_MESSAGE = 'Closing method definition brace must be on the ' \
112
- 'line after the last parameter when opening brace is on a separate ' \
113
- 'line from the first parameter.'
112
+ 'line after the last parameter when opening brace is on a separate ' \
113
+ 'line from the first parameter.'
114
114
 
115
115
  ALWAYS_NEW_LINE_MESSAGE = 'Closing method definition brace must be ' \
116
- 'on the line after the last parameter.'
116
+ 'on the line after the last parameter.'
117
117
 
118
118
  ALWAYS_SAME_LINE_MESSAGE = 'Closing method definition brace must be ' \
119
- 'on the same line as the last parameter.'
119
+ 'on the same line as the last parameter.'
120
120
 
121
121
  def on_def(node)
122
122
  check_brace_layout(node.arguments)
@@ -59,9 +59,9 @@ module RuboCop
59
59
  return unless style == :aligned && cop_config['IndentationWidth']
60
60
 
61
61
  raise ValidationError, 'The `Layout/MultilineOperationIndentation`' \
62
- ' cop only accepts an `IndentationWidth` ' \
63
- 'configuration parameter when ' \
64
- '`EnforcedStyle` is `indented`.'
62
+ ' cop only accepts an `IndentationWidth` ' \
63
+ 'configuration parameter when ' \
64
+ '`EnforcedStyle` is `indented`.'
65
65
  end
66
66
 
67
67
  private
@@ -73,10 +73,10 @@ module RuboCop
73
73
  extend AutoCorrector
74
74
 
75
75
  ALIGN_PARAMS_MSG = 'Align the parameters of a method definition if ' \
76
- 'they span more than one line.'
76
+ 'they span more than one line.'
77
77
 
78
78
  FIXED_INDENT_MSG = 'Use one level of indentation for parameters ' \
79
- 'following the first line of a multi-line method definition.'
79
+ 'following the first line of a multi-line method definition.'
80
80
 
81
81
  def on_def(node)
82
82
  return if node.arguments.size < 2
@@ -63,6 +63,10 @@ module RuboCop
63
63
  [Style::SelfAssignment]
64
64
  end
65
65
 
66
+ def on_sclass(node)
67
+ check_operator(:sclass, node.loc.operator, node.source_range)
68
+ end
69
+
66
70
  def on_pair(node)
67
71
  return unless node.hash_rocket?
68
72
 
@@ -198,7 +202,7 @@ module RuboCop
198
202
  elsif excess_leading_space?(type, operator, with_space) ||
199
203
  excess_trailing_space?(right_operand, with_space)
200
204
  "Operator `#{operator.source}` should be surrounded " \
201
- 'by a single space.'
205
+ 'by a single space.'
202
206
  end
203
207
  end
204
208
 
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Cop
5
5
  module Lint
6
6
  # This cop checks that there are no repeated bodies
7
- # within `if/unless`, `case-when` and `rescue` constructs.
7
+ # within `if/unless`, `case-when`, `case-in` and `rescue` constructs.
8
8
  #
9
9
  # With `IgnoreLiteralBranches: true`, branches are not registered
10
10
  # as offenses if they return a basic literal value (string, symbol,
@@ -97,6 +97,7 @@ module RuboCop
97
97
  end
98
98
  alias on_if on_branching_statement
99
99
  alias on_case on_branching_statement
100
+ alias on_case_match on_branching_statement
100
101
  alias on_rescue on_branching_statement
101
102
 
102
103
  private
@@ -33,7 +33,7 @@ module RuboCop
33
33
  include PercentLiteral
34
34
 
35
35
  MSG = 'Within percent literals, nested percent literals do not ' \
36
- 'function and may be unwanted in the result.'
36
+ 'function and may be unwanted in the result.'
37
37
 
38
38
  # The array of regular expressions representing percent literals that,
39
39
  # if found within a percent literal expression, will cause a
@@ -29,7 +29,7 @@ module RuboCop
29
29
  TRAILING_QUOTE = /['"]?,?$/.freeze
30
30
 
31
31
  MSG = "Within `%w`/`%W`, quotes and ',' are unnecessary and may be " \
32
- 'unwanted in the resulting strings.'
32
+ 'unwanted in the resulting strings.'
33
33
 
34
34
  def on_array(node)
35
35
  process(node, '%w', '%W')
@@ -25,7 +25,7 @@ module RuboCop
25
25
  extend AutoCorrector
26
26
 
27
27
  MSG = "Within `%i`/`%I`, ':' and ',' are unnecessary and may be " \
28
- 'unwanted in the resulting symbols.'
28
+ 'unwanted in the resulting symbols.'
29
29
 
30
30
  def on_array(node)
31
31
  process(node, '%i', '%I')
@@ -70,7 +70,7 @@ module RuboCop
70
70
 
71
71
  MSG = 'Unnecessary symbol conversion; use `%<correction>s` instead.'
72
72
  MSG_CONSISTENCY = 'Symbol hash key should be quoted for consistency; ' \
73
- 'use `%<correction>s` instead.'
73
+ 'use `%<correction>s` instead.'
74
74
  RESTRICT_ON_SEND = %i[to_sym intern].freeze
75
75
 
76
76
  def on_send(node)
@@ -143,7 +143,7 @@ module RuboCop
143
143
 
144
144
  def message_for_underscore_prefix(variable)
145
145
  "If it's necessary, use `_` or `_#{variable.name}` " \
146
- "as an argument name to indicate that it won't be used."
146
+ "as an argument name to indicate that it won't be used."
147
147
  end
148
148
 
149
149
  def define_method_call?(variable)
@@ -85,7 +85,7 @@ module RuboCop
85
85
  return unless assignment.meta_assignment_node.equal?(return_value_node)
86
86
 
87
87
  " Use `#{assignment.operator.sub(/=$/, '')}` " \
88
- "instead of `#{assignment.operator}`."
88
+ "instead of `#{assignment.operator}`."
89
89
  end
90
90
 
91
91
  def similar_name_message(variable)
@@ -81,7 +81,7 @@ module RuboCop
81
81
  return if block_reassigns_arg?(node, block_arg)
82
82
 
83
83
  source = node.body.source
84
- source.gsub!(/\b#{block_arg}\b/, '1') if block_arg
84
+ source.gsub!(/\b#{block_arg}\b/, '0') if block_arg
85
85
 
86
86
  corrector.replace(node, fix_indentation(source, node.loc.column...node.body.loc.column))
87
87
  end
@@ -50,7 +50,7 @@ module RuboCop
50
50
  ->(node) { heredoc_node?(node) }
51
51
  else
52
52
  raise ArgumentError, "Unknown foldable type: #{type.inspect}. "\
53
- "Valid foldable types are: #{FOLDABLE_TYPES.join(', ')}."
53
+ "Valid foldable types are: #{FOLDABLE_TYPES.join(', ')}."
54
54
  end
55
55
  end
56
56
  end
@@ -72,7 +72,9 @@ module RuboCop
72
72
 
73
73
  # If a `send` node is not parenthesized, don't move the first element, because it
74
74
  # can result in changed behavior or a syntax error.
75
- elements = elements.drop(1) if node.send_type? && !node.parenthesized?
75
+ if node.send_type? && !node.parenthesized? && !first_argument_is_heredoc?(node)
76
+ elements = elements.drop(1)
77
+ end
76
78
 
77
79
  i = 0
78
80
  i += 1 while within_column_limit?(elements[i], max, line)
@@ -84,13 +86,20 @@ module RuboCop
84
86
  elements[i - 1]
85
87
  end
86
88
 
89
+ # @api private
90
+ def first_argument_is_heredoc?(node)
91
+ first_argument = node.first_argument
92
+
93
+ first_argument.respond_to?(:heredoc?) && first_argument.heredoc?
94
+ end
95
+
87
96
  # @api private
88
97
  # If a send node contains a heredoc argument, splitting cannot happen
89
98
  # after the heredoc or else it will cause a syntax error.
90
99
  def shift_elements_for_heredoc_arg(node, elements, index)
91
- return index unless node.send_type?
100
+ return index unless node.send_type? || node.array_type?
92
101
 
93
- heredoc_index = elements.index { |arg| (arg.str_type? || arg.dstr_type?) && arg.heredoc? }
102
+ heredoc_index = elements.index { |arg| arg.respond_to?(:heredoc?) && arg.heredoc? }
94
103
  return index unless heredoc_index
95
104
  return nil if heredoc_index.zero?
96
105
 
@@ -175,7 +175,12 @@ module RuboCop
175
175
  end
176
176
 
177
177
  def set_new_body_expression(transforming_body_expr, corrector)
178
- corrector.replace(block_node.body, transforming_body_expr.loc.expression.source)
178
+ body_source = transforming_body_expr.loc.expression.source
179
+ if transforming_body_expr.hash_type? && !transforming_body_expr.braces?
180
+ body_source = "{ #{body_source} }"
181
+ end
182
+
183
+ corrector.replace(block_node.body, body_source)
179
184
  end
180
185
  end
181
186
  end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Naming
6
+ # This cops recommends the use of inclusive language instead of problematic terms.
7
+ # The cop can check the following locations for offenses:
8
+ # - identifiers
9
+ # - constants
10
+ # - variables
11
+ # - strings
12
+ # - symbols
13
+ # - comments
14
+ # - file paths
15
+ # Each of these locations can be individually enabled/disabled via configuration,
16
+ # for example CheckIdentifiers = true/false.
17
+ #
18
+ # Flagged terms are configurable for the cop. For each flagged term an optional
19
+ # Regex can be specified to identify offenses. Suggestions for replacing a flagged term can
20
+ # be configured and will be displayed as part of the offense message.
21
+ # An AllowedRegex can be specified for a flagged term to exempt allowed uses of the term.
22
+ #
23
+ # @example FlaggedTerms: { whitelist: { Suggestions: ['allowlist'] } }
24
+ # # Suggest replacing identifier whitelist with allowlist
25
+ #
26
+ # # bad
27
+ # whitelist_users = %w(user1 user1)
28
+ #
29
+ # # good
30
+ # allowlist_users = %w(user1 user2)
31
+ #
32
+ # @example FlaggedTerms: { master: { Suggestions: ['main', 'primary', 'leader'] } }
33
+ # # Suggest replacing master in an instance variable name with main, primary, or leader
34
+ #
35
+ # # bad
36
+ # @master_node = 'node1.example.com'
37
+ #
38
+ # # good
39
+ # @primary_node = 'node1.example.com'
40
+ #
41
+ # @example FlaggedTerms: { whitelist: { Regex: !ruby/regexp '/white[-_\s]?list' } }
42
+ # # Identify problematic terms using a Regexp
43
+ #
44
+ # # bad
45
+ # white_list = %w(user1 user2)
46
+ #
47
+ # # good
48
+ # allow_list = %w(user1 user2)
49
+ #
50
+ # @example FlaggedTerms: { master: { AllowedRegex: 'master\'?s degree' } }
51
+ # # Specify allowed uses of the flagged term as a string or regexp.
52
+ #
53
+ # # bad
54
+ # # They had a masters
55
+ #
56
+ # # good
57
+ # # They had a master's degree
58
+ #
59
+ class InclusiveLanguage < Base
60
+ include RangeHelp
61
+
62
+ EMPTY_ARRAY = [].freeze
63
+
64
+ WordLocation = Struct.new(:word, :position)
65
+
66
+ def initialize(config = nil, options = nil)
67
+ super
68
+ @flagged_term_hash = {}
69
+ @flagged_terms_regex = nil
70
+ @allowed_regex = nil
71
+ @check_token = preprocess_check_config
72
+ preprocess_flagged_terms
73
+ end
74
+
75
+ def on_new_investigation
76
+ investigate_filepath if cop_config['CheckFilepaths']
77
+ investigate_tokens
78
+ end
79
+
80
+ private
81
+
82
+ def investigate_tokens
83
+ processed_source.each_token do |token|
84
+ next unless check_token?(token.type)
85
+
86
+ word_locations = scan_for_words(token.text)
87
+ next if word_locations.empty?
88
+
89
+ add_offenses_for_token(token, word_locations)
90
+ end
91
+ end
92
+
93
+ def add_offenses_for_token(token, word_locations)
94
+ word_locations.each do |word_location|
95
+ start_position = token.pos.begin_pos + token.pos.source.index(word_location.word)
96
+ range = range_between(start_position, start_position + word_location.word.length)
97
+ add_offense(range, message: create_message(word_location.word))
98
+ end
99
+ end
100
+
101
+ def check_token?(type)
102
+ !!@check_token[type]
103
+ end
104
+
105
+ def preprocess_check_config # rubocop:disable Metrics/AbcSize
106
+ {
107
+ tIDENTIFIER: cop_config['CheckIdentifiers'],
108
+ tCONSTANT: cop_config['CheckConstants'],
109
+ tIVAR: cop_config['CheckVariables'],
110
+ tCVAR: cop_config['CheckVariables'],
111
+ tGVAR: cop_config['CheckVariables'],
112
+ tSYMBOL: cop_config['CheckSymbols'],
113
+ tSTRING: cop_config['CheckStrings'],
114
+ tSTRING_CONTENT: cop_config['CheckStrings'],
115
+ tCOMMENT: cop_config['CheckComments']
116
+ }.freeze
117
+ end
118
+
119
+ def preprocess_flagged_terms
120
+ allowed_strings = []
121
+ flagged_term_strings = []
122
+ cop_config['FlaggedTerms'].each do |term, term_definition|
123
+ next if term_definition.nil?
124
+
125
+ allowed_strings.concat(process_allowed_regex(term_definition['AllowedRegex']))
126
+ regex_string = ensure_regex_string(term_definition['Regex'] || term)
127
+ flagged_term_strings << regex_string
128
+
129
+ add_to_flagged_term_hash(regex_string, term, term_definition)
130
+ end
131
+
132
+ set_regexes(flagged_term_strings, allowed_strings)
133
+ end
134
+
135
+ def add_to_flagged_term_hash(regex_string, term, term_definition)
136
+ @flagged_term_hash[Regexp.new(regex_string, Regexp::IGNORECASE)] =
137
+ term_definition.merge('Term' => term,
138
+ 'SuggestionString' =>
139
+ preprocess_suggestions(term_definition['Suggestions']))
140
+ end
141
+
142
+ def set_regexes(flagged_term_strings, allowed_strings)
143
+ @flagged_terms_regex = array_to_ignorecase_regex(flagged_term_strings)
144
+ @allowed_regex = array_to_ignorecase_regex(allowed_strings) unless allowed_strings.empty?
145
+ end
146
+
147
+ def process_allowed_regex(allowed)
148
+ return EMPTY_ARRAY if allowed.nil?
149
+
150
+ Array(allowed).map do |allowed_term|
151
+ next if allowed_term.is_a?(String) && allowed_term.strip.empty?
152
+
153
+ ensure_regex_string(allowed_term)
154
+ end
155
+ end
156
+
157
+ def ensure_regex_string(regex)
158
+ regex.is_a?(Regexp) ? regex.source : regex
159
+ end
160
+
161
+ def array_to_ignorecase_regex(strings)
162
+ Regexp.new(strings.join('|'), Regexp::IGNORECASE)
163
+ end
164
+
165
+ def investigate_filepath
166
+ word_locations = scan_for_words(processed_source.file_path)
167
+
168
+ case word_locations.length
169
+ when 0
170
+ return
171
+ when 1
172
+ message = create_single_word_message_for_file(word_locations.first.word)
173
+ else
174
+ words = word_locations.map(&:word)
175
+ message = create_multiple_word_message_for_file(words)
176
+ end
177
+
178
+ range = source_range(processed_source.buffer, 1, 0)
179
+ add_offense(range, message: message)
180
+ end
181
+
182
+ def create_single_word_message_for_file(word)
183
+ create_message(word).sub(/\.$/, ' in file path.')
184
+ end
185
+
186
+ def create_multiple_word_message_for_file(words)
187
+ quoted_words = words.map { |word| "'#{word}'" }
188
+ "Consider replacing problematic terms #{quoted_words.join(', ')} in file path."
189
+ end
190
+
191
+ def scan_for_words(input)
192
+ mask_input(input).enum_for(:scan, @flagged_terms_regex).map do
193
+ match = Regexp.last_match
194
+ WordLocation.new(match.to_s, match.offset(0).first)
195
+ end
196
+ end
197
+
198
+ def mask_input(str)
199
+ return str if @allowed_regex.nil?
200
+
201
+ safe_str = if str.valid_encoding?
202
+ str
203
+ else
204
+ str.encode('UTF-8', invalid: :replace, undef: :replace)
205
+ end
206
+ safe_str.gsub(@allowed_regex) { |match| '*' * match.size }
207
+ end
208
+
209
+ def create_message(word)
210
+ flagged_term = find_flagged_term(word)
211
+ "Consider replacing problematic term '#{word}'#{flagged_term['SuggestionString']}."
212
+ end
213
+
214
+ def find_flagged_term(word)
215
+ _regexp, flagged_term = @flagged_term_hash.find do |key, _term|
216
+ key.match?(word)
217
+ end
218
+ flagged_term
219
+ end
220
+
221
+ def create_message_for_file(word)
222
+ create_message(word).sub(/\.$/, ' in file path.')
223
+ end
224
+
225
+ def preprocess_suggestions(suggestions)
226
+ return '' if suggestions.nil? ||
227
+ (suggestions.is_a?(String) && suggestions.strip.empty?) || suggestions.empty?
228
+
229
+ format_suggestions(suggestions)
230
+ end
231
+
232
+ def format_suggestions(suggestions)
233
+ quoted_suggestions = Array(suggestions).map { |word| "'#{word}'" }
234
+ suggestion_str = case quoted_suggestions.size
235
+ when 1
236
+ quoted_suggestions.first
237
+ when 2
238
+ quoted_suggestions.join(' or ')
239
+ else
240
+ last_quoted = quoted_suggestions.pop
241
+ quoted_suggestions << "or #{last_quoted}"
242
+ quoted_suggestions.join(', ')
243
+ end
244
+ " with #{suggestion_str}"
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end