rubocop 1.6.1 → 1.9.1

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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +4 -3
  4. data/config/default.yml +145 -19
  5. data/lib/rubocop.rb +16 -1
  6. data/lib/rubocop/cli/command/auto_genenerate_config.rb +5 -4
  7. data/lib/rubocop/comment_config.rb +6 -6
  8. data/lib/rubocop/config.rb +13 -7
  9. data/lib/rubocop/config_loader.rb +11 -14
  10. data/lib/rubocop/config_loader_resolver.rb +21 -4
  11. data/lib/rubocop/config_obsoletion.rb +5 -3
  12. data/lib/rubocop/config_store.rb +12 -1
  13. data/lib/rubocop/cop/base.rb +2 -1
  14. data/lib/rubocop/cop/exclude_limit.rb +26 -0
  15. data/lib/rubocop/cop/gemspec/required_ruby_version.rb +3 -2
  16. data/lib/rubocop/cop/generator.rb +1 -3
  17. data/lib/rubocop/cop/internal_affairs.rb +6 -1
  18. data/lib/rubocop/cop/internal_affairs/empty_line_between_expect_offense_and_correction.rb +68 -0
  19. data/lib/rubocop/cop/internal_affairs/example_description.rb +89 -0
  20. data/lib/rubocop/cop/internal_affairs/redundant_described_class_as_subject.rb +61 -0
  21. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +64 -0
  22. data/lib/rubocop/cop/internal_affairs/style_detected_api_use.rb +145 -0
  23. data/lib/rubocop/cop/layout/class_structure.rb +7 -2
  24. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +56 -20
  25. data/lib/rubocop/cop/layout/first_argument_indentation.rb +16 -2
  26. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +14 -0
  27. data/lib/rubocop/cop/layout/line_length.rb +2 -1
  28. data/lib/rubocop/cop/layout/multiline_operation_indentation.rb +2 -2
  29. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +3 -10
  30. data/lib/rubocop/cop/layout/space_around_equals_in_parameter_default.rb +1 -0
  31. data/lib/rubocop/cop/layout/space_before_block_braces.rb +2 -0
  32. data/lib/rubocop/cop/layout/space_before_brackets.rb +67 -0
  33. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +13 -10
  34. data/lib/rubocop/cop/layout/space_inside_hash_literal_braces.rb +2 -2
  35. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +59 -0
  36. data/lib/rubocop/cop/lint/binary_operator_with_identical_operands.rb +7 -2
  37. data/lib/rubocop/cop/lint/deprecated_constants.rb +80 -0
  38. data/lib/rubocop/cop/lint/duplicate_branch.rb +64 -2
  39. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +44 -0
  40. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +10 -6
  41. data/lib/rubocop/cop/lint/number_conversion.rb +41 -6
  42. data/lib/rubocop/cop/lint/numbered_parameter_assignment.rb +47 -0
  43. data/lib/rubocop/cop/lint/or_assignment_to_constant.rb +39 -0
  44. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +2 -1
  45. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +50 -0
  46. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +50 -17
  47. data/lib/rubocop/cop/lint/shadowed_exception.rb +1 -11
  48. data/lib/rubocop/cop/lint/symbol_conversion.rb +103 -0
  49. data/lib/rubocop/cop/lint/triple_quotes.rb +71 -0
  50. data/lib/rubocop/cop/lint/unreachable_loop.rb +17 -0
  51. data/lib/rubocop/cop/message_annotator.rb +4 -1
  52. data/lib/rubocop/cop/metrics/block_nesting.rb +2 -2
  53. data/lib/rubocop/cop/metrics/parameter_lists.rb +5 -2
  54. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  55. data/lib/rubocop/cop/mixin/allowed_identifiers.rb +18 -0
  56. data/lib/rubocop/cop/mixin/check_line_breakable.rb +5 -0
  57. data/lib/rubocop/cop/mixin/code_length.rb +3 -1
  58. data/lib/rubocop/cop/mixin/comments_help.rb +1 -11
  59. data/lib/rubocop/cop/mixin/configurable_max.rb +1 -0
  60. data/lib/rubocop/cop/mixin/first_element_line_break.rb +1 -1
  61. data/lib/rubocop/cop/mixin/method_complexity.rb +3 -1
  62. data/lib/rubocop/cop/mixin/uncommunicative_name.rb +5 -1
  63. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +59 -5
  64. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +38 -5
  65. data/lib/rubocop/cop/naming/variable_name.rb +2 -0
  66. data/lib/rubocop/cop/naming/variable_number.rb +2 -9
  67. data/lib/rubocop/cop/registry.rb +10 -0
  68. data/lib/rubocop/cop/severity.rb +3 -3
  69. data/lib/rubocop/cop/style/access_modifier_declarations.rb +3 -1
  70. data/lib/rubocop/cop/style/ascii_comments.rb +1 -1
  71. data/lib/rubocop/cop/style/collection_methods.rb +14 -1
  72. data/lib/rubocop/cop/style/commented_keyword.rb +22 -5
  73. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +49 -9
  74. data/lib/rubocop/cop/style/empty_literal.rb +6 -2
  75. data/lib/rubocop/cop/style/endless_method.rb +102 -0
  76. data/lib/rubocop/cop/style/eval_with_location.rb +63 -34
  77. data/lib/rubocop/cop/style/explicit_block_argument.rb +10 -0
  78. data/lib/rubocop/cop/style/float_division.rb +3 -0
  79. data/lib/rubocop/cop/style/for.rb +2 -0
  80. data/lib/rubocop/cop/style/format_string_token.rb +18 -2
  81. data/lib/rubocop/cop/style/hash_except.rb +95 -0
  82. data/lib/rubocop/cop/style/hash_like_case.rb +2 -1
  83. data/lib/rubocop/cop/style/if_inside_else.rb +22 -10
  84. data/lib/rubocop/cop/style/if_with_boolean_literal_branches.rb +120 -0
  85. data/lib/rubocop/cop/style/keyword_parameters_order.rb +12 -2
  86. data/lib/rubocop/cop/style/lambda_call.rb +2 -1
  87. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +7 -0
  88. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +16 -6
  89. data/lib/rubocop/cop/style/method_def_parentheses.rb +7 -0
  90. data/lib/rubocop/cop/style/multiline_method_signature.rb +26 -1
  91. data/lib/rubocop/cop/style/multiline_when_then.rb +3 -1
  92. data/lib/rubocop/cop/style/mutable_constant.rb +13 -3
  93. data/lib/rubocop/cop/style/nested_parenthesized_calls.rb +4 -0
  94. data/lib/rubocop/cop/style/nil_comparison.rb +3 -0
  95. data/lib/rubocop/cop/style/non_nil_check.rb +23 -13
  96. data/lib/rubocop/cop/style/numeric_literals.rb +6 -9
  97. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  98. data/lib/rubocop/cop/style/raise_args.rb +5 -2
  99. data/lib/rubocop/cop/style/redundant_argument.rb +7 -1
  100. data/lib/rubocop/cop/style/redundant_freeze.rb +8 -4
  101. data/lib/rubocop/cop/style/redundant_return.rb +1 -1
  102. data/lib/rubocop/cop/style/single_line_methods.rb +36 -2
  103. data/lib/rubocop/cop/style/sole_nested_conditional.rb +29 -5
  104. data/lib/rubocop/cop/style/string_concatenation.rb +1 -1
  105. data/lib/rubocop/cop/style/symbol_proc.rb +5 -4
  106. data/lib/rubocop/cop/style/ternary_parentheses.rb +1 -1
  107. data/lib/rubocop/cop/style/while_until_modifier.rb +2 -4
  108. data/lib/rubocop/cop/util.rb +3 -1
  109. data/lib/rubocop/formatter/git_hub_actions_formatter.rb +1 -0
  110. data/lib/rubocop/formatter/simple_text_formatter.rb +2 -1
  111. data/lib/rubocop/magic_comment.rb +30 -1
  112. data/lib/rubocop/options.rb +10 -10
  113. data/lib/rubocop/rspec/cop_helper.rb +0 -4
  114. data/lib/rubocop/rspec/expect_offense.rb +37 -22
  115. data/lib/rubocop/runner.rb +17 -1
  116. data/lib/rubocop/target_finder.rb +4 -2
  117. data/lib/rubocop/target_ruby.rb +47 -11
  118. data/lib/rubocop/util.rb +16 -0
  119. data/lib/rubocop/version.rb +8 -2
  120. metadata +27 -7
