rubocop 1.90.0 → 1.91.0

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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +238 -10
  3. data/config/obsoletion.yml +9 -0
  4. data/lib/rubocop/changed_files.rb +112 -0
  5. data/lib/rubocop/cli/command/execute_runner.rb +21 -1
  6. data/lib/rubocop/cli.rb +9 -3
  7. data/lib/rubocop/comment_config/directive_range.rb +3 -1
  8. data/lib/rubocop/comment_config/disable_next.rb +49 -3
  9. data/lib/rubocop/comment_config/push_pop.rb +55 -0
  10. data/lib/rubocop/comment_config.rb +21 -51
  11. data/lib/rubocop/config.rb +10 -0
  12. data/lib/rubocop/config_loader.rb +12 -4
  13. data/lib/rubocop/config_loader_resolver.rb +49 -7
  14. data/lib/rubocop/config_obsoletion/cop_rule.rb +2 -1
  15. data/lib/rubocop/config_obsoletion/removed_cop.rb +8 -0
  16. data/lib/rubocop/config_obsoletion.rb +1 -1
  17. data/lib/rubocop/config_validator.rb +18 -3
  18. data/lib/rubocop/cop/badge.rb +1 -1
  19. data/lib/rubocop/cop/base.rb +15 -3
  20. data/lib/rubocop/cop/correctors.rb +2 -2
  21. data/lib/rubocop/cop/gemspec/require_mfa.rb +1 -1
  22. data/lib/rubocop/cop/generator.rb +1 -1
  23. data/lib/rubocop/cop/internal_affairs/on_send_without_on_csend.rb +1 -1
  24. data/lib/rubocop/cop/layout/argument_alignment.rb +2 -0
  25. data/lib/rubocop/cop/layout/comment_indentation.rb +8 -5
  26. data/lib/rubocop/cop/layout/else_alignment.rb +1 -0
  27. data/lib/rubocop/cop/layout/empty_lines_after_module_inclusion.rb +1 -1
  28. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +17 -4
  29. data/lib/rubocop/cop/layout/first_parameter_indentation.rb +11 -0
  30. data/lib/rubocop/cop/layout/hash_alignment.rb +6 -5
  31. data/lib/rubocop/cop/layout/indentation_width.rb +2 -2
  32. data/lib/rubocop/cop/layout/line_length.rb +19 -1
  33. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +14 -3
  34. data/lib/rubocop/cop/layout/space_around_operators.rb +5 -1
  35. data/lib/rubocop/cop/lint/argument_mismatch.rb +14 -4
  36. data/lib/rubocop/cop/lint/assignment_in_condition.rb +2 -0
  37. data/lib/rubocop/cop/lint/cop_directive_syntax.rb +88 -14
  38. data/lib/rubocop/cop/lint/duplicate_methods.rb +1 -1
  39. data/lib/rubocop/cop/lint/empty_block.rb +1 -1
  40. data/lib/rubocop/cop/lint/literal_as_condition.rb +8 -2
  41. data/lib/rubocop/cop/lint/misplaced_magic_comment.rb +185 -0
  42. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +1 -1
  43. data/lib/rubocop/cop/lint/nested_method_definition.rb +1 -1
  44. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +1 -1
  45. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +17 -1
  46. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +30 -16
  47. data/lib/rubocop/cop/lint.rb +1 -0
  48. data/lib/rubocop/cop/migration/department_name.rb +1 -1
  49. data/lib/rubocop/cop/mixin/alignment.rb +1 -1
  50. data/lib/rubocop/cop/mixin/annotation_comment.rb +1 -1
  51. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +11 -1
  52. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +6 -2
  53. data/lib/rubocop/cop/mixin/hash_transform_method.rb +1 -1
  54. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +12 -1
  55. data/lib/rubocop/cop/mixin.rb +2 -2
  56. data/lib/rubocop/cop/naming/block_forwarding.rb +4 -1
  57. data/lib/rubocop/cop/naming/block_parameter_name.rb +1 -1
  58. data/lib/rubocop/cop/registry.rb +9 -1
  59. data/lib/rubocop/cop/severity.rb +12 -0
  60. data/lib/rubocop/cop/style/access_modifier_declarations.rb +3 -2
  61. data/lib/rubocop/cop/style/arguments_forwarding.rb +2 -2
  62. data/lib/rubocop/cop/style/case_like_if.rb +3 -3
  63. data/lib/rubocop/cop/style/constant_visibility.rb +6 -2
  64. data/lib/rubocop/cop/style/data_inheritance.rb +14 -0
  65. data/lib/rubocop/cop/style/directive_scope.rb +154 -21
  66. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +53 -9
  67. data/lib/rubocop/cop/style/documentation_method.rb +1 -9
  68. data/lib/rubocop/cop/style/each_for_simple_loop.rb +1 -1
  69. data/lib/rubocop/cop/style/empty_block_parameter.rb +1 -1
  70. data/lib/rubocop/cop/style/empty_lambda_parameter.rb +1 -1
  71. data/lib/rubocop/cop/style/empty_method.rb +12 -2
  72. data/lib/rubocop/cop/style/endless_method.rb +31 -12
  73. data/lib/rubocop/cop/style/file_null.rb +2 -2
  74. data/lib/rubocop/cop/style/float_division.rb +10 -16
  75. data/lib/rubocop/cop/style/for.rb +5 -0
  76. data/lib/rubocop/cop/style/format_string_token.rb +1 -1
  77. data/lib/rubocop/cop/style/guard_clause.rb +4 -0
  78. data/lib/rubocop/cop/style/hash_lookup_method.rb +3 -0
  79. data/lib/rubocop/cop/style/magic_comment_format.rb +1 -1
  80. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +1 -1
  81. data/lib/rubocop/cop/style/nested_modifier.rb +9 -4
  82. data/lib/rubocop/cop/style/nil_comparison.rb +36 -8
  83. data/lib/rubocop/cop/style/nil_lambda.rb +1 -1
  84. data/lib/rubocop/cop/style/non_nil_check.rb +18 -2
  85. data/lib/rubocop/cop/style/not.rb +3 -1
  86. data/lib/rubocop/cop/style/numeric_literal_prefix.rb +11 -1
  87. data/lib/rubocop/cop/style/one_line_conditional.rb +1 -1
  88. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -1
  89. data/lib/rubocop/cop/style/parentheses_around_condition.rb +6 -1
  90. data/lib/rubocop/cop/style/redundant_condition.rb +4 -4
  91. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +10 -1
  92. data/lib/rubocop/cop/style/redundant_fetch_block.rb +1 -1
  93. data/lib/rubocop/cop/style/redundant_parentheses.rb +11 -1
  94. data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +6 -5
  95. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +6 -6
  96. data/lib/rubocop/cop/style/single_line_block_params.rb +1 -1
  97. data/lib/rubocop/cop/style/special_global_vars.rb +1 -1
  98. data/lib/rubocop/cop/style/struct_inheritance.rb +14 -0
  99. data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
  100. data/lib/rubocop/cop/style.rb +0 -1
  101. data/lib/rubocop/cop/util.rb +1 -1
  102. data/lib/rubocop/cops_documentation_generator.rb +23 -2
  103. data/lib/rubocop/directive_comment.rb +37 -8
  104. data/lib/rubocop/formatter/disabled_config_formatter.rb +19 -1
  105. data/lib/rubocop/formatter/formatter_set.rb +2 -1
  106. data/lib/rubocop/formatter/github_actions_formatter.rb +6 -5
  107. data/lib/rubocop/formatter/html_formatter.rb +2 -2
  108. data/lib/rubocop/formatter/junit_formatter.rb +1 -1
  109. data/lib/rubocop/formatter/sarif_formatter.rb +181 -0
  110. data/lib/rubocop/formatter.rb +1 -0
  111. data/lib/rubocop/magic_comment.rb +27 -0
  112. data/lib/rubocop/options.rb +59 -1
  113. data/lib/rubocop/path_util.rb +1 -1
  114. data/lib/rubocop/plugin/loader.rb +1 -1
  115. data/lib/rubocop/rake_task.rb +1 -1
  116. data/lib/rubocop/remote_config.rb +4 -4
  117. data/lib/rubocop/result_cache.rb +16 -5
  118. data/lib/rubocop/runner.rb +64 -21
  119. data/lib/rubocop/unified_diff.rb +257 -0
  120. data/lib/rubocop/version.rb +1 -1
  121. data/lib/rubocop.rb +4 -1
  122. metadata +8 -4
  123. data/lib/rubocop/cop/style/double_cop_disable_directive.rb +0 -46
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module RuboCop
6
+ # Lists the files that differ from a git revision, for `--changed`.
7
+ #
8
+ # Shelling out to `git` keeps RuboCop free of a version control dependency,
9
+ # and means the answer is exactly what git would give on the command line.
10
+ #
11
+ # @api private
12
+ class ChangedFiles
13
+ DEFAULT_REVISION = 'HEAD'
14
+ FILESYSTEM_ENCODING = Encoding.find('filesystem')
15
+
16
+ def initialize(revision = nil)
17
+ @revision = revision || DEFAULT_REVISION
18
+ end
19
+
20
+ # Files that were added or modified since the revision, as absolute paths.
21
+ # Deleted files are left out - there is nothing left to inspect - and
22
+ # untracked files are included, since a file that is new to the working
23
+ # tree has changed as surely as one git already knows about.
24
+ #
25
+ # @return [Set<String>]
26
+ def paths
27
+ root = repository_root
28
+
29
+ (modified_paths + untracked_paths).to_set { |path| File.expand_path(path, root) }
30
+ end
31
+
32
+ private
33
+
34
+ def repository_root
35
+ filesystem_path(git('rev-parse', '--show-toplevel').b.chomp)
36
+ end
37
+
38
+ def modified_paths
39
+ # `diff.relative` would make git report paths relative to the current
40
+ # directory rather than to the repository root.
41
+ split_paths(
42
+ git('-c', 'diff.relative=false', 'diff', '--name-only', '--diff-filter=d', '-z', @revision)
43
+ )
44
+ rescue Error
45
+ raise unless unborn_head?
46
+
47
+ # A repository without commits has nothing to diff against, and
48
+ # everything in it is untracked - which is what the caller wants to hear
49
+ # rather than an error about `HEAD` not resolving.
50
+ []
51
+ end
52
+
53
+ def unborn_head?
54
+ return false unless @revision == DEFAULT_REVISION
55
+
56
+ _stdout, _stderr, status = Open3.capture3('git', 'rev-parse', '--verify', '--quiet', 'HEAD')
57
+ !status.success?
58
+ end
59
+
60
+ def untracked_paths
61
+ # `--full-name` anchors the paths to the repository root, the way the
62
+ # paths from `git diff` already are.
63
+ split_paths(git('ls-files', '--others', '--exclude-standard', '--full-name', '-z'))
64
+ end
65
+
66
+ # The `-z` above is what makes these usable: without it git wraps any path
67
+ # that is not plain ASCII in quotes and octal escapes, and those files drop
68
+ # out of the comparison without a word.
69
+ def split_paths(output)
70
+ output.b.split("\0").reject(&:empty?).map { |path| filesystem_path(path) }
71
+ end
72
+
73
+ # git hands back bytes, which Ruby tags with the encoding the process
74
+ # happens to have been started in - splitting those bytes as US-ASCII raises
75
+ # on any path that is not. They have to end up in the encoding `Dir.glob`
76
+ # uses, or a path with a non-ASCII character in it never matches the file
77
+ # list it is compared against.
78
+ #
79
+ # On POSIX those bytes are already the filesystem's own, so the encoding is
80
+ # simply corrected. Windows is the exception: git converts paths to UTF-8 on
81
+ # the way out, while Ruby reports file names in the local code page, so the
82
+ # two have to be reconciled.
83
+ def filesystem_path(path)
84
+ path = path.dup
85
+
86
+ return path.force_encoding(FILESYSTEM_ENCODING) unless Platform.windows?
87
+
88
+ path.force_encoding(Encoding::UTF_8).encode(FILESYSTEM_ENCODING)
89
+ rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
90
+ # A name the local code page cannot represent. Nothing good is left to do
91
+ # with it, and dropping it would be worse than reporting it as-is.
92
+ path.force_encoding(FILESYSTEM_ENCODING)
93
+ end
94
+
95
+ def git(*args)
96
+ stdout, stderr, status = Open3.capture3('git', *args)
97
+
98
+ raise Error, git_error_message(stderr) unless status.success?
99
+
100
+ stdout
101
+ rescue Errno::ENOENT
102
+ raise Error, '--changed requires git to be installed and on your PATH.'
103
+ end
104
+
105
+ def git_error_message(stderr)
106
+ message = stderr.strip
107
+ message = 'git failed with no output on standard error.' if message.empty?
108
+
109
+ "--changed could not ask git which files changed: #{message}"
110
+ end
111
+ end
112
+ end
@@ -28,7 +28,16 @@ module RuboCop
28
28
  all_passed || @options[:auto_gen_config]
