rubocop 1.23.0 → 1.24.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/config/default.yml +46 -1
  4. data/lib/rubocop/cli/command/auto_genenerate_config.rb +1 -1
  5. data/lib/rubocop/cli/command/init_dotfile.rb +1 -1
  6. data/lib/rubocop/cli/command/show_docs_url.rb +48 -0
  7. data/lib/rubocop/cli/command/suggest_extensions.rb +1 -1
  8. data/lib/rubocop/cli.rb +1 -0
  9. data/lib/rubocop/config_loader_resolver.rb +1 -1
  10. data/lib/rubocop/cop/bundler/duplicated_gem.rb +1 -1
  11. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  12. data/lib/rubocop/cop/correctors/if_then_corrector.rb +55 -0
  13. data/lib/rubocop/cop/documentation.rb +19 -2
  14. data/lib/rubocop/cop/gemspec/require_mfa.rb +8 -10
  15. data/lib/rubocop/cop/internal_affairs/redundant_method_dispatch_node.rb +47 -0
  16. data/lib/rubocop/cop/internal_affairs/undefined_config.rb +3 -1
  17. data/lib/rubocop/cop/internal_affairs.rb +1 -0
  18. data/lib/rubocop/cop/layout/comment_indentation.rb +31 -2
  19. data/lib/rubocop/cop/layout/dot_position.rb +4 -0
  20. data/lib/rubocop/cop/layout/hash_alignment.rb +1 -1
  21. data/lib/rubocop/cop/layout/space_after_colon.rb +1 -1
  22. data/lib/rubocop/cop/layout/space_before_first_arg.rb +4 -0
  23. data/lib/rubocop/cop/lint/constant_definition_in_block.rb +1 -1
  24. data/lib/rubocop/cop/lint/deprecated_class_methods.rb +16 -4
  25. data/lib/rubocop/cop/lint/deprecated_open_ssl_constant.rb +6 -0
  26. data/lib/rubocop/cop/lint/each_with_object_argument.rb +1 -1
  27. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +4 -0
  28. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +7 -4
  29. data/lib/rubocop/cop/metrics/block_length.rb +1 -0
  30. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +0 -9
  31. data/lib/rubocop/cop/metrics/method_length.rb +1 -0
  32. data/lib/rubocop/cop/metrics/module_length.rb +1 -1
  33. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  34. data/lib/rubocop/cop/metrics/utils/repeated_attribute_discount.rb +1 -1
  35. data/lib/rubocop/cop/mixin/enforce_superclass.rb +5 -0
  36. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +4 -3
  37. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +56 -0
  38. data/lib/rubocop/cop/naming/block_forwarding.rb +107 -0
  39. data/lib/rubocop/cop/security/open.rb +11 -1
  40. data/lib/rubocop/cop/style/character_literal.rb +8 -1
  41. data/lib/rubocop/cop/style/collection_compact.rb +31 -13
  42. data/lib/rubocop/cop/style/combinable_loops.rb +2 -2
  43. data/lib/rubocop/cop/style/empty_case_condition.rb +10 -0
  44. data/lib/rubocop/cop/style/file_read.rb +112 -0
  45. data/lib/rubocop/cop/style/file_write.rb +124 -0
  46. data/lib/rubocop/cop/style/hash_conversion.rb +2 -1
  47. data/lib/rubocop/cop/style/hash_syntax.rb +22 -0
  48. data/lib/rubocop/cop/style/hash_transform_keys.rb +6 -6
  49. data/lib/rubocop/cop/style/hash_transform_values.rb +6 -6
  50. data/lib/rubocop/cop/style/if_inside_else.rb +15 -0
  51. data/lib/rubocop/cop/style/map_to_hash.rb +68 -0
  52. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +13 -0
  53. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +3 -1
  54. data/lib/rubocop/cop/style/method_def_parentheses.rb +17 -13
  55. data/lib/rubocop/cop/style/numeric_literals.rb +10 -1
  56. data/lib/rubocop/cop/style/one_line_conditional.rb +18 -39
  57. data/lib/rubocop/cop/style/redundant_interpolation.rb +17 -3
  58. data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +5 -1
  59. data/lib/rubocop/cop/style/redundant_self.rb +1 -1
  60. data/lib/rubocop/cop/style/safe_navigation.rb +1 -5
  61. data/lib/rubocop/cop/style/single_line_block_params.rb +2 -2
  62. data/lib/rubocop/cop/style/sole_nested_conditional.rb +3 -1
  63. data/lib/rubocop/cop/team.rb +1 -1
  64. data/lib/rubocop/cop/util.rb +9 -1
  65. data/lib/rubocop/options.rb +6 -1
  66. data/lib/rubocop/remote_config.rb +1 -3
  67. data/lib/rubocop/result_cache.rb +1 -1
  68. data/lib/rubocop/version.rb +1 -1
  69. data/lib/rubocop.rb +7 -0
  70. metadata +13 -5
@@ -41,7 +41,11 @@ module RuboCop
41
41
  end
42
42
 
43
43
  node.operator_method? || node.setter_method? || chained_calls?(node) ||
44
- operator_keyword?(node) || node.first_argument.hash_type?
44
+ valid_first_argument?(node.first_argument)
45
+ end
46
+
47
+ def valid_first_argument?(first_arg)
48
+ first_arg.operator_keyword? || first_arg.hash_type? || ternary_expression?(first_arg)
45
49
  end
46
50
 
47
51
  def first_argument_starts_with_left_parenthesis?(node)
@@ -53,9 +57,8 @@ module RuboCop
53
57
  first_argument.send_type? && (node.children.last&.children&.count || 0) > 1
54
58
  end
55
59
 
56
- def operator_keyword?(node)
57
- first_argument = node.first_argument
58
- first_argument.operator_keyword?
60
+ def ternary_expression?(node)
61
+ node.if_type? && node.ternary?
59
62
  end
60
63
 
61
64
  def spaces_before_left_parenthesis(node)
@@ -50,6 +50,7 @@ module RuboCop
50
50
 
51
51
  check_code_length(node)
52
52
  end
53
+ alias on_numblock on_block
53
54
 
54
55
  private
55
56
 
@@ -46,15 +46,6 @@ module RuboCop
46
46
  1
47
47
  end
48
48
 
49
- def block_method(node)
50
- case node.type
51
- when :block
52
- node.method_name
53
- when :block_pass
54
- node.parent.method_name
55
- end
56
- end
57
-
58
49
  def count_block?(block)
59
50
  KNOWN_ITERATING_METHODS.include? block.method_name
60
51
  end
@@ -52,6 +52,7 @@ module RuboCop
52
52
 
53
53
  check_code_length(node)
54
54
  end
55
+ alias on_numblock on_block
55
56
 
56
57
  private
57
58
 
@@ -44,7 +44,7 @@ module RuboCop
44
44
 
45
45
  # @!method module_definition?(node)
46
46
  def_node_matcher :module_definition?, <<~PATTERN
47
- (casgn nil? _ (block (send (const {nil? cbase} :Module) :new) ...))
47
+ (casgn nil? _ ({block numblock} (send (const {nil? cbase} :Module) :new) ...))
48
48
  PATTERN
49
49
 
50
50
  def message(length, max_length)
@@ -135,7 +135,7 @@ module RuboCop
135
135
 
136
136
  def extract_body(node)
137
137
  case node.type
138
- when :class, :module, :block, :def, :defs
138
+ when :class, :module, :block, :numblock, :def, :defs
139
139
  node.body
140
140
  when :casgn
141
141
  _scope, _name, value = *node
@@ -59,7 +59,7 @@ module RuboCop
59
59
 
60
60
  # @!method attribute_call?(node)
61
61
  def_node_matcher :attribute_call?, <<~PATTERN
62
- ( {csend send} _receiver _method # and no parameters
62
+ (call _receiver _method # and no parameters
63
63
  )
64
64
  PATTERN
65
65
 
@@ -13,6 +13,11 @@ module RuboCop
13
13
  # @api private
14
14
  module EnforceSuperclass
15
15
  def self.included(base)
16
+ warn Rainbow(
17
+ '`RuboCop::Cop::EnforceSuperclass` is deprecated and will be removed in RuboCop 2.0. ' \
18
+ 'Please upgrade to RuboCop Rails 2.9 or newer to continue.'
19
+ ).yellow
20
+
16
21
  # @!method class_definition(node)
17
22
  base.def_node_matcher :class_definition, <<~PATTERN
18
23
  (class (const _ !:#{base::SUPERCLASS}) #{base::BASE_PATTERN} ...)
@@ -43,7 +43,7 @@ module RuboCop
43
43
  end
44
44
 
45
45
  def value_delta(pair)
46
- return 0 if pair.value_on_new_line?
46
+ return 0 if pair.value_on_new_line? || pair.value_omission?
47
47
 
48
48
  correct_value_column = pair.loc.operator.end.column + 1
49
49
  actual_value_column = pair.value.loc.column
@@ -111,7 +111,8 @@ module RuboCop
111
111
  correct_value_column = first_pair.key.loc.column +
112
112
  current_pair.delimiter(true).length +
113
113
  max_key_width
114
- correct_value_column - current_pair.value.loc.column
114
+
115
+ current_pair.value_omission? ? 0 : correct_value_column - current_pair.value.loc.column
115
116
  end
116
117
  end
117
118
 
@@ -134,7 +135,7 @@ module RuboCop
134
135
  end
135
136
 
136
137
  def value_delta(first_pair, current_pair)
137
- first_pair.value_delta(current_pair)
138
+ current_pair.value_omission? ? 0 : first_pair.value_delta(current_pair)
138
139
  end
139
140
  end
140
141
 
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # This module checks for Ruby 3.1's hash value omission syntax.
6
+ module HashShorthandSyntax
7
+ OMIT_HASH_VALUE_MSG = 'Omit the hash value.'
8
+ EXPLICIT_HASH_VALUE_MSG = 'Explicit the hash value.'
9
+
10
+ def on_pair(node)
11
+ return if target_ruby_version <= 3.0
12
+
13
+ hash_key_source = node.key.source
14
+
15
+ if enforced_shorthand_syntax == 'always'
16
+ return if node.value_omission? || require_hash_value?(hash_key_source, node)
17
+
18
+ message = OMIT_HASH_VALUE_MSG
19
+ replacement = "#{hash_key_source}:"
20
+ else
21
+ return unless node.value_omission?
22
+
23
+ message = EXPLICIT_HASH_VALUE_MSG
24
+ replacement = "#{hash_key_source}: #{hash_key_source}"
25
+ end
26
+
27
+ add_offense(node.value, message: message) do |corrector|
28
+ corrector.replace(node, replacement)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def enforced_shorthand_syntax
35
+ cop_config.fetch('EnforcedShorthandSyntax', 'always')
36
+ end
37
+
38
+ def require_hash_value?(hash_key_source, node)
39
+ return true if without_parentheses_call_expr_follows?(node)
40
+
41
+ hash_value = node.value
42
+ return true unless hash_value.send_type? || hash_value.lvar_type?
43
+
44
+ hash_key_source != hash_value.source || hash_key_source.end_with?('!', '?')
45
+ end
46
+
47
+ def without_parentheses_call_expr_follows?(node)
48
+ return false unless (ancestor = node.parent.parent)
49
+ return false unless (right_sibling = ancestor.right_sibling)
50
+
51
+ ancestor.respond_to?(:parenthesized?) && !ancestor.parenthesized? &&
52
+ right_sibling.respond_to?(:parenthesized?) && !right_sibling.parenthesized?
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Naming
6
+ # In Ruby 3.1, anonymous block forwarding has been added.
7
+ #
8
+ # This cop identifies places where `do_something(&block)` can be replaced
9
+ # by `do_something(&)`.
10
+ #
11
+ # It also supports the opposite style by alternative `explicit` option.
12
+ #
13
+ # @example EnforcedStyle: anonymous (default)
14
+ #
15
+ # # bad
16
+ # def foo(&block)
17
+ # bar(&block)
18
+ # end
19
+ #
20
+ # # good
21
+ # def foo(&)
22
+ # bar(&)
23
+ # end
24
+ #
25
+ # @example EnforcedStyle: explicit
26
+ #
27
+ # # bad
28
+ # def foo(&)
29
+ # bar(&)
30
+ # end
31
+ #
32
+ # # good
33
+ # def foo(&block)
34
+ # bar(&block)
35
+ # end
36
+ #
37
+ class BlockForwarding < Base
38
+ include ConfigurableEnforcedStyle
39
+ include RangeHelp
40
+ extend AutoCorrector
41
+ extend TargetRubyVersion
42
+
43
+ minimum_target_ruby_version 3.1
44
+
45
+ MSG = 'Use %<style>s block forwarding.'
46
+
47
+ def on_def(node)
48
+ return if node.arguments.empty?
49
+
50
+ last_argument = node.arguments.last
51
+ return if expected_block_forwarding_style?(node, last_argument)
52
+
53
+ register_offense(last_argument)
54
+
55
+ node.each_descendant(:block_pass) do |block_pass_node|
56
+ next if block_pass_node.children.first&.sym_type?
57
+
58
+ register_offense(block_pass_node)
59
+ end
60
+ end
61
+ alias on_defs on_def
62
+
63
+ private
64
+
65
+ def expected_block_forwarding_style?(node, last_argument)
66
+ if style == :anonymous
67
+ !explicit_block_argument?(last_argument) ||
68
+ use_kwarg_in_method_definition?(node) ||
69
+ use_block_argument_as_local_variable?(node, last_argument)
70
+ else
71
+ !anonymous_block_argument?(last_argument)
72
+ end
73
+ end
74
+
75
+ def use_kwarg_in_method_definition?(node)
76
+ node.arguments.each_descendant(:kwarg, :kwoptarg).any?
77
+ end
78
+
79
+ def anonymous_block_argument?(node)
80
+ node.blockarg_type? && node.name.nil?
81
+ end
82
+
83
+ def explicit_block_argument?(node)
84
+ node.blockarg_type? && !node.name.nil?
85
+ end
86
+
87
+ def use_block_argument_as_local_variable?(node, last_argument)
88
+ return if node.body.nil?
89
+
90
+ node.body.each_descendant(:lvar).any? do |lvar|
91
+ !lvar.parent.block_pass_type? && lvar.source == last_argument.source[1..-1]
92
+ end
93
+ end
94
+
95
+ def register_offense(block_argument)
96
+ add_offense(block_argument, message: format(MSG, style: style)) do |corrector|
97
+ corrector.replace(block_argument, '&')
98
+
99
+ arguments = block_argument.parent
100
+
101
+ add_parentheses(arguments, corrector) unless arguments.parenthesized_call?
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -3,7 +3,8 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Security
6
- # This cop checks for the use of `Kernel#open` and `URI.open`.
6
+ # This cop checks for the use of `Kernel#open` and `URI.open` with dynamic
7
+ # data.
7
8
  #
8
9
  # `Kernel#open` and `URI.open` enable not only file access but also process
9
10
  # invocation by prefixing a pipe symbol (e.g., `open("| ls")`).
@@ -11,6 +12,9 @@ module RuboCop
11
12
  # the argument of `Kernel#open` and `URI.open`. It would be better to use
12
13
  # `File.open`, `IO.popen` or `URI.parse#open` explicitly.
13
14
  #
15
+ # NOTE: `open` and `URI.open` with literal strings are not flagged by this
16
+ # cop.
17
+ #
14
18
  # @safety
15
19
  # This cop could register false positives if `open` is redefined
16
20
  # in a class and then used without a receiver in that class.
@@ -18,12 +22,18 @@ module RuboCop
18
22
  # @example
19
23
  # # bad
20
24
  # open(something)
25
+ # open("| #{something}")
21
26
  # URI.open(something)
22
27
  #
23
28
  # # good
24
29
  # File.open(something)
25
30
  # IO.popen(something)
26
31
  # URI.parse(something).open
32
+ #
33
+ # # good (literal strings)
34
+ # open("foo.text")
35
+ # open("| foo")
36
+ # URI.open("http://example.com")
27
37
  class Open < Base
28
38
  MSG = 'The use of `%<receiver>sopen` is a serious security risk.'
29
39
  RESTRICT_ON_SEND = %i[open].freeze
@@ -4,6 +4,12 @@ module RuboCop
4
4
  module Cop
5
5
  module Style
6
6
  # Checks for uses of the character literal ?x.
7
+ # Starting with Ruby 1.9 character literals are
8
+ # essentially one-character strings, so this syntax
9
+ # is mostly redundant at this point.
10
+ #
11
+ # ? character literal can be used to express meta and control character.
12
+ # That's a good use case of ? literal so it doesn't count it as an offense.
7
13
  #
8
14
  # @example
9
15
  # # bad
@@ -12,8 +18,9 @@ module RuboCop
12
18
  # # good
13
19
  # 'x'
14
20
  #
15
- # # good
21
+ # # good - control & meta escapes
16
22
  # ?\C-\M-d
23
+ # "\C-\M-d" # same as above
17
24
  class CharacterLiteral < Base
18
25
  include StringHelp
19
26
  extend AutoCorrector
@@ -16,6 +16,7 @@ module RuboCop
16
16
  #
17
17
  # @example
18
18
  # # bad
19
+ # array.reject(&:nil?)
19
20
  # array.reject { |e| e.nil? }
20
21
  # array.select { |e| !e.nil? }
21
22
  #
@@ -23,6 +24,7 @@ module RuboCop
23
24
  # array.compact
24
25
  #
25
26
  # # bad
27
+ # hash.reject!(&:nil?)
26
28
  # hash.reject! { |k, v| v.nil? }
27
29
  # hash.select! { |k, v| !v.nil? }
28
30
  #
@@ -37,11 +39,18 @@ module RuboCop
37
39
 
38
40
  RESTRICT_ON_SEND = %i[reject reject! select select!].freeze
39
41
 
42
+ # @!method reject_method_with_block_pass?(node)
43
+ def_node_matcher :reject_method_with_block_pass?, <<~PATTERN
44
+ (send _ {:reject :reject!}
45
+ (block_pass
46
+ (sym :nil?)))
47
+ PATTERN
48
+
40
49
  # @!method reject_method?(node)
41
50
  def_node_matcher :reject_method?, <<~PATTERN
42
51
  (block
43
52
  (send
44
- _ ${:reject :reject!})
53
+ _ {:reject :reject!})
45
54
  $(args ...)
46
55
  (send
47
56
  $(lvar _) :nil?))
@@ -51,7 +60,7 @@ module RuboCop
51
60
  def_node_matcher :select_method?, <<~PATTERN
52
61
  (block
53
62
  (send
54
- _ ${:select :select!})
63
+ _ {:select :select!})
55
64
  $(args ...)
56
65
  (send
57
66
  (send
@@ -59,16 +68,9 @@ module RuboCop
59
68
  PATTERN
60
69
 
61
70
  def on_send(node)
62
- block_node = node.parent
63
- return unless block_node&.block_type?
64
-
65
- return unless (method_name, args, receiver =
66
- reject_method?(block_node) || select_method?(block_node))
71
+ return unless (range = offense_range(node))
67
72
 
68
- return unless args.last.source == receiver.source
69
-
70
- range = offense_range(node, block_node)
71
- good = good_method_name(method_name)
73
+ good = good_method_name(node.method_name)
72
74
  message = format(MSG, good: good, bad: range.source)
73
75
 
74
76
  add_offense(range, message: message) { |corrector| corrector.replace(range, good) }
@@ -76,6 +78,22 @@ module RuboCop
76
78
 
77
79
  private
78
80
 
81
+ def offense_range(node)
82
+ if reject_method_with_block_pass?(node)
83
+ range(node, node)
84
+ else
85
+ block_node = node.parent
86
+
87
+ return unless block_node&.block_type?
88
+ unless (args, receiver = reject_method?(block_node) || select_method?(block_node))
89
+ return
90
+ end
91
+ return unless args.last.source == receiver.source
92
+
93
+ range(node, block_node)
94
+ end
95
+ end
96
+
79
97
  def good_method_name(method_name)
80
98
  if method_name.to_s.end_with?('!')
81
99
  'compact!'
@@ -84,8 +102,8 @@ module RuboCop
84
102
  end
85
103
  end
86
104
 
87
- def offense_range(send_node, block_node)
88
- range_between(send_node.loc.selector.begin_pos, block_node.loc.end.end_pos)
105
+ def range(begin_pos_node, end_pos_node)
106
+ range_between(begin_pos_node.loc.selector.begin_pos, end_pos_node.loc.end.end_pos)
89
107
  end
90
108
  end
91
109
  end
@@ -77,14 +77,14 @@ module RuboCop
77
77
 
78
78
  def collection_looping_method?(node)
79
79
  # TODO: Remove `Symbol#to_s` after supporting only Ruby >= 2.7.
80
- method_name = node.send_node.method_name.to_s
80
+ method_name = node.method_name.to_s
81
81
  method_name.start_with?('each') || method_name.end_with?('_each')
82
82
  end
83
83
 
84
84
  def same_collection_looping?(node, sibling)
85
85
  sibling&.block_type? &&
86
86
  sibling.send_node.method?(node.method_name) &&
87
- sibling.send_node.receiver == node.send_node.receiver &&
87
+ sibling.receiver == node.receiver &&
88
88
  sibling.send_node.arguments == node.send_node.arguments
89
89
  end
90
90
  end
@@ -79,6 +79,8 @@ module RuboCop
79
79
  when_nodes.each do |when_node|
80
80
  conditions = when_node.conditions
81
81
 
82
+ replace_then_with_line_break(corrector, conditions, when_node)
83
+
82
84
  next unless conditions.size > 1
83
85
 
84
86
  range = range_between(conditions.first.source_range.begin_pos,
@@ -97,6 +99,14 @@ module RuboCop
97
99
  line_beginning = case_range.adjust(begin_pos: -case_range.column)
98
100
  corrector.insert_before(line_beginning, comments)
99
101
  end
102
+
103
+ def replace_then_with_line_break(corrector, conditions, when_node)
104
+ return unless when_node.parent.parent && when_node.then?
105
+
106
+ range = range_between(conditions.last.source_range.end_pos, when_node.loc.begin.end_pos)
107
+
108
+ corrector.replace(range, "\n")
109
+ end
100
110
  end
101
111
  end
102
112
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Favor `File.(bin)read` convenience methods.
7
+ #
8
+ # @example
9
+ # ## text mode
10
+ # # bad
11
+ # File.open(filename).read
12
+ # File.open(filename, &:read)
13
+ # File.open(filename) { |f| f.read }
14
+ # File.open(filename) do |f|
15
+ # f.read
16
+ # end
17
+ # File.open(filename, 'r').read
18
+ # File.open(filename, 'r', &:read)
19
+ # File.open(filename, 'r') do |f|
20
+ # f.read
21
+ # end
22
+ #
23
+ # # good
24
+ # File.read(filename)
25
+ #
26
+ # @example
27
+ # ## binary mode
28
+ # # bad
29
+ # File.open(filename, 'rb').read
30
+ # File.open(filename, 'rb', &:read)
31
+ # File.open(filename, 'rb') do |f|
32
+ # f.read
33
+ # end
34
+ #
35
+ # # good
36
+ # File.binread(filename)
37
+ #
38
+ class FileRead < Base
39
+ extend AutoCorrector
40
+ include RangeHelp
41
+
42
+ MSG = 'Use `File.%<read_method>s`.'
43
+
44
+ RESTRICT_ON_SEND = %i[open].freeze
45
+
46
+ READ_FILE_START_TO_FINISH_MODES = %w[r rt rb r+ r+t r+b].to_set.freeze
47
+
48
+ # @!method file_open?(node)
49
+ def_node_matcher :file_open?, <<~PATTERN
50
+ (send
51
+ (const {nil? cbase} :File)
52
+ :open
53
+ $_
54
+ (str $%READ_FILE_START_TO_FINISH_MODES)?
55
+ $(block-pass (sym :read))?
56
+ )
57
+ PATTERN
58
+
59
+ # @!method send_read?(node)
60
+ def_node_matcher :send_read?, <<~PATTERN
61
+ (send _ :read)
62
+ PATTERN
63
+
64
+ # @!method block_read?(node)
65
+ def_node_matcher :block_read?, <<~PATTERN
66
+ (block _ (args (arg $_)) (send (lvar $_) :read))
67
+ PATTERN
68
+
69
+ def on_send(node)
70
+ evidence(node) do |filename, mode, read_node|
71
+ message = format(MSG, read_method: read_method(mode))
72
+
73
+ add_offense(read_node, message: message) do |corrector|
74
+ range = range_between(node.loc.selector.begin_pos, read_node.loc.expression.end_pos)
75
+ replacement = "#{read_method(mode)}(#{filename.source})"
76
+
77
+ corrector.replace(range, replacement)
78
+ end
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def evidence(node)
85
+ file_open?(node) do |filename, mode_array, block_pass|
86
+ read_node?(node, block_pass) do |read_node|
87
+ yield(filename, mode_array.first || 'r', read_node)
88
+ end
89
+ end
90
+ end
91
+
92
+ def read_node?(node, block_pass)
93
+ if block_pass.any?
94
+ yield(node)
95
+ elsif file_open_read?(node.parent)
96
+ yield(node.parent)
97
+ end
98
+ end
99
+
100
+ def file_open_read?(node)
101
+ return true if send_read?(node)
102
+
103
+ block_read?(node) { |block_arg, read_lvar| block_arg == read_lvar }
104
+ end
105
+
106
+ def read_method(mode)
107
+ mode.end_with?('b') ? :binread : :read
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end