rubocop 1.7.0 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +4 -3
  4. data/config/default.yml +137 -31
  5. data/config/obsoletion.yml +4 -0
  6. data/lib/rubocop.rb +14 -1
  7. data/lib/rubocop/cli/command/auto_genenerate_config.rb +5 -4
  8. data/lib/rubocop/comment_config.rb +6 -6
  9. data/lib/rubocop/config.rb +5 -2
  10. data/lib/rubocop/config_loader.rb +7 -14
  11. data/lib/rubocop/config_store.rb +12 -1
  12. data/lib/rubocop/cop/base.rb +2 -1
  13. data/lib/rubocop/cop/exclude_limit.rb +26 -0
  14. data/lib/rubocop/cop/gemspec/date_assignment.rb +56 -0
  15. data/lib/rubocop/cop/generator.rb +1 -3
  16. data/lib/rubocop/cop/internal_affairs.rb +5 -1
  17. data/lib/rubocop/cop/internal_affairs/empty_line_between_expect_offense_and_correction.rb +68 -0
  18. data/lib/rubocop/cop/internal_affairs/example_description.rb +89 -0
  19. data/lib/rubocop/cop/internal_affairs/redundant_described_class_as_subject.rb +61 -0
  20. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +64 -0
  21. data/lib/rubocop/cop/layout/class_structure.rb +7 -2
  22. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +38 -18
  23. data/lib/rubocop/cop/layout/first_argument_indentation.rb +16 -2
  24. data/lib/rubocop/cop/layout/line_length.rb +2 -1
  25. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +26 -0
  26. data/lib/rubocop/cop/layout/multiline_operation_indentation.rb +2 -2
  27. data/lib/rubocop/cop/layout/space_before_brackets.rb +19 -16
  28. data/lib/rubocop/cop/lint/debugger.rb +58 -14
  29. data/lib/rubocop/cop/lint/deprecated_constants.rb +80 -0
  30. data/lib/rubocop/cop/lint/deprecated_open_ssl_constant.rb +13 -4
  31. data/lib/rubocop/cop/lint/duplicate_require.rb +2 -2
  32. data/lib/rubocop/cop/lint/else_layout.rb +1 -1
  33. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +44 -0
  34. data/lib/rubocop/cop/lint/multiple_comparison.rb +4 -4
  35. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +10 -6
  36. data/lib/rubocop/cop/lint/number_conversion.rb +41 -6
  37. data/lib/rubocop/cop/lint/numbered_parameter_assignment.rb +47 -0
  38. data/lib/rubocop/cop/lint/or_assignment_to_constant.rb +39 -0
  39. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +2 -1
  40. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +50 -0
  41. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +5 -3
  42. data/lib/rubocop/cop/lint/symbol_conversion.rb +103 -0
  43. data/lib/rubocop/cop/lint/triple_quotes.rb +71 -0
  44. data/lib/rubocop/cop/message_annotator.rb +4 -1
  45. data/lib/rubocop/cop/metrics/block_nesting.rb +2 -2
  46. data/lib/rubocop/cop/metrics/parameter_lists.rb +5 -2
  47. data/lib/rubocop/cop/mixin/allowed_identifiers.rb +18 -0
  48. data/lib/rubocop/cop/mixin/check_line_breakable.rb +5 -0
  49. data/lib/rubocop/cop/mixin/code_length.rb +3 -1
  50. data/lib/rubocop/cop/mixin/comments_help.rb +1 -11
  51. data/lib/rubocop/cop/mixin/configurable_max.rb +1 -0
  52. data/lib/rubocop/cop/mixin/first_element_line_break.rb +1 -1
  53. data/lib/rubocop/cop/mixin/method_complexity.rb +3 -1
  54. data/lib/rubocop/cop/mixin/preferred_delimiters.rb +2 -2
  55. data/lib/rubocop/cop/mixin/uncommunicative_name.rb +5 -1
  56. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +38 -5
  57. data/lib/rubocop/cop/naming/variable_name.rb +2 -0
  58. data/lib/rubocop/cop/naming/variable_number.rb +2 -9
  59. data/lib/rubocop/cop/registry.rb +1 -1
  60. data/lib/rubocop/cop/severity.rb +3 -3
  61. data/lib/rubocop/cop/style/ascii_comments.rb +1 -1
  62. data/lib/rubocop/cop/style/constant_visibility.rb +27 -0
  63. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +49 -9
  64. data/lib/rubocop/cop/style/double_negation.rb +2 -2
  65. data/lib/rubocop/cop/style/empty_literal.rb +6 -2
  66. data/lib/rubocop/cop/style/endless_method.rb +102 -0
  67. data/lib/rubocop/cop/style/eval_with_location.rb +138 -49
  68. data/lib/rubocop/cop/style/explicit_block_argument.rb +11 -1
  69. data/lib/rubocop/cop/style/exponential_notation.rb +6 -7
  70. data/lib/rubocop/cop/style/float_division.rb +3 -0
  71. data/lib/rubocop/cop/style/format_string_token.rb +18 -2
  72. data/lib/rubocop/cop/style/hash_conversion.rb +81 -0
  73. data/lib/rubocop/cop/style/hash_like_case.rb +2 -1
  74. data/lib/rubocop/cop/style/if_inside_else.rb +22 -10
  75. data/lib/rubocop/cop/style/if_with_boolean_literal_branches.rb +120 -0
  76. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +4 -0
  77. data/lib/rubocop/cop/style/nested_parenthesized_calls.rb +4 -0
  78. data/lib/rubocop/cop/style/nil_comparison.rb +3 -0
  79. data/lib/rubocop/cop/style/non_nil_check.rb +23 -13
  80. data/lib/rubocop/cop/style/numeric_literals.rb +6 -9
  81. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  82. data/lib/rubocop/cop/style/raise_args.rb +3 -2
  83. data/lib/rubocop/cop/style/redundant_return.rb +1 -1
  84. data/lib/rubocop/cop/style/single_line_methods.rb +32 -2
  85. data/lib/rubocop/cop/style/sole_nested_conditional.rb +29 -5
  86. data/lib/rubocop/cop/style/special_global_vars.rb +3 -3
  87. data/lib/rubocop/cop/style/string_concatenation.rb +1 -1
  88. data/lib/rubocop/cop/style/ternary_parentheses.rb +1 -1
  89. data/lib/rubocop/cop/style/while_until_modifier.rb +2 -4
  90. data/lib/rubocop/formatter/git_hub_actions_formatter.rb +1 -0
  91. data/lib/rubocop/formatter/offense_count_formatter.rb +1 -1
  92. data/lib/rubocop/formatter/simple_text_formatter.rb +2 -1
  93. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -1
  94. data/lib/rubocop/magic_comment.rb +30 -1
  95. data/lib/rubocop/options.rb +1 -1
  96. data/lib/rubocop/rspec/expect_offense.rb +5 -2
  97. data/lib/rubocop/runner.rb +1 -0
  98. data/lib/rubocop/target_ruby.rb +47 -11
  99. data/lib/rubocop/version.rb +2 -2
  100. metadata +24 -7