29
29
  end
30
30
 
31
- maybe_print_corrected_source
31
+ if @options[:diff]
32
+ print_diffs(runner.diffs)
33
+ else
34
+ maybe_print_corrected_source
35
+ end
36
+
37
+ # A diff means autocorrectable offenses are still in the working
38
+ # tree, so the run has not passed even though they were "corrected"
39
+ # in memory.
40
+ all_pass_or_excluded &&= runner.diffs.empty?
32
41
 
33
42
  merge_todo_audit_status(runner_status(runner, all_pass_or_excluded))
34
43
  end
@@ -119,6 +128,17 @@ module RuboCop
119
128
  "#{Gem.loaded_specs['rubocop'].metadata['bug_tracker_uri']}\n"
120
129
  end
121
130
 
131
+ def print_diffs(diffs)
132
+ return if diffs.empty?
133
+ # Integration tools own stdout when they ask for a machine-readable
134
+ # format, so the diff would only corrupt their input.
135
+ return if INTEGRATION_FORMATTERS.include?(@options[:format])
136
+
137
+ output = @options[:stderr] ? $stderr : $stdout
138
+ output.puts
139
+ diffs.each { |diff| output.print(diff) }
140
+ end
141
+
122
142
  def maybe_print_corrected_source
123
143
  # Integration tools (like RubyMine) expect to have only the JSON result
