rubocop 0.78.0 → 0.79.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +2 -1
  4. data/lib/rubocop.rb +6 -0
  5. data/lib/rubocop/ast/builder.rb +43 -42
  6. data/lib/rubocop/ast/node/def_node.rb +11 -0
  7. data/lib/rubocop/ast/node/forward_args_node.rb +18 -0
  8. data/lib/rubocop/ast/traversal.rb +11 -3
  9. data/lib/rubocop/cli/command/show_cops.rb +11 -4
  10. data/lib/rubocop/config_loader.rb +19 -19
  11. data/lib/rubocop/config_validator.rb +49 -91
  12. data/lib/rubocop/cop/autocorrect_logic.rb +6 -3
  13. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +1 -1
  14. data/lib/rubocop/cop/generator.rb +3 -4
  15. data/lib/rubocop/cop/generator/configuration_injector.rb +1 -1
  16. data/lib/rubocop/cop/layout/hash_alignment.rb +8 -4
  17. data/lib/rubocop/cop/layout/multiline_block_layout.rb +14 -5
  18. data/lib/rubocop/cop/layout/space_before_block_braces.rb +2 -2
  19. data/lib/rubocop/cop/lint/debugger.rb +2 -2
  20. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +1 -1
  21. data/lib/rubocop/cop/migration/department_name.rb +16 -1
  22. data/lib/rubocop/cop/mixin/frozen_string_literal.rb +1 -7
  23. data/lib/rubocop/cop/naming/method_parameter_name.rb +1 -1
  24. data/lib/rubocop/cop/registry.rb +7 -2
  25. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +4 -207
  26. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +168 -0
  27. data/lib/rubocop/cop/style/method_call_with_args_parentheses/require_parentheses.rb +54 -0
  28. data/lib/rubocop/cop/style/multiline_when_then.rb +5 -1
  29. data/lib/rubocop/cop/style/numeric_predicate.rb +4 -3
  30. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +7 -7
  31. data/lib/rubocop/cop/style/yoda_condition.rb +16 -1
  32. data/lib/rubocop/rspec/shared_contexts.rb +5 -0
  33. data/lib/rubocop/target_ruby.rb +151 -0
  34. data/lib/rubocop/version.rb +1 -1
  35. metadata +8 -4
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ class MethodCallWithArgsParentheses
7
+ # Style omit_parentheses
8
+ module OmitParentheses
9
+ TRAILING_WHITESPACE_REGEX = /\s+\Z/.freeze
10
+
11
+ def on_send(node)
12
+ return unless node.parenthesized?
13
+ return if node.implicit_call?
14
+ return if super_call_without_arguments?(node)
15
+ return if allowed_camel_case_method_call?(node)
16
+ return if legitimate_call_with_parentheses?(node)
17
+
18
+ add_offense(node, location: node.loc.begin.join(node.loc.end))
19
+ end
20
+ alias on_csend on_send
21
+ alias on_super on_send
22
+ alias on_yield on_send
23
+
24
+ def autocorrect(node)
25
+ lambda do |corrector|
26
+ if parentheses_at_the_end_of_multiline_call?(node)
27
+ corrector.replace(args_begin(node), ' \\')
28
+ else
29
+ corrector.replace(args_begin(node), ' ')
30
+ end
31
+ corrector.remove(node.loc.end)
32
+ end
33
+ end
34
+
35
+ def message(_node = nil)
36
+ 'Omit parentheses for method calls with arguments.'
37
+ end
38
+
39
+ private
40
+
41
+ def super_call_without_arguments?(node)
42
+ node.super_type? && node.arguments.none?
43
+ end
44
+
45
+ def allowed_camel_case_method_call?(node)
46
+ node.camel_case_method? &&
47
+ (node.arguments.none? ||
48
+ cop_config['AllowParenthesesInCamelCaseMethod'])
49
+ end
50
+
51
+ def parentheses_at_the_end_of_multiline_call?(node)
52
+ node.multiline? &&
53
+ node.loc.begin.source_line
54
+ .gsub(TRAILING_WHITESPACE_REGEX, '')
55
+ .end_with?('(')
56
+ end
57
+
58
+ def legitimate_call_with_parentheses?(node)
59
+ call_in_literals?(node) ||
60
+ call_with_ambiguous_arguments?(node) ||
61
+ call_in_logical_operators?(node) ||
62
+ call_in_optional_arguments?(node) ||
63
+ allowed_multiline_call_with_parentheses?(node) ||
64
+ allowed_chained_call_with_parentheses?(node)
65
+ end
66
+
67
+ def call_in_literals?(node)
68
+ node.parent &&
69
+ (node.parent.pair_type? ||
70
+ node.parent.array_type? ||
71
+ node.parent.range_type? ||
72
+ splat?(node.parent) ||
73
+ ternary_if?(node.parent))
74
+ end
75
+
76
+ def call_in_logical_operators?(node)
77
+ node.parent &&
78
+ (logical_operator?(node.parent) ||
79
+ node.parent.send_type? &&
80
+ node.parent.arguments.any?(&method(:logical_operator?)))
81
+ end
82
+
83
+ def call_in_optional_arguments?(node)
84
+ node.parent &&
85
+ (node.parent.optarg_type? || node.parent.kwoptarg_type?)
86
+ end
87
+
88
+ def call_with_ambiguous_arguments?(node)
89
+ call_with_braced_block?(node) ||
90
+ call_as_argument_or_chain?(node) ||
91
+ hash_literal_in_arguments?(node) ||
92
+ node.descendants.any? do |n|
93
+ ambigious_literal?(n) || logical_operator?(n) ||
94
+ call_with_braced_block?(n)
95
+ end
96
+ end
97
+
98
+ def call_with_braced_block?(node)
99
+ (node.send_type? || node.super_type?) &&
100
+ node.block_node && node.block_node.braces?
101
+ end
102
+
103
+ def call_as_argument_or_chain?(node)
104
+ node.parent &&
105
+ (node.parent.send_type? && !assigned_before?(node.parent, node) ||
106
+ node.parent.csend_type? || node.parent.super_type?)
107
+ end
108
+
109
+ def hash_literal_in_arguments?(node)
110
+ node.arguments.any? do |n|
111
+ hash_literal?(n) ||
112
+ n.send_type? && node.descendants.any?(&method(:hash_literal?))
113
+ end
114
+ end
115
+
116
+ def allowed_multiline_call_with_parentheses?(node)
117
+ cop_config['AllowParenthesesInMultilineCall'] && node.multiline?
118
+ end
119
+
120
+ def allowed_chained_call_with_parentheses?(node)
121
+ return false unless cop_config['AllowParenthesesInChaining']
122
+
123
+ previous = node.descendants.first
124
+ return false unless previous&.send_type?
125
+
126
+ previous.parenthesized? ||
127
+ allowed_chained_call_with_parentheses?(previous)
128
+ end
129
+
130
+ def ambigious_literal?(node)
131
+ splat?(node) || ternary_if?(node) || regexp_slash_literal?(node) ||
132
+ unary_literal?(node)
133
+ end
134
+
135
+ def splat?(node)
136
+ node.splat_type? || node.kwsplat_type? || node.block_pass_type?
137
+ end
138
+
139
+ def ternary_if?(node)
140
+ node.if_type? && node.ternary?
141
+ end
142
+
143
+ def logical_operator?(node)
144
+ (node.and_type? || node.or_type?) && node.logical_operator?
145
+ end
146
+
147
+ def hash_literal?(node)
148
+ node.hash_type? && node.braces?
149
+ end
150
+
151
+ def regexp_slash_literal?(node)
152
+ node.regexp_type? && node.loc.begin.source == '/'
153
+ end
154
+
155
+ def unary_literal?(node)
156
+ node.numeric_type? && node.sign? ||
157
+ node.parent&.send_type? && node.parent&.unary_operation?
158
+ end
159
+
160
+ def assigned_before?(node, target)
161
+ node.assignment? &&
162
+ node.loc.operator.begin < target.loc.begin
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ class MethodCallWithArgsParentheses
7
+ # Style require_parentheses
8
+ module RequireParentheses
9
+ def on_send(node)
10
+ return if ignored_method?(node.method_name)
11
+ return if matches_ignored_pattern?(node.method_name)
12
+ return if eligible_for_parentheses_omission?(node)
13
+ return unless node.arguments? && !node.parenthesized?
14
+
15
+ add_offense(node)
16
+ end
17
+ alias on_csend on_send
18
+ alias on_super on_send
19
+ alias on_yield on_send
20
+
21
+ def autocorrect(node)
22
+ lambda do |corrector|
23
+ corrector.replace(args_begin(node), '(')
24
+
25
+ unless args_parenthesized?(node)
26
+ corrector.insert_after(args_end(node), ')')
27
+ end
28
+ end
29
+ end
30
+
31
+ def message(_node = nil)
32
+ 'Use parentheses for method calls with arguments.'
33
+ end
34
+
35
+ private
36
+
37
+ def eligible_for_parentheses_omission?(node)
38
+ node.operator_method? || node.setter_method? || ignored_macro?(node)
39
+ end
40
+
41
+ def included_macros_list
42
+ cop_config.fetch('IncludedMacros', []).map(&:to_sym)
43
+ end
44
+
45
+ def ignored_macro?(node)
46
+ cop_config['IgnoreMacros'] &&
47
+ node.macro? &&
48
+ !included_macros_list.include?(node.method_name)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -35,7 +35,7 @@ module RuboCop
35
35
  return if !node.children.last.nil? && !node.multiline? && node.then?