@@ -71,11 +71,7 @@ module RuboCop
71
71
  add_offense(range, message: message) do |corrector|
72
72
  corrector.replace(range, preferred_name)
73
73
 
74
- node.body&.each_descendant(:lvar) do |var|
75
- next unless var.children.first == offending_name
76
-
77
- corrector.replace(var, preferred_name)
78
- end
74
+ correct_node(corrector, node.body, offending_name, preferred_name)
79
75
  end
80
76
  end
81
77
 
@@ -86,6 +82,43 @@ module RuboCop
86
82
  variable.loc.expression
87
83
  end
88
84
 
85
+ def variable_name_matches?(node, name)
86
+ if node.masgn_type?
87
+ node.each_descendant(:lvasgn).any? do |lvasgn_node|
88
+ variable_name_matches?(lvasgn_node, name)
89
+ end
90
+ else
91
+ node.children.first == name
92
+ end
93
+ end
94
+
95
+ def correct_node(corrector, node, offending_name, preferred_name)
96
+ return unless node
97
+
98
+ node.each_node(:lvar, :lvasgn, :masgn) do |child_node|
99
+ next unless variable_name_matches?(child_node, offending_name)
100
+
101
+ corrector.replace(child_node, preferred_name) if child_node.lvar_type?
102
+
103
+ if child_node.masgn_type? || child_node.lvasgn_type?
104
+ correct_reassignment(corrector, child_node, offending_name, preferred_name)
105
+ break
106
+ end
107
+ end
108
+ end
109
+
110
+ # If the exception variable is reassigned, that assignment needs to be corrected.
111
+ # Further `lvar` nodes will not be corrected though since they now refer to a
112
+ # different variable.
113
+ def correct_reassignment(corrector, node, offending_name, preferred_name)
114
+ if node.lvasgn_type?
115
+ correct_node(corrector, node.child_nodes.first, offending_name, preferred_name)
116
+ elsif node.masgn_type?
117
+ # With multiple assign, the assignments are in an array as the last child
118
+ correct_node(corrector, node.children.last, offending_name, preferred_name)
119
+ end
120
+ end
121
+
89
122
  def preferred_name(variable_name)