124
144
  # when specifying JSON format. Similar HTML and JUnit are targeted as well.
data/lib/rubocop/cli.rb CHANGED
@@ -13,7 +13,7 @@ module RuboCop
13
13
  DEFAULT_PARALLEL_OPTIONS = %i[
14
14
  color config debug display_style_guide display_time display_only_fail_level_offenses
15
15
  display_only_failed editor_mode except extra_details fail_level fix_layout format formatters
16
- ignore_disable_comments lint only only_guide_cops out require safe
16
+ ignore_disable_comments lint only only_guide_cops out preview require safe
17
17
  autocorrect safe_autocorrect autocorrect_all
18
18
  ].freeze
19
19
 
@@ -183,12 +183,18 @@ module RuboCop
183
183
 
184
184
  def set_options_to_config_loader
185
185
  ConfigLoader.debug = @options[:debug]
186
- ConfigLoader.disable_pending_cops = @options[:disable_pending_cops]
187
- ConfigLoader.enable_pending_cops = @options[:enable_pending_cops]
188
186
  ConfigLoader.ignore_parent_exclusion = @options[:ignore_parent_exclusion]
189
187
  ConfigLoader.ignore_unrecognized_cops = @options[:ignore_unrecognized_cops]
188
+ set_cop_selection_options_to_config_loader
189
+ end
190
+
191
+ # Options that decide which cops run, as opposed to how configuration is loaded.
192
+ def set_cop_selection_options_to_config_loader
193
+ ConfigLoader.disable_pending_cops = @options[:disable_pending_cops]
194
+ ConfigLoader.enable_pending_cops = @options[:enable_pending_cops]
190
195
  ConfigLoader.enabled_by_default = @options[:enable_all_cops]
191
196
  ConfigLoader.disabled_by_default = @options[:disable_all_cops]
197
+ ConfigLoader.preview = @options[:preview]
192
198
  end
193
199
 
194
200
  def set_options_to_pending_cops_reporter
@@ -22,8 +22,10 @@ module RuboCop
22
22
  CopAnalysis = Struct.new(:line_ranges, :start_line_number, :start_directive) do
23
23
  # The open range (if any) closed at the given line, appended to the
24
24
  # accumulated ranges, each remembering the directive that opened it.
25
+ # Closing above the opening line (e.g. a `pop` directly below a
26
+ # statement whose disable a `next +Cop` suspended) yields no range.
25
27
  def close(line)
26
- return line_ranges unless start_line_number
28
+ return line_ranges if !start_line_number || line < start_line_number
27
29
 
28
30
  line_ranges + [DirectiveRange.new(start_line_number, line, start_directive)]
29
31
  end
@@ -2,14 +2,15 @@
2
2
 
3
3
  module RuboCop
4
4
  class CommentConfig
5
- # Handling of `disable-next` directives: computing the statement scope
6
- # they apply to and tracking directives that attached to nothing.
5
+ # Handling of next-statement directives (`disable-next`, `todo-next` and
6
+ # `next`): computing the statement scope they apply to and tracking
7
+ # directives that attached to nothing.
7
8
  # The scope is the whole statement starting on the next line bearing
8
9
  # code. Comment-only lines between the directive and the code chain (so
9
10
  # several directives can stack), while a blank line breaks the attachment.
10
11
  # @api private
11
12
  module DisableNext
12
- # `disable-next` directives that suppress nothing: nothing follows
13
+ # Next-statement directives that affect nothing: nothing follows
13
14
  # them, or they sit at the end of a code line.
14
15
  def detached_next_directives
15
16
  cop_disabled_line_ranges # ensure the analysis that collects them ran
@@ -48,6 +49,51 @@ module RuboCop
48
49
  analysis.start_line_number, analysis.start_directive)
49
50
  end
50
51
 