@@ -106,23 +106,13 @@ module RuboCop
106
106
  error && error.ancestors[1] == SystemCallError
107
107
  end
108
108
 
109
- def silence_warnings
110
- # Replaces Kernel::silence_warnings since it hides any warnings,
111
- # including the RuboCop ones
112
- old_verbose = $VERBOSE
113
- $VERBOSE = nil
114
- yield
115
- ensure
116
- $VERBOSE = old_verbose
117
- end
118
-
119
109
  def evaluate_exceptions(group)
120
110
  rescued_exceptions = group.exceptions
121
111
 
122
112
  if rescued_exceptions.any?
123
113
  rescued_exceptions.each_with_object([]) do |exception, converted|
124
114
  begin
125
- silence_warnings do
115
+ RuboCop::Util.silence_warnings do
126
116
  # Avoid printing deprecation warnings about constants
127
117
  converted << Kernel.const_get(exception.source)
128
118
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # This cop checks for uses of literal strings converted to
7
+ # a symbol where a literal symbol could be used instead.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # 'string'.to_sym
12
+ # :symbol.to_sym
13
+ # 'underscored_string'.to_sym
14
+ # :'underscored_symbol'
15
+ # 'hyphenated-string'.to_sym
16
+ #
17
+ # # good
18
+ # :string
19
+ # :symbol
20
+ # :underscored_string
21
+ # :underscored_symbol
22
+ # :'hyphenated-string'
23
+ #
24
+ class SymbolConversion < Base
25
+ extend AutoCorrector
26
+
27
+ MSG = 'Unnecessary symbol conversion; use `%<correction>s` instead.'
28
+ RESTRICT_ON_SEND = %i[to_sym intern].freeze
29
+
30
+ def on_send(node)
31
+ return unless node.receiver
32
+ return unless node.receiver.str_type? || node.receiver.sym_type?
33
+
34
+ register_offense(node, correction: node.receiver.value.to_sym.inspect)
35
+ end
36
+
37
+ def on_sym(node)
38
+ return if properly_quoted?(node.source, node.value.inspect)
39
+
40
+ # `alias` arguments are symbols but since a symbol that requires
41
+ # being quoted is not a valid method identifier, it can be ignored
42
+ return if in_alias?(node)
43
+
44
+ # The `%I[]` and `%i[]` macros are parsed as normal arrays of symbols
45
+ # so they need to be ignored.
46
+ return if in_percent_literal_array?(node)
47
+
48
+ # Symbol hash keys have a different format and need to be handled separately
49
+ return correct_hash_key(node) if hash_key?(node)
50
+
51
+ register_offense(node, correction: node.value.inspect)
52
+ end
53
+
54
+ private
55
+
56
+ def register_offense(node, correction:, message: format(MSG, correction: correction))
57
+ add_offense(node, message: message) do |corrector|
58
+ corrector.replace(node, correction)
59
+ end
60
+ end
61
+
62
+ def properly_quoted?(source, value)
63
+ return true if !source.match?(/['"]/) || value.end_with?('=')
64
+
65
+ source == value ||
66
+ # `Symbol#inspect` uses double quotes, but allow single-quoted
67
+ # symbols to work as well.
68
+ source.tr("'", '"') == value
69
+ end
70
+
71
+ def in_alias?(node)
72
+ node.parent&.alias_type?
73
+ end
74
+
75
+ def in_percent_literal_array?(node)
76
+ node.parent&.array_type? && node.parent&.percent_literal?
77
+ end
78
+
79
+ def hash_key?(node)
80
+ node.parent&.pair_type? && node == node.parent.child_nodes.first
81
+ end
82
+
83
+ def correct_hash_key(node)
84
+ # Although some operators can be converted to symbols normally
85
+ # (ie. `:==`), these are not accepted as hash keys and will
86
+ # raise a syntax error (eg. `{ ==: ... }`). Therefore, if the
87
+ # symbol does not start with an alpha-numeric or underscore, it
88
+ # will be ignored.
89
+ return unless node.value.to_s.match?(/\A[a-z0-9_]/i)
90
+
91
+ correction = node.value.inspect.gsub(/\A:/, '')
92
+ return if properly_quoted?(node.source, correction)
93
+
94
+ register_offense(
95
+ node,
96
+ correction: correction,
97
+ message: format(MSG, correction: "#{correction}:")
98
+ )
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # This cop checks for "triple quotes" (strings delimted by any odd number
7
+ # of quotes greater than 1).
8
+ #
9
+ # Ruby allows multiple strings to be implicitly concatenated by just
10
+ # being adjacent in a statement (ie. `"foo""bar" == "foobar"`). This sometimes
11
+ # gives the impression that there is something special about triple quotes, but
12
+ # in fact it is just extra unnecessary quotes and produces the same string. Each
13
+ # pair of quotes produces an additional concatenated empty string, so the result
14
+ # is still only the "actual" string within the delimiters.
15
+ #
16
+ # NOTE: Although this cop is called triple quotes, the same behavior is present
17
+ # for strings delimited by 5, 7, etc. quotation marks.
18
+ #
19
+ # @example
20
+ # # bad
21
+ # """
22
+ # A string
23
+ # """
24
+ #
25
+ # # bad
26
+ # '''
27
+ # A string
28
+ # '''
29
+ #
30
+ # # good
31
+ # "
32
+ # A string
33
+ # "
34
+ #
35
+ # # good
36
+ # <<STRING
37
+ # A string
38
+ # STRING
39
+ #
40
+ # # good (but not the same spacing as the bad case)
41
+ # 'A string'
42
+ class TripleQuotes < Base
43
+ extend AutoCorrector
44
+
45
+ MSG = 'Delimiting a string with multiple quotes has no effect, use a single quote instead.'
46
+
47
+ def on_dstr(node)
48
+ return if (empty_str_nodes = empty_str_nodes(node)).none?
49
+
50
+ opening_quotes = node.source.scan(/(?<=\A)['"]*/)[0]
51
+ return if opening_quotes.size < 3
52
+
53
+ # If the node is composed of only empty `str` nodes, keep one
54
+ empty_str_nodes.shift if empty_str_nodes.size == node.child_nodes.size
55
+
56
+ add_offense(node) do |corrector|
57
+ empty_str_nodes.each do |str|
58
+ corrector.remove(str)
59
+ end
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def empty_str_nodes(node)
66
+ node.each_child_node(:str).select { |str| str.value == '' }
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -9,6 +9,12 @@ module RuboCop
9
9
  # In rare cases where only one iteration (or at most one iteration) is intended behavior,
10
10
  # the code should be refactored to use `if` conditionals.
11
11
  #
12
+ # NOTE: Block methods that are used with `Enumerable`s are considered to be loops.
13
+ #
14
+ # `IgnoredPatterns` can be used to match against the block receiver in order to allow
15
+ # code that would otherwise be registered as an offense (eg. `times` used not in an
16
+ # `Enumerable` context).
17
+ #
12
18
  # @example
13
19
  # # bad
14
20
  # while node
@@ -70,7 +76,16 @@ module RuboCop
70
76
  # raise NotFoundError
71
77
  # end
72
78
  #
79
+ # # bad
80
+ # 2.times { raise ArgumentError }
81
+ #
82
+ # @example IgnoredPatterns: [/(exactly|at_least|at_most)\(\d+\)\.times/] (default)
83
+ #
84
+ # # good
85
+ # exactly(2).times { raise StandardError }
73
86
  class UnreachableLoop < Base
87
+ include IgnoredPattern
88
+
74
89
  MSG = 'This loop will have at most one iteration.'
75
90
 
76
91
  def on_while(node)
@@ -91,6 +106,8 @@ module RuboCop
91
106
  return false unless node.block_type?
92
107
 
93
108
  send_node = node.send_node
109
+ return false if matches_ignored_pattern?(send_node.source)
110
+
94
111
  send_node.enumerable_method? || send_node.enumerator_method? || send_node.method?(:loop)
95
112
  end
96
113
 
@@ -85,8 +85,11 @@ module RuboCop
85
85
  end
86
86
  end
87
87
 
88
+ # Returns the base style guide URL from AllCops or the specific department
89
+ #
90
+ # @return [String] style guide URL
88
91
  def style_guide_base_url
89
- department_name = cop_name.split('/').first
92
+ department_name = cop_name.split('/')[0..-2].join('/')
90
93
 
91
94
  config.for_department(department_name)['StyleGuideBaseURL'] ||
92
95
  config.for_all_cops['StyleGuideBaseURL']
@@ -12,13 +12,13 @@ module RuboCop
12
12
  #
13
13
  # The maximum level of nesting allowed is configurable.
14
14
  class BlockNesting < Base
15
- include ConfigurableMax
16
-
17
15
  NESTING_BLOCKS = %i[
18
16
  case if while while_post
19
17
  until until_post for resbody
20
18
  ].freeze
21
19
 
20
+ exclude_limit 'Max'
21
+
22
22
  def on_new_investigation
23
23
  return if processed_source.blank?
24
24
 
@@ -51,7 +51,8 @@ module RuboCop
51
51
  # end
52
52
  #
53
53
  class ParameterLists < Base
54
- include ConfigurableMax
54
+ exclude_limit 'Max'
55
+ exclude_limit 'MaxOptionalParameters'
55
56
 
56
57
  MSG = 'Avoid parameter lists longer than %<max>d parameters. ' \
57
58
  '[%<count>d/%<max>d]'
@@ -70,7 +71,9 @@ module RuboCop
70
71
  count: optargs.count
71
72
  )
72
73
 
73
- add_offense(node, message: message)
74
+ add_offense(node, message: message) do
75
+ self.max_optional_parameters = optargs.count
76
+ end
74
77
  end
75
78
  alias on_defs on_def
76
79
 
@@ -124,7 +124,7 @@ module RuboCop
124
124
  end
125
125
 
126
126
  def classlike_node?(node)
127
- CLASSLIKE_TYPES.include?(node.type)
127
+ CLASSLIKE_TYPES.include?(node&.type)
128
128
  end
129
129
 
130
130
  def foldable_node?(node)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # This module encapsulates the ability to allow certain identifiers in a cop.
6
+ module AllowedIdentifiers
7
+ SIGILS = '@$' # if a variable starts with a sigil it will be removed
8
+
9
+ def allowed_identifier?(name)
10
+ allowed_identifiers.include?(name.to_s.delete(SIGILS))
11
+ end
12
+
13
+ def allowed_identifiers
14
+ cop_config.fetch('AllowedIdentifiers', [])
15
+ end
16
+ end
17
+ end
18
+ end
@@ -69,6 +69,11 @@ module RuboCop
69
69
  # @api private
70
70
  def extract_first_element_over_column_limit(node, elements, max)
71
71
  line = node.first_line
72
+
73
+ # If the first argument is a hash pair but the method is not parenthesized,
74
+ # the argument cannot be moved to another line because it cause a syntax error.
75
+ elements.shift if node.send_type? && !node.parenthesized? && elements.first.pair_type?
76
+
72
77
  i = 0
73
78
  i += 1 while within_column_limit?(elements[i], max, line)
74
79
  return elements.first if i.zero?
@@ -4,10 +4,12 @@ module RuboCop
4
4
  module Cop
5
5
  # Common functionality for checking length of code segments.
6
6
  module CodeLength
7
- include ConfigurableMax
7
+ extend ExcludeLimit
8
8
 
9
9
  MSG = '%<label>s has too many lines. [%<length>d/%<max>d]'
10
10
 
11
+ exclude_limit 'Max'
12
+
11
13
  private
12
14
 
13
15
  def message(length, max_length)
@@ -9,7 +9,6 @@ module RuboCop
9
9
  def source_range_with_comment(node)
10
10
  begin_pos = begin_pos_with_comment(node)
11
11
  end_pos = end_position_for(node)
12
- end_pos += 1 if node.def_type?
13
12
 
14
13
  Parser::Source::Range.new(buffer, begin_pos, end_pos)
15
14
  end
@@ -22,16 +21,7 @@ module RuboCop
22
21
  end
23
22
 
24
23
  def begin_pos_with_comment(node)
25
- annotation_line = node.first_line - 1
26
- first_comment = nil
27
-
28
- processed_source.comments_before_line(annotation_line)
29
- .reverse_each do |comment|
30
- if comment.location.line == annotation_line
31
- first_comment = comment
32
- annotation_line -= 1
33
- end
34
- end
24
+ first_comment = processed_source.ast_with_comments[node].first
35
25
 
36
26
  start_line_position(first_comment || node)
37
27
  end
@@ -4,6 +4,7 @@ module RuboCop
4
4
  module Cop
5
5
  # Handles `Max` configuration parameters, especially setting them to an
6
6
  # appropriate value with --auto-gen-config.
7
+ # @deprecated Use `exclude_limit ParameterName` instead.
7
8
  module ConfigurableMax
8
9
  private
9
10
 
@@ -21,7 +21,7 @@ module RuboCop
21
21
  end
22
22
 
23
23
  def check_children_line_break(node, children, start = node)
24
- return if children.size < 2
24
+ return if children.empty?
25
25
 
26
26
  line = start.first_line
27
27
 
@@ -6,10 +6,12 @@ module RuboCop
6
6
  #
7
7
  # This module handles measurement and reporting of complexity in methods.
8
8
  module MethodComplexity
9
- include ConfigurableMax
10
9
  include IgnoredMethods
11
10
  include Metrics::Utils::RepeatedCsendDiscount
12
11
  extend NodePattern::Macros
12
+ extend ExcludeLimit
13
+
14
+ exclude_limit 'Max'
13
15
 
14
16
  # Ensure cops that include `MethodComplexity` have the config
15
17
  # `attr_accessor`s that `ignored_method?` needs.
@@ -24,7 +24,11 @@ module RuboCop
24
24
  name = full_name.gsub(/\A(_+)/, '')
25
25
  next if allowed_names.include?(name)
26
26
 
27
- range = arg_range(arg, name.size)
27
+ length = full_name.size
28
+ length += 1 if arg.restarg_type?
29
+ length += 2 if arg.kwrestarg_type?
30
+
31
+ range = arg_range(arg, length)
28
32
  issue_offenses(node, range, name)
29
33
  end
30
34
  end
@@ -4,7 +4,9 @@ module RuboCop
4
4
  module Cop
5
5
  module Naming
6
6
  # This cop checks for memoized methods whose instance variable name
7
- # does not match the method name.
7
+ # does not match the method name. Applies to both regular methods
8
+ # (defined with `def`) and dynamic methods (defined with
9
+ # `define_method` or `define_singleton_method`).
8
10
  #
9
11
  # This cop can be configured with the EnforcedStyleForLeadingUnderscores
10
12
  # directive. It can be configured to allow for memoized instance variables
@@ -48,6 +50,17 @@ module RuboCop
48
50
  # @foo ||= calculate_expensive_thing(helper_variable)
49
51
  # end
50
52
  #
53
+ # # good
54
+ # define_method(:foo) do
55
+ # @foo ||= calculate_expensive_thing
56
+ # end
57
+ #
58
+ # # good
59
+ # define_method(:foo) do
60
+ # return @foo if defined?(@foo)
61
+ # @foo = calculate_expensive_thing
62
+ # end
63
+ #
51
64
  # @example EnforcedStyleForLeadingUnderscores: required
52
65
  # # bad
53
66
  # def foo
@@ -79,6 +92,17 @@ module RuboCop
79
92
  # @_foo = calculate_expensive_thing
80
93
  # end
81
94
  #
95
+ # # good
96
+ # define_method(:foo) do
97
+ # @_foo ||= calculate_expensive_thing
98
+ # end
99
+ #
100
+ # # good
101
+ # define_method(:foo) do
102
+ # return @_foo if defined?(@_foo)
103
+ # @_foo = calculate_expensive_thing
104
+ # end
105
+ #
82
106
  # @example EnforcedStyleForLeadingUnderscores :optional
83
107
  # # bad
84
108
  # def foo
@@ -105,6 +129,16 @@ module RuboCop
105
129
  # return @_foo if defined?(@_foo)
106
130
  # @_foo = calculate_expensive_thing
107
131
  # end
132
+ #
133
+ # # good
134
+ # define_method(:foo) do
135
+ # @foo ||= calculate_expensive_thing
136
+ # end
137
+ #
138
+ # # good
139
+ # define_method(:foo) do
140
+ # @_foo ||= calculate_expensive_thing
141
+ # end
108
142
  class MemoizedInstanceVariableName < Base
109
143
  include ConfigurableEnforcedStyle
110
144
 
@@ -112,17 +146,27 @@ module RuboCop
112
146
  'method name `%<method>s`. Use `@%<suggested_var>s` instead.'
113
147
  UNDERSCORE_REQUIRED = 'Memoized variable `%<var>s` does not start ' \
114
148
  'with `_`. Use `@%<suggested_var>s` instead.'
149
+ DYNAMIC_DEFINE_METHODS = %i[define_method define_singleton_method].to_set.freeze
150
+
151
+ def_node_matcher :method_definition?, <<~PATTERN
152
+ ${
153
+ (block (send _ %DYNAMIC_DEFINE_METHODS ({sym str} $_)) ...)
154
+ (def $_ ...)
155
+ (defs _ $_ ...)
156
+ }
157
+ PATTERN
115
158
 
116
159
  # rubocop:disable Metrics/AbcSize
117
160
  def on_or_asgn(node)
118
161
  lhs, _value = *node
119
162
  return unless lhs.ivasgn_type?
120
- return unless (method_node = node.each_ancestor(:def, :defs).first)
163
+
164
+ method_node, method_name = find_definition(node)
165
+ return unless method_node
121
166
 
122
167
  body = method_node.body
123
168
  return unless body == node || body.children.last == node
124
169
 
125
- method_name = method_node.method_name
126
170
  return if matches?(method_name, lhs)
127
171
 
128
172
  msg = format(
@@ -147,11 +191,10 @@ module RuboCop
147
191
  arg = node.arguments.first
148
192
  return unless arg.ivar_type?
149
193
 
150
- method_node = node.each_ancestor(:def, :defs).first
194
+ method_node, method_name = find_definition(node)
151
195
  return unless method_node
152
196
 
153
197
  var_name = arg.children.first
154
- method_name = method_node.method_name
155
198
  defined_memoized?(method_node.body, var_name) do |defined_ivar, return_ivar, ivar_assign|
156
199
  return if matches?(method_name, ivar_assign)
157
200
 
@@ -174,6 +217,17 @@ module RuboCop
174
217
  'EnforcedStyleForLeadingUnderscores'
175
218
  end
176
219
 
220
+ def find_definition(node)
221
+ # Methods can be defined in a `def` or `defs`,
222
+ # or dynamically via a `block` node.
223
+ node.each_ancestor(:def, :defs, :block).each do |ancestor|
224
+ method_node, method_name = method_definition?(ancestor)
225
+ return [method_node, method_name] if method_node
226
+ end
227
+
228
+ nil
229
+ end
230
+
177
231
  def matches?(method_name, ivar_assign)
178
232
  return true if ivar_assign.nil? || method_name == :initialize
179
233