90
123
  preferred_name = cop_config.fetch('PreferredName', 'e')
91
124
  if variable_name.to_s.start_with?('_')
@@ -20,6 +20,7 @@ module RuboCop
20
20
  # # good
21
21
  # fooBar = 1
22
22
  class VariableName < Base
23
+ include AllowedIdentifiers
23
24
  include ConfigurableNaming
24
25
 
25
26
  MSG = 'Use %<style>s for variable names.'
@@ -27,6 +28,7 @@ module RuboCop
27
28
  def on_lvasgn(node)
28
29
  name, = *node
29
30
  return unless name
31
+ return if allowed_identifier?(name)
30
32
 
31
33
  check_name(node, name, node.loc.name)
32
34
  end
@@ -92,11 +92,12 @@ module RuboCop
92
92
  # # good
93
93
  # :some_sym_1
94
94
  #
95
- # @example AllowedIdentifier: [capture3]
95
+ # @example AllowedIdentifiers: [capture3]
96
96
  # # good
97
97
  # expect(Open3).to receive(:capture3)
98
98
  #
99
99
  class VariableNumber < Base
100
+ include AllowedIdentifiers
100
101
  include ConfigurableNumbering
101
102
 
102
103
  MSG = 'Use %<style>s for %<identifier_type>s numbers.'
@@ -139,14 +140,6 @@ module RuboCop
139
140
 
140
141
  format(MSG, style: style, identifier_type: identifier_type)
141
142
  end
142
-
143
- def allowed_identifier?(name)
144
- allowed_identifiers.include?(name.to_s.delete('@'))
145
- end
146
-
147
- def allowed_identifiers
148
- cop_config.fetch('AllowedIdentifiers', [])
149
- end
150
143
  end
151
144
  end
152
145
  end
@@ -185,7 +185,7 @@ module RuboCop
185
185
 
186
186
  def sort!
187
187
  clear_enrollment_queue
188
- @registry = Hash[@registry.sort_by { |badge, _| badge.cop_name }]
188
+ @registry = @registry.sort_by { |badge, _| badge.cop_name }.to_h
189
189
 
190
190
  self
191
191
  end
@@ -6,10 +6,10 @@ module RuboCop
6
6
  class Severity
7
7
  include Comparable
8
8
 
9
- NAMES = %i[refactor convention warning error fatal].freeze
9
+ NAMES = %i[info refactor convention warning error fatal].freeze
10
10
 
11
11
  # @api private
12
- CODE_TABLE = { R: :refactor, C: :convention,
12
+ CODE_TABLE = { I: :info, R: :refactor, C: :convention,
13
13
  W: :warning, E: :error, F: :fatal }.freeze
14
14
 
15
15
  # @api public
@@ -18,7 +18,7 @@ module RuboCop
18
18
  #
19
19
  # @return [Symbol]
20
20
  # severity.
21
- # any of `:refactor`, `:convention`, `:warning`, `:error` or `:fatal`.
21
+ # any of `:info`, `:refactor`, `:convention`, `:warning`, `:error` or `:fatal`.
22
22
  attr_reader :name
23
23
 
24
24
  def self.name_from_code(code)
@@ -7,7 +7,7 @@ module RuboCop
7
7
  module Style
8
8
  # This cop checks for non-ascii (non-English) characters
9
9
  # in comments. You could set an array of allowed non-ascii chars in
10
- # AllowedChars attribute (empty by default).
10
+ # `AllowedChars` attribute (copyright notice "©" by default).
11
11
  #
12
12
  # @example
13
13
  # # bad
@@ -26,6 +26,24 @@ module RuboCop
26
26
  # public_constant :BAZ
27
27
  # end
28
28
  #
29
+ # @example IgnoreModules: false (default)
30
+ # # bad
31
+ # class Foo
32
+ # MyClass = Struct.new()
33
+ # end
34
+ #
35
+ # # good
36
+ # class Foo
37
+ # MyClass = Struct.new()
38
+ # public_constant :MyClass
39
+ # end
40
+ #
41
+ # @example IgnoreModules: true
42
+ # # good
43
+ # class Foo
44
+ # MyClass = Struct.new()
45
+ # end
46
+ #
29
47
  class ConstantVisibility < Base
30
48
  MSG = 'Explicitly make `%<constant_name>s` public or private using ' \
31
49
  'either `#public_constant` or `#private_constant`.'
@@ -33,6 +51,7 @@ module RuboCop
33
51
  def on_casgn(node)
34
52
  return unless class_or_module_scope?(node)
35
53
  return if visibility_declaration?(node)
54
+ return if ignore_modules? && module?(node)
36
55
 
37
56
  message = message(node)
38
57
  add_offense(node, message: message)
@@ -40,6 +59,14 @@ module RuboCop
40
59
 
41
60
  private
42
61
 
62
+ def ignore_modules?
63
+ cop_config.fetch('IgnoreModules', false)
64
+ end
65
+
66
+ def module?(node)
67
+ node.children.last.class_constructor?
68
+ end
69
+
43
70
  def message(node)
44
71
  _namespace, constant_name, _value = *node
45
72
 
@@ -9,37 +9,77 @@ module RuboCop
9
9
  # This is useful if want to make sure that every RuboCop error gets fixed
10
10
  # and not quickly disabled with a comment.
11
11
  #
12
+ # Specific cops can be allowed with the `AllowedCops` configuration. Note that
13
+ # if this configuration is set, `rubocop:disable all` is still disallowed.
14
+ #
12
15
  # @example
13
16
  # # bad
14
17
  # # rubocop:disable Metrics/AbcSize
15
- # def f
18
+ # def foo
16
19
  # end
17
20
  # # rubocop:enable Metrics/AbcSize
18
21
  #
19
22
  # # good
20
- # def fixed_method_name_and_no_rubocop_comments
23
+ # def foo
24
+ # end
25
+ #
26
+ # @example AllowedCops: [Metrics/AbcSize]
27
+ # # good
28
+ # # rubocop:disable Metrics/AbcSize
29
+ # def foo
21
30
  # end
31
+ # # rubocop:enable Metrics/AbcSize
22
32
  #
23
33
  class DisableCopsWithinSourceCodeDirective < Base
24
34
  extend AutoCorrector
25
35
 
26
36
  # rubocop:enable Lint/RedundantCopDisableDirective
27
- MSG = 'Comment to disable/enable RuboCop.'
37
+ MSG = 'Rubocop disable/enable directives are not permitted.'
38
+ MSG_FOR_COPS = 'Rubocop disable/enable directives for %<cops>s are not permitted.'
28
39
 
29
40
  def on_new_investigation
30
41
  processed_source.comments.each do |comment|
31
- next unless rubocop_directive_comment?(comment)
42
+ directive_cops = directive_cops(comment)
43
+ disallowed_cops = directive_cops - allowed_cops
32
44
 
33
- add_offense(comment) do |corrector|
34
- corrector.replace(comment, '')
35
- end
45
+ next unless disallowed_cops.any?
46
+
47
+ register_offense(comment, directive_cops, disallowed_cops)
36
48
  end
37
49
  end
38
50
 
39
51
  private
40
52
 
41
- def rubocop_directive_comment?(comment)
42
- CommentConfig::COMMENT_DIRECTIVE_REGEXP.match?(comment.text)
53
+ def register_offense(comment, directive_cops, disallowed_cops)
54
+ message = if any_cops_allowed?
55
+ format(MSG_FOR_COPS, cops: "`#{disallowed_cops.join('`, `')}`")
56
+ else
57
+ MSG
58
+ end
59
+
60
+ add_offense(comment, message: message) do |corrector|
61
+ replacement = ''
62
+
63
+ if directive_cops.length != disallowed_cops.length
64
+ replacement = comment.text.sub(/#{Regexp.union(disallowed_cops)},?\s*/, '')
65
+ .sub(/,\s*$/, '')
66
+ end
67
+
68
+ corrector.replace(comment, replacement)
69
+ end
70
+ end
71
+
72
+ def directive_cops(comment)
73
+ match = CommentConfig::COMMENT_DIRECTIVE_REGEXP.match(comment.text)
74
+ match && match[2] ? match[2].split(',').map(&:strip) : []
75
+ end
76
+
77
+ def allowed_cops
78
+ Array(cop_config['AllowedCops'])
79
+ end
80
+
81
+ def any_cops_allowed?
82
+ allowed_cops.any?
43
83
  end
44
84
  end
45
85
  end
@@ -5,8 +5,8 @@ module RuboCop
5
5
  module Style
6
6
  # This cop checks for uses of double negation (`!!`) to convert something to a boolean value.
7
7
  #
8
- # When using `EnforcedStyle: allowed_in_returns`, allow double nagation in contexts
9
- # that use boolean as a return value. When using `EnforcedStyle: forbidden`, double nagation
8
+ # When using `EnforcedStyle: allowed_in_returns`, allow double negation in contexts
9
+ # that use boolean as a return value. When using `EnforcedStyle: forbidden`, double negation
10
10
  # should be forbidden always.
11
11
  #
12
12
  # @example
@@ -32,8 +32,12 @@ module RuboCop
32
32
  def_node_matcher :str_node, '(send (const {nil? cbase} :String) :new)'
33
33
  def_node_matcher :array_with_block,
34
34
  '(block (send (const {nil? cbase} :Array) :new) args _)'
35
- def_node_matcher :hash_with_block,
36
- '(block (send (const {nil? cbase} :Hash) :new) args _)'
35
+ def_node_matcher :hash_with_block, <<~PATTERN
36
+ {
37
+ (block (send (const {nil? cbase} :Hash) :new) args _)
38
+ (numblock (send (const {nil? cbase} :Hash) :new) ...)
39
+ }
40
+ PATTERN
37
41
 
38
42
  def on_send(node)
39
43
  return unless (message = offense_message(node))
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # This cop checks for endless methods.
7
+ #
8
+ # It can enforce either the use of endless methods definitions
9
+ # for single-lined method bodies, or disallow endless methods.
10
+ #
11
+ # Other method definition types are not considered by this cop.
12
+ #
13
+ # The supported styles are:
14
+ # * allow_single_line (default) - only single line endless method definitions are allowed.
15
+ # * allow_always - all endless method definitions are allowed.
16
+ # * disallow - all endless method definitions are disallowed.
17
+ #
18
+ # NOTE: Incorrect endless method definitions will always be
19
+ # corrected to a multi-line definition.
20
+ #
21
+ # @example EnforcedStyle: allow_single_line (default)
22
+ # # good
23
+ # def my_method() = x
24
+ #
25
+ # # bad, multi-line endless method
26
+ # def my_method() = x.foo
27
+ # .bar
28
+ # .baz
29
+ #
30
+ # @example EnforcedStyle: allow_always
31
+ # # good
32
+ # def my_method() = x
33
+ #
34
+ # # good
35
+ # def my_method() = x.foo
36
+ # .bar
37
+ # .baz
38
+ #
39
+ # @example EnforcedStyle: disallow
40
+ # # bad
41
+ # def my_method; x end
42
+ #
43
+ # # bad
44
+ # def my_method() = x.foo
45
+ # .bar
46
+ # .baz
47
+ #
48
+ class EndlessMethod < Base
49
+ include ConfigurableEnforcedStyle
50
+ extend TargetRubyVersion
51
+ extend AutoCorrector
52
+
53
+ minimum_target_ruby_version 3.0
54
+
55
+ CORRECTION_STYLES = %w[multiline single_line].freeze
56
+ MSG = 'Avoid endless method definitions.'
57
+ MSG_MULTI_LINE = 'Avoid endless method definitions with multiple lines.'
58
+
59
+ def on_def(node)
60
+ if style == :disallow
61
+ handle_disallow_style(node)
62
+ else
63
+ handle_allow_style(node)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def handle_allow_style(node)
70
+ return unless node.endless?
71
+ return if node.single_line? || style == :allow_always
72
+
73
+ add_offense(node, message: MSG_MULTI_LINE) do |corrector|
74
+ correct_to_multiline(corrector, node)
75
+ end
76
+ end
77
+
78
+ def handle_disallow_style(node)
79
+ return unless node.endless?
80
+
81
+ add_offense(node) do |corrector|
82
+ correct_to_multiline(corrector, node)
83
+ end
84
+ end
85
+
86
+ def correct_to_multiline(corrector, node)
87
+ replacement = <<~RUBY.strip
88
+ def #{node.method_name}#{arguments(node)}
89
+ #{node.body.source}
90
+ end
91
+ RUBY
92
+
93
+ corrector.replace(node, replacement)
94
+ end
95
+
96
+ def arguments(node, missing = '')
97
+ node.arguments.any? ? node.arguments.source : missing
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -3,9 +3,19 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Style
6
- # This cop checks `eval` method usage. `eval` can receive source location
7
- # metadata, that are filename and line number. The metadata is used by
8
- # backtraces. This cop recommends to pass the metadata to `eval` method.
6
+ # This cop ensures that eval methods (`eval`, `instance_eval`, `class_eval`
7
+ # and `module_eval`) are given filename and line number values (`__FILE__`
8
+ # and `__LINE__`). This data is used to ensure that any errors raised
9
+ # within the evaluated code will be given the correct identification
10
+ # in a backtrace.
11
+ #
12
+ # The cop also checks that the line number given relative to `__LINE__` is
13
+ # correct.
14
+ #
15
+ # This cop will autocorrect incorrect or missing filename and line number
16
+ # values. However, if `eval` is called without a binding argument, the cop
17
+ # will not attempt to automatically add a binding, or add filename and
18
+ # line values.
9
19
  #
10
20
  # @example
11
21
  # # bad
@@ -31,28 +41,32 @@ module RuboCop
31
41
  # def do_something
32
42
  # end
33
43
  # RUBY
44
+ #
45
+ # This cop works only when a string literal is given as a code string.
46
+ # No offence is reported if a string variable is given as below:
47
+ #
48
+ # @example
49
+ # # not checked
50
+ # code = <<-RUBY
51
+ # def do_something
52
+ # end
53
+ # RUBY
54
+ # eval code
55
+ #
34
56
  class EvalWithLocation < Base
35
- MSG = 'Pass `__FILE__` and `__LINE__` to `eval` method, ' \
36
- 'as they are used by backtraces.'
37
- MSG_INCORRECT_LINE = 'Use `%<expected>s` instead of `%<actual>s`, ' \
38
- 'as they are used by backtraces.'
57
+ extend AutoCorrector
58
+
59
+ MSG = 'Pass `__FILE__` and `__LINE__` to `%<method_name>s`.'
60
+ MSG_EVAL = 'Pass a binding, `__FILE__` and `__LINE__` to `eval`.'
61
+ MSG_INCORRECT_FILE = 'Incorrect file for `%<method_name>s`; ' \
62
+ 'use `%<expected>s` instead of `%<actual>s`.'
63
+ MSG_INCORRECT_LINE = 'Incorrect line number for `%<method_name>s`; ' \
64
+ 'use `%<expected>s` instead of `%<actual>s`.'
39
65
 
40
66
  RESTRICT_ON_SEND = %i[eval class_eval module_eval instance_eval].freeze
41
67
 
42
- def_node_matcher :eval_without_location?, <<~PATTERN
43
- {
44
- (send nil? :eval ${str dstr})
45
- (send nil? :eval ${str dstr} _)
46
- (send nil? :eval ${str dstr} _ #special_file_keyword?)
47
- (send nil? :eval ${str dstr} _ #special_file_keyword? _)
48
-
49
- (send _ {:class_eval :module_eval :instance_eval}
50
- ${str dstr})
51
- (send _ {:class_eval :module_eval :instance_eval}
52
- ${str dstr} #special_file_keyword?)
53
- (send _ {:class_eval :module_eval :instance_eval}
54
- ${str dstr} #special_file_keyword? _)
55
- }
68
+ def_node_matcher :valid_eval_receiver?, <<~PATTERN
69
+ { nil? (const {nil? cbase} :Kernel) }
56
70
  PATTERN
57
71
 
58
72
  def_node_matcher :line_with_offset?, <<~PATTERN
@@ -63,17 +77,38 @@ module RuboCop
63
77
  PATTERN
64
78
 
65
79
  def on_send(node)
66
- eval_without_location?(node) do |code|
67
- if with_lineno?(node)
68
- on_with_lineno(node, code)
69
- else
70
- add_offense(node)
71
- end
72
- end
80
+ # Classes should not redefine eval, but in case one does, it shouldn't
81
+ # register an offense. Only `eval` without a receiver and `Kernel.eval`
82
+ # are considered.
83
+ return if node.method?(:eval) && !valid_eval_receiver?(node.receiver)
84
+
85
+ code = node.arguments.first
86
+ return unless code && (code.str_type? || code.dstr_type?)
87
+
88
+ check_location(node, code)
73
89
  end
74
90
 
75
91
  private
76
92
 
93
+ def check_location(node, code)
94
+ file, line = file_and_line(node)
95
+
96
+ if line
97
+ check_file(node, file)
98
+ check_line(node, code)
99
+ elsif file
100
+ check_file(node, file)
101
+ add_offense_for_missing_line(node, code)
102
+ else
103
+ add_offense_for_missing_location(node, code)
104
+ end
105
+ end
106
+
107
+ def register_offense(node, &block)
108
+ msg = node.method?(:eval) ? MSG_EVAL : format(MSG, method_name: node.method_name)
109
+ add_offense(node, message: msg, &block)
110
+ end
111
+
77
112
  def special_file_keyword?(node)
78
113
  node.str_type? &&
79
114
  node.source == '__FILE__'
@@ -84,6 +119,15 @@ module RuboCop
84
119
  node.source == '__LINE__'
85
120
  end
86
121
 
122
+ def file_and_line(node)
123
+ base = node.method?(:eval) ? 2 : 1
124
+ [node.arguments[base], node.arguments[base + 1]]
125
+ end
126
+
127
+ def with_binding?(node)
128
+ node.method?(:eval) ? node.arguments.size >= 2 : true
129
+ end
130
+
87
131
  # FIXME: It's a Style/ConditionalAssignment's false positive.
88
132
  # rubocop:disable Style/ConditionalAssignment
89
133
  def with_lineno?(node)
@@ -95,20 +139,34 @@ module RuboCop
95
139
  end
96
140
  # rubocop:enable Style/ConditionalAssignment
97
141
 
98
- def message_incorrect_line(actual, sign, line_diff)
99
- expected =
100
- if line_diff.zero?
101
- '__LINE__'
102
- else
103
- "__LINE__ #{sign} #{line_diff}"
104
- end
105
- format(MSG_INCORRECT_LINE, actual: actual.source, expected: expected)
142
+ def add_offense_for_incorrect_line(method_name, line_node, sign, line_diff)
143
+ expected = expected_line(sign, line_diff)
144
+ message = format(MSG_INCORRECT_LINE,
145
+ method_name: method_name,
146
+ actual: line_node.source,
147
+ expected: expected)
148
+
149
+ add_offense(line_node.loc.expression, message: message) do |corrector|
150
+ corrector.replace(line_node, expected)
151
+ end
152
+ end
153
+
154
+ def check_file(node, file_node)
155
+ return true if special_file_keyword?(file_node)
156
+
157
+ message = format(MSG_INCORRECT_FILE,
158
+ method_name: node.method_name,
159
+ expected: '__FILE__',
160
+ actual: file_node.source)
161
+
162
+ add_offense(file_node, message: message) do |corrector|
163
+ corrector.replace(file_node, '__FILE__')
164
+ end
106
165
  end
107
166
 
108
- def on_with_lineno(node, code)
167
+ def check_line(node, code)
109
168
  line_node = node.arguments.last
110
- lineno_range = line_node.loc.expression
111
- line_diff = string_first_line(code) - lineno_range.first_line
169
+ line_diff = line_difference(line_node, code)
112
170
  if line_diff.zero?
113
171
  add_offense_for_same_line(node, line_node)
114
172
  else
@@ -116,6 +174,10 @@ module RuboCop
116
174
  end
117
175
  end
118
176
 
177
+ def line_difference(line_node, code)
178
+ string_first_line(code) - line_node.loc.expression.first_line
179
+ end
180
+
119
181
  def string_first_line(str_node)
120
182
  if str_node.heredoc?
121
183
  str_node.loc.heredoc_body.first_line
@@ -124,23 +186,50 @@ module RuboCop
124
186
  end
125
187
  end
126
188
 
127
- def add_offense_for_same_line(_node, line_node)
189
+ def add_offense_for_same_line(node, line_node)
128
190
  return if special_line_keyword?(line_node)
129
191
 
130
- add_offense(
131
- line_node.loc.expression,
132
- message: message_incorrect_line(line_node, nil, 0)
133
- )
192
+ add_offense_for_incorrect_line(node.method_name, line_node, nil, 0)
134
193
  end
135
194
 
136
- def add_offense_for_different_line(_node, line_node, line_diff)
195
+ def add_offense_for_different_line(node, line_node, line_diff)
137
196
  sign = line_diff.positive? ? :+ : :-
138
197
  return if line_with_offset?(line_node, sign, line_diff.abs)
139
198
 
140
- add_offense(
141
- line_node.loc.expression,
142
- message: message_incorrect_line(line_node, sign, line_diff.abs)
143
- )
199
+ add_offense_for_incorrect_line(node.method_name, line_node, sign, line_diff.abs)
200
+ end
201
+
202
+ def expected_line(sign, line_diff)
203
+ if line_diff.zero?
204
+ '__LINE__'
205
+ else
206
+ "__LINE__ #{sign} #{line_diff.abs}"
207
+ end
208
+ end
209
+
210
+ def add_offense_for_missing_line(node, code)
211
+ register_offense(node) do |corrector|
212
+ line_str = missing_line(node, code)
213
+ corrector.insert_after(node.loc.expression.end, ", #{line_str}")
214
+ end
215
+ end
216
+
217
+ def add_offense_for_missing_location(node, code)
218
+ if node.method?(:eval) && !with_binding?(node)
219
+ register_offense(node)
220
+ return
221
+ end
222
+
223
+ register_offense(node) do |corrector|
224
+ line_str = missing_line(node, code)
225
+ corrector.insert_after(node.loc.expression.end, ", __FILE__, #{line_str}")
226
+ end
227
+ end
228
+
229
+ def missing_line(node, code)
230
+ line_diff = line_difference(node.arguments.last, code)
231
+ sign = line_diff.positive? ? :+ : :-
232
+ expected_line(sign, line_diff)
144
233
  end
145
234
  end
146
235
  end