52
+ # Applies a `next` directive: `-` cops are disabled for the attached
53
+ # statement (exactly like `disable-next`), `+` cops have any open
54
+ # disable suspended for it. The `resolved_cops` come pre-expanded and
55
+ # qualified, mirroring `push`.
56
+ def apply_next_directive(analyses, resolved_cops, directive)
57
+ bounds = statement_scope_after(directive.line_number)
58
+ return @detached_next_directives << directive unless bounds
59
+
60
+ range = DirectiveRange.new(bounds.begin, bounds.end, directive)
61
+ resolved_cops.each do |op, cops|
62
+ cops.each do |cop|
63
+ if op == '-'
64
+ add_next_range(analyses, cop, range)
65
+ else
66
+ suspend_disable(analyses, cop, bounds)
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ # Applies an `enable-next` directive: every listed cop (departments
73
+ # and `all` included) has any open disable suspended for the attached
74
+ # statement.
75
+ def apply_enable_next(analyses, directive)
76
+ bounds = statement_scope_after(directive.line_number)
77
+ return @detached_next_directives << directive unless bounds
78
+
79
+ directive.cop_names.each do |cop_name|
80
+ suspend_disable(analyses, qualified_cop_name(cop_name), bounds)
81
+ end
82
+ end
83
+
84
+ # Punches a statement-sized hole into the currently open disable of the
85
+ # cop (whether opened by a directive or injected for a config-disabled
86
+ # cop): the open range closes just above the statement and reopens
87
+ # right below it, still attributed to its opening directive. Aligned
88
+ # with `push`, a `+` for a cop that is not disabled is a no-op.
89
+ def suspend_disable(analyses, cop, bounds)
90
+ analysis = analyses[cop]
91
+ return unless analysis.start_line_number
92
+
93
+ analyses[cop] = CopAnalysis.new(analysis.close(bounds.begin - 1),
94
+ bounds.end + 1, analysis.start_directive)
95
+ end
96
+
51
97
  def attached_code_line(directive_line)
52
98
  ((directive_line + 1)..processed_source.lines.size).each do |line|
53
99
  return line unless comment_only_line?(line)
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ class CommentConfig
5
+ # Handling of `push`/`pop` directives and their signed cop arguments:
6
+ # saving and restoring the per-cop analysis state, and resolving the
7
+ # `+`/`-` argument lists (shared with the `next` directive).
8
+ # @api private
9
+ module PushPop
10
+ private
11
+
12
+ def resolve_push_cops(directive)
13
+ directive.push_args.transform_values do |names|
14
+ names.flat_map { |name| expand_cop_name(name) }
15
+ end
16
+ end
17
+
18
+ def expand_cop_name(name)
19
+ registry = Cop::Registry.global
20
+ cops = registry.department?(name) ? registry.names_for_department(name) : [name]
21
+ cops.map { |c| qualified_cop_name(c) }
22
+ end
23
+
24
+ def apply_push(analyses, resolved_cops, directive)
25
+ resolved_cops.each do |op, cops|
26
+ cops.each { |cop| apply_cop_op(analyses, op, cop, directive) }
27
+ end
28
+ end
29
+
30
+ def apply_cop_op(analyses, operation, cop, directive)
31
+ analysis = analyses[cop]
32
+ line = directive.line_number
33
+ if operation == '-' && !analysis.start_line_number
34
+ analyses[cop] = CopAnalysis.new(analysis.line_ranges, line, directive)
35
+ elsif operation == '+' && analysis.start_line_number
36
+ analyses[cop] = CopAnalysis.new(analysis.close(line), nil)
37
+ end
38
+ end
39
+
40
+ def pop_state(analyses, line)
41
+ restore_point = @stack.pop
42
+ (restore_point.keys | analyses.keys).each do |cop|
43
+ analyses[cop] = popped_analysis(analyses[cop], restore_point[cop], line)
44
+ end
45
+ end
46
+
47
+ def popped_analysis(current, restored, line)
48
+ ranges = current.close(line - 1)
49
+ return CopAnalysis.new(ranges, nil) unless restored&.start_line_number
50
+
51
+ CopAnalysis.new(ranges, line, restored.start_directive)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -6,6 +6,7 @@ module RuboCop
6
6
  class CommentConfig
7
7
  extend SimpleForwardable
8
8
  include DisableNext
9
+ include PushPop
9
10
 
10
11
  CONFIG_DISABLED_LINE_RANGE_MIN = -Float::INFINITY
11
12
 
@@ -75,8 +76,9 @@ module RuboCop
75
76
  non_comment_token_line_numbers.none?(line_number)
76
77
  end
77
78
 
78
- # The names of the cops that are opted in by an `enable` comment directive,
79
- # used to mobilize cops disabled in the config on demand.
79
+ # The names of the cops that are opted in by an `enable` comment directive
80
+ # or a `+` argument of a `push`/`next` directive, used to mobilize cops
81
+ # disabled in the config on demand.
80
82
  #
81
83
  # @api private
82
84
  # @return [Set<String>]
@@ -84,10 +86,11 @@ module RuboCop
84
86
  @opt_in_cops ||= begin
85
87
  cops = Set.new
86
88
  each_directive do |directive|
87
- next unless directive.enabled?
88
- next if directive.all_cops?
89
-
90
- cops.merge(directive.raw_cop_names)
89
+ if directive.enabled?
90
+ cops.merge(directive.raw_cop_names) unless directive.all_cops?
91
+ elsif directive.push? || directive.next?
92
+ cops.merge(directive.signed_args.fetch('+', []))
93
+ end
91
94
  end
92
95
  cops
93
96
  end
@@ -98,9 +101,7 @@ module RuboCop
98
101
  def extra_enabled_comments_with_names(extras:, names:)
99
102
  each_directive do |directive|
100
103
  next unless comment_only_line?(directive.line_number)
101
- # Push/pop and next-statement directives close themselves, so they
102
- # play no part in the disable/enable pairing.
103
- next if directive.push? || directive.pop? || directive.disable_next?
104
+ next if self_closing_directive?(directive)
104
105
 
105
106
  if directive.enabled_all?
106
107
  handle_enable_all(directive, names, extras)
@@ -128,6 +129,10 @@ module RuboCop
128
129
  pop_state(analyses, directive.line_number) if @stack.any?
129
130
  elsif directive.disable_next?
130
131
  apply_disable_next(analyses, directive)
132
+ elsif directive.next?
133
+ apply_next_directive(analyses, resolve_push_cops(directive), directive)
134
+ elsif directive.enable_next?
135
+ apply_enable_next(analyses, directive)
131
136
  else
132
137
  directive.cop_names.each do |cop_name|
133
138
  cop_name = qualified_cop_name(cop_name)
@@ -144,48 +149,6 @@ module RuboCop
144
149
  end
145
150
  end
146
151
 
147
- def resolve_push_cops(directive)
148
- directive.push_args.transform_values do |names|
149
- names.flat_map { |name| expand_cop_name(name) }
150
- end
151
- end
152
-
153
- def expand_cop_name(name)
154
- registry = Cop::Registry.global
155
- cops = registry.department?(name) ? registry.names_for_department(name) : [name]
156
- cops.map { |c| qualified_cop_name(c) }
157
- end
158
-
159
- def apply_push(analyses, resolved_cops, directive)
160
- resolved_cops.each do |op, cops|
161
- cops.each { |cop| apply_cop_op(analyses, op, cop, directive) }
162
- end
163
- end
164
-
165
- def apply_cop_op(analyses, operation, cop, directive)
166
- analysis = analyses[cop]
167
- line = directive.line_number
168
- if operation == '-' && !analysis.start_line_number
169
- analyses[cop] = CopAnalysis.new(analysis.line_ranges, line, directive)
170
- elsif operation == '+' && analysis.start_line_number
171
- analyses[cop] = CopAnalysis.new(analysis.close(line), nil)
172
- end
173
- end
174
-
175
- def pop_state(analyses, line)
176
- restore_point = @stack.pop
177
- (restore_point.keys | analyses.keys).each do |cop|
178
- analyses[cop] = popped_analysis(analyses[cop], restore_point[cop], line)
179
- end
180
- end
181
-
182
- def popped_analysis(current, restored, line)
183
- ranges = current.close(line - 1)
184
- return CopAnalysis.new(ranges, nil) unless restored&.start_line_number
185
-
186
- CopAnalysis.new(ranges, line, restored.start_directive)
187
- end
188
-
189
152
  def inject_disabled_cops_directives(analyses)
190
153
  registry.disabled_names(config).each do |cop_name|
191
154
  analyses[cop_name] = analyze_cop(
@@ -236,6 +199,13 @@ module RuboCop
236
199
  end
237
200
  end
238
201
 
202
+ # Push/pop and next-statement directives close themselves, so they
203
+ # play no part in the disable/enable pairing.
204
+ def self_closing_directive?(directive)
205
+ directive.push? || directive.pop? || directive.disable_next? ||
206
+ directive.next? || directive.enable_next?
207
+ end
208
+
239
209
  def qualified_cop_name(cop_name)
240
210
  # A directive naming a cop under the wrong department is not honored -
241
211
  # the mistake is surfaced by `Lint/RedundantCopDisableDirective` (with a
@@ -226,6 +226,16 @@ module RuboCop
226
226
  end
227
227
  end
228
228
 
229
+ # Whether preview behavior is on, for cops that gate an unstable change
230
+ # behind it and for cops that are themselves `Enabled: preview`.
231
+ # `--preview` / `--no-preview` win over `AllCops: Preview`.
232
+ def preview?(options = {})
233
+ preview = options[:preview]
234
+ return preview unless preview.nil?
235
+
236
+ for_all_cops['Preview'] == true
237
+ end
238
+
229
239
  def active_support_extensions_enabled?
230
240
  for_all_cops['ActiveSupportExtensionsEnabled']
231
241
  end
@@ -24,7 +24,8 @@ module RuboCop
24
24
  include FileFinder
25
25
 
26
26
  attr_accessor :debug, :ignore_parent_exclusion, :disable_pending_cops, :enable_pending_cops,
27
- :enabled_by_default, :disabled_by_default, :ignore_unrecognized_cops
27
+ :enabled_by_default, :disabled_by_default, :ignore_unrecognized_cops,
28
+ :preview
28
29
  attr_writer :default_configuration, :cache_root
29
30
  attr_reader :loaded_plugins, :loaded_features
30
31
 
@@ -41,6 +42,7 @@ module RuboCop
41
42
  @loaded_features = Set.new
42
43
  @disable_pending_cops = nil
43
44
  @enable_pending_cops = nil
45
+ @preview = nil
44
46
  @enabled_by_default = nil
45
47
  @disabled_by_default = nil
46
48
  @ignore_parent_exclusion = nil
@@ -195,12 +197,18 @@ module RuboCop
195
197
  # `AllCops/DisabledByDefault` to the given configuration. Used when the
196
198
  # configuration would otherwise be returned without going through
197
199
  # `merge_with_default` (e.g. there is no user-supplied `.rubocop.yml`).
200
+ # Used when there is no configuration file, so the defaults are the whole
201
+ # configuration and still need the same treatment `merge_with_default`
202
+ # would have given them.
198
203
  def apply_default_overrides(config)
199
- return config if @enabled_by_default.nil? && @disabled_by_default.nil?
204
+ hash = resolver.apply_preview_defaults(config, preview == true)
200
205
 
201
- hash = config.transform_values do |params|
202
- params.is_a?(Hash) ? params.merge('Enabled' => !@disabled_by_default) : params
206
+ unless @enabled_by_default.nil? && @disabled_by_default.nil?
207
+ hash = hash.transform_values do |params|
208
+ params.is_a?(Hash) ? params.merge('Enabled' => !@disabled_by_default) : params
209
+ end
203
210
  end
211
+
204
212
  Config.new(hash, config.loaded_path)
205
213
  end
206
214
 
@@ -95,19 +95,22 @@ module RuboCop
95
95
  # When the `--disable-all-cops` or `--enable-all-cops` CLI option is given,
96
96
  # it takes precedence over the configuration values.
97
97
  def merge_with_default(config, config_file, unset_nil:)
98
- default_configuration = ConfigLoader.default_configuration
98
+ base_defaults = apply_preview_defaults(ConfigLoader.default_configuration, preview?(config))
99
+ default_configuration = base_defaults
99
100
  disabled_by_default, enabled_by_default = resolve_default_overrides(config)
100
101
 
101
102
  if disabled_by_default || enabled_by_default
102
- default_configuration = transform(default_configuration) do |params|
103
+ default_configuration = transform(base_defaults) do |params|
103
104
  params.merge('Enabled' => !disabled_by_default)
104
105
  end
105
106
  end
106
107
 
107
- config = handle_disabled_by_default(config, default_configuration) if disabled_by_default
108
+ if disabled_by_default
109
+ config = handle_disabled_by_default(config, default_configuration, base_defaults)
110
+ end
108
111
  override_enabled_for_disabled_departments(default_configuration, config)
109
112
 
110
- opts = { inherit_mode: config['inherit_mode'] || {}, unset_nil: unset_nil }
113
+ opts = { inherit_mode: inherit_mode_for_default(config), unset_nil: unset_nil }
111
114
  Config.new(merge(default_configuration, config, **opts), config_file)
112
115
  end
113
116
 
@@ -166,8 +169,47 @@ module RuboCop
166
169
  end
167
170
  end
168
171
 
172
+ # A cop's entry in the default configuration may carry a `Preview` section
173
+ # with the defaults it is expected to adopt in the next major release. Under
174
+ # `Preview` those replace the current defaults. The section is dropped either
175
+ # way, so the resolved configuration only ever shows what is in effect.
176
+ def apply_preview_defaults(default_configuration, preview)
177
+ transform(default_configuration) do |params|
178
+ next params unless params['Preview'].is_a?(Hash)
179
+
180
+ params = params.dup
181
+ preview_params = params.delete('Preview')
182
+ preview ? params.merge(preview_params) : params
183
+ end
184
+ end
185
+
169
186
  private
170
187
 
188
+ def inherit_mode_for_default(config)
189
+ with_preview_exclude_merge(config['inherit_mode'] || {}, config)
190
+ end
191
+
192
+ # Under `Preview`, `Exclude` is merged rather than replaced, so that excluding
193
+ # one directory does not silently drop the excludes it would have inherited -
194
+ # from the default configuration or from a file named in `inherit_from`. An
195
+ # explicit `inherit_mode` still wins, in either direction.
196
+ def with_preview_exclude_merge(mode, config)
197
+ return mode unless preview?(config)
198
+ return mode if Array(mode['override']).include?('Exclude')
199
+ return mode if Array(mode['merge']).include?('Exclude')
200
+
201
+ mode.merge('merge' => Array(mode['merge']) + ['Exclude'])
202
+ end
203
+
204
+ # `config` has already been through `handle_disabled_by_default` by this
205
+ # point, which returns a plain hash, so read `AllCops` directly rather than
206
+ # going through `Config#for_all_cops`.
207
+ def preview?(config)
208
+ return ConfigLoader.preview unless ConfigLoader.preview.nil?
209
+
210
+ (config['AllCops'] || {})['Preview'] == true
211
+ end
212
+
171
213
  def resolve_default_overrides(config)
172
214
  if ConfigLoader.disabled_by_default || ConfigLoader.enabled_by_default
173
215
  [ConfigLoader.disabled_by_default, ConfigLoader.enabled_by_default]
@@ -210,7 +252,7 @@ module RuboCop
210
252
  def determine_inherit_mode(hash, key)
211
253
  cop_cfg = hash[key]
212
254
  local_inherit = cop_cfg['inherit_mode'] if cop_cfg.is_a?(Hash)
213
- local_inherit || hash['inherit_mode'] || {}
255
+ with_preview_exclude_merge(local_inherit || hash['inherit_mode'] || {}, hash)
214
256
  end
215
257
 
216
258
  def should_union?(derived_hash, base_hash, root_mode, key)
@@ -276,7 +318,7 @@ module RuboCop
276
318
  file.is_a?(RemoteConfig)
277
319
  end
278
320
 
279
- def handle_disabled_by_default(config, new_default_configuration)
321
+ def handle_disabled_by_default(config, new_default_configuration, base_defaults)
280
322
  department_config = config.to_hash.reject { |cop| cop.include?('/') }
281
323
  department_config.each do |dept, dept_params|
282
324
  next unless dept_params['Enabled']
@@ -285,7 +327,7 @@ module RuboCop
285
327
  next unless cop.start_with?("#{dept}/")
286
328
 
287
329
  # Retain original default configuration for cops in the department.
288
- params['Enabled'] = ConfigLoader.default_configuration[cop]['Enabled']
330
+ params['Enabled'] = base_defaults[cop]['Enabled']
289
331
  end
290
332
  end
291
333
 
@@ -20,7 +20,8 @@ module RuboCop
20
20
  rule_message + "\n(obsolete configuration found in #{smart_loaded_path}, please update it)"
21
21
  end
22
22
 
23
- # Cop rules currently can only be failures, not warnings
23
+ # Failure by default; rules that accept `severity: warning` say so
24
+ # themselves.
24
25
  def warning?
25
26
  false
26
27
  end
@@ -27,8 +27,16 @@ module RuboCop
27
27
  end
28
28
  end
29
29
 
30
+ def warning?
31
+ severity == 'warning'
32
+ end
33
+
30
34
  private
31
35
 
36
+ def severity
37
+ metadata['severity']
38
+ end
39
+
32
40
  def reason
33
41
  metadata['reason']
34
42
  end
@@ -15,7 +15,7 @@ module RuboCop
15
15
  'changed_parameters' => ChangedParameter,
16
16
  'changed_enforced_styles' => ChangedEnforcedStyles
17
17
  }.freeze
18
- LOAD_RULES_CACHE = {} # rubocop:disable Style/MutableConstant
18
+ LOAD_RULES_CACHE = {} # rubocop:disable Style/MutableConstant -- a cache, written to at runtime
19
19
  private_constant :LOAD_RULES_CACHE
20
20
 
21
21
  attr_reader :rules, :warnings