36
36
 
37
37
  # With more than one statements after then, there's not offense
38
- return if node.children.last&.begin_type?
38
+ return if accept_node_type?(node.body)
39
39
 
40
40
  add_offense(node, location: :begin)
41
41
  end
@@ -49,6 +49,10 @@ module RuboCop
49
49
  )
50
50
  end
51
51
  end
52
+
53
+ def accept_node_type?(node)
54
+ node&.begin_type? || node&.array_type? || node&.hash_type?
55
+ end
52
56
  end
53
57
  end
54
58
  end
@@ -54,9 +54,10 @@ module RuboCop
54
54
  }.freeze
55
55
 
56
56
  def on_send(node)
57
- return if node.each_ancestor(:send, :block).any? do |ancestor|
58
- ignored_method?(ancestor.method_name)
59
- end
57
+ return if ignored_method?(node.method_name) ||
58
+ node.each_ancestor(:send, :block).any? do |ancestor|
59
+ ignored_method?(ancestor.method_name)
60
+ end
60
61
 
61
62
  numeric, replacement = check(node)
62
63
 
@@ -88,27 +88,27 @@ module RuboCop
88
88
  end
89
89
 
90
90
  def contains_preferred_delimiter?(node, type)
91
- preferred_delimiters = preferred_delimiters_for(type)
92
- node
93
- .children.map { |n| string_source(n) }.compact
94
- .any? { |s| preferred_delimiters.any? { |d| s.include?(d) } }
91
+ contains_delimiter?(node, preferred_delimiters_for(type))
95
92
  end
96
93
 
97
94
  def include_same_character_as_used_for_delimiter?(node, type)
98
95
  return false unless %w[%w %i].include?(type)
99
96
 
100
97
  used_delimiters = matchpairs(begin_source(node)[-1])
101
- escaped_delimiters = used_delimiters.map { |d| "\\#{d}" }.join('|')
98
+ contains_delimiter?(node, used_delimiters)
99
+ end
102
100
 
101
+ def contains_delimiter?(node, delimiters)
102
+ delimiters_regexp = Regexp.union(delimiters)
103
103
  node
104
104
  .children.map { |n| string_source(n) }.compact
105
- .any? { |s| Regexp.new(escaped_delimiters) =~ s }
105
+ .any? { |s| delimiters_regexp =~ s }
106
106
  end
107
107
 
108
108
  def string_source(node)
109
109
  if node.is_a?(String)
110
110
  node
111
- elsif node.respond_to?(:type) && node.str_type?
111
+ elsif node.respond_to?(:type) && (node.str_type? || node.sym_type?)
112
112
  node.source
113
113
  end
114
114
  end
@@ -67,9 +67,16 @@ module RuboCop
67
67
 
68
68
  NONCOMMUTATIVE_OPERATORS = %i[===].freeze
69
69
 
70
+ PROGRAM_NAMES = %i[$0 $PROGRAM_NAME].freeze
71
+
72
+ def_node_matcher :file_constant_equal_program_name?, <<~PATTERN
73
+ (send #source_file_path_constant? {:== :!=} (gvar #program_name?))
74
+ PATTERN
75
+
70
76
  def on_send(node)
71
77
  return unless yoda_compatible_condition?(node)
72
- return if equality_only? && non_equality_operator?(node)
78
+ return if equality_only? && non_equality_operator?(node) ||
79
+ file_constant_equal_program_name?(node)
73
80
 
74
81
  valid_yoda?(node) || add_offense(node)
75
82
  end
@@ -135,6 +142,14 @@ module RuboCop
135
142
  def noncommutative_operator?(node)
136
143
  NONCOMMUTATIVE_OPERATORS.include?(node.method_name)
137
144
  end
145
+
146
+ def source_file_path_constant?(node)
147
+ node.source == '__FILE__'
148
+ end
149
+
150
+ def program_name?(name)
151
+ PROGRAM_NAMES.include?(name)
152
+ end
138
153
  end
139
154
  end
140
155
  end
@@ -52,6 +52,7 @@ RSpec.shared_context 'config', :config do
52
52
  cop_name = described_class.cop_name
53
53
  hash[cop_name] = RuboCop::ConfigLoader
54
54
  .default_configuration[cop_name]
55
+ .merge('Enabled' => true) # in case it is 'pending'
55
56
  .merge(cop_config)
56
57
  end
57
58
 
@@ -88,3 +89,7 @@ end
88
89
  RSpec.shared_context 'ruby 2.6', :ruby26 do
89
90
  let(:ruby_version) { 2.6 }
90
91
  end
92
+
93
+ RSpec.shared_context 'ruby 2.7', :ruby27 do
94
+ let(:ruby_version) { 2.7 }
95
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ # The kind of Ruby that code inspected by RuboCop is written in.
5
+ class TargetRuby
6
+ KNOWN_RUBIES = [2.3, 2.4, 2.5, 2.6, 2.7].freeze
7
+ DEFAULT_VERSION = KNOWN_RUBIES.first
8
+
9
+ OBSOLETE_RUBIES = {
10
+ 1.9 => '0.50', 2.0 => '0.50', 2.1 => '0.58', 2.2 => '0.69'
11
+ }.freeze
12
+ private_constant :KNOWN_RUBIES, :OBSOLETE_RUBIES
13
+
14
+ # A place where information about a target ruby version is found.
15
+ class Source
16
+ attr_reader :version, :name
17
+
18
+ def initialize(config)
19
+ @config = config
20
+ @version = find_version
21
+ end
22
+
23
+ def to_s
24
+ name
25
+ end
26
+ end
27
+
28
+ # The target ruby version may be configured in RuboCop's config.
29
+ class RuboCopConfig < Source
30
+ def name
31
+ "`TargetRubyVersion` parameter (in #{@config.smart_loaded_path})"
32
+ end
33
+
34
+ private
35
+
36
+ def find_version
37
+ @config.for_all_cops['TargetRubyVersion']&.to_f
38
+ end
39
+ end
40
+
41
+ # The target ruby version may be found in a .ruby-version file.
42
+ class RubyVersionFile < Source
43
+ FILENAME = '.ruby-version'
44
+
45
+ def name
46
+ "`#{FILENAME}`"
47
+ end
48
+
49
+ private
50
+
51
+ def find_version
52
+ file = ruby_version_file
53
+ return unless file && File.file?(file)
54
+
55
+ File.read(file).match(/\A(ruby-)?(?<version>\d+\.\d+)/) do |md|
56
+ md[:version].to_f
57
+ end
58
+ end
59
+
60
+ def ruby_version_file
61
+ @ruby_version_file ||=
62
+ @config.find_file_upwards(FILENAME,
63
+ @config.base_dir_for_path_parameters)
64
+ end
65
+ end
66
+
67
+ # The lock file of Bundler may identify the target ruby version.
68
+ class BundlerLockFile < Source
69
+ def name
70
+ "`#{bundler_lock_file_path}`"
71
+ end
72
+
73
+ private
74
+
75
+ def find_version
76
+ lock_file_path = bundler_lock_file_path
77
+ return nil unless lock_file_path
78
+
79
+ in_ruby_section = false
80
+ File.foreach(lock_file_path) do |line|
81
+ # If ruby is in Gemfile.lock or gems.lock, there should be two lines
82
+ # towards the bottom of the file that look like:
83
+ # RUBY VERSION
84
+ # ruby W.X.YpZ
85
+ # We ultimately want to match the "ruby W.X.Y.pZ" line, but there's
86
+ # extra logic to make sure we only start looking once we've seen the
87
+ # "RUBY VERSION" line.
88
+ in_ruby_section ||= line.match(/^\s*RUBY\s*VERSION\s*$/)
89
+ next unless in_ruby_section
90
+
91
+ # We currently only allow this feature to work with MRI ruby. If
92
+ # jruby (or something else) is used by the project, it's lock file
93
+ # will have a line that looks like:
94
+ # RUBY VERSION
95
+ # ruby W.X.YpZ (jruby x.x.x.x)
96
+ # The regex won't match in this situation.
97
+ result = line.match(/^\s*ruby\s+(\d+\.\d+)[p.\d]*\s*$/)
98
+ return result.captures.first.to_f if result
99
+ end
100
+ end
101
+
102
+ def bundler_lock_file_path
103
+ @config.bundler_lock_file_path
104
+ end
105
+ end
106
+
107
+ # If all else fails, a default version will be picked.
108
+ class Default < Source
109
+ def name
110
+ 'default'
111
+ end
112
+
113
+ private
114
+
115
+ def find_version
116
+ DEFAULT_VERSION
117
+ end
118
+ end
119
+
120
+ def self.supported_versions
121
+ KNOWN_RUBIES
122
+ end
123
+
124
+ SOURCES = [RuboCopConfig, RubyVersionFile, BundlerLockFile, Default].freeze
125
+ private_constant :SOURCES
126
+
127
+ def initialize(config)
128
+ @config = config
129
+ end
130
+
131
+ def source
132
+ @source ||= SOURCES.each.lazy.map { |c| c.new(@config) }.detect(&:version)
133
+ end
134
+
135
+ def version
136
+ source.version
137
+ end
138
+
139
+ def supported?
140
+ KNOWN_RUBIES.include?(version)
141
+ end
142
+
143
+ def rubocop_version_with_support
144
+ if supported?
145
+ RuboCop::Version.version
146
+ else
147
+ OBSOLETE_RUBIES[version]
148
+ end
149
+ end
150
+ end
151
+ end