shirobai-rspec 2026.0709.0000

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 (30) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/lib/shirobai/cop/rspec/described_class.rb +213 -0
  4. data/lib/shirobai/cop/rspec/duplicated_metadata.rb +70 -0
  5. data/lib/shirobai/cop/rspec/empty_example_group.rb +156 -0
  6. data/lib/shirobai/cop/rspec/empty_line_after_example.rb +50 -0
  7. data/lib/shirobai/cop/rspec/empty_line_after_example_group.rb +46 -0
  8. data/lib/shirobai/cop/rspec/empty_line_after_final_let.rb +47 -0
  9. data/lib/shirobai/cop/rspec/empty_line_after_hook.rb +48 -0
  10. data/lib/shirobai/cop/rspec/empty_line_after_subject.rb +46 -0
  11. data/lib/shirobai/cop/rspec/empty_line_separation_support.rb +61 -0
  12. data/lib/shirobai/cop/rspec/empty_metadata.rb +58 -0
  13. data/lib/shirobai/cop/rspec/focus.rb +110 -0
  14. data/lib/shirobai/cop/rspec/let_setup.rb +63 -0
  15. data/lib/shirobai/cop/rspec/metadata_style.rb +200 -0
  16. data/lib/shirobai/cop/rspec/metadata_support.rb +109 -0
  17. data/lib/shirobai/cop/rspec/multiple_memoized_helpers.rb +96 -0
  18. data/lib/shirobai/cop/rspec/named_subject.rb +71 -0
  19. data/lib/shirobai/cop/rspec/pending_without_reason.rb +129 -0
  20. data/lib/shirobai/cop/rspec/repeated_description.rb +129 -0
  21. data/lib/shirobai/cop/rspec/repeated_example.rb +129 -0
  22. data/lib/shirobai/cop/rspec/scattered_setup.rb +133 -0
  23. data/lib/shirobai/cop/rspec/send_candidate_support.rb +48 -0
  24. data/lib/shirobai/cop/rspec/sort_metadata.rb +88 -0
  25. data/lib/shirobai/cop/rspec/variable_definition.rb +81 -0
  26. data/lib/shirobai/cop/rspec/variable_name.rb +92 -0
  27. data/lib/shirobai/rspec/node_locator.rb +126 -0
  28. data/lib/shirobai/rspec/version.rb +9 -0
  29. data/lib/shirobai-rspec.rb +175 -0
  30. metadata +93 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7a73330fe18d5f58a09e051463f6072c5dd4fefc91600459640adc00d3cc5ce2
4
+ data.tar.gz: dada93df314d81800637e9d14a93940d9cd67097d5671fe636d261c6515b9b4f
5
+ SHA512:
6
+ metadata.gz: 1b9225390133b623be2a87797d411001de0a7cb415245ca0e1c5055b5d4b70aa9f1bd658d20030ce07698423a116a8b9014e5fdebf5131c61afb6bba0bbb74d5
7
+ data.tar.gz: 54689cbadf3ac1cda17865f99c481a2e9223ca3c926fd7141f6821a7bf7c7b7f828c88736837ea8aa7e7deabde4c55134de87bff5a05c582e7e7454836cfbbea
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fusagiko / takayamaki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module RSpec
6
+ # Drop-in Rust reimplementation of `RSpec/DescribedClass`
7
+ # (rubocop-rspec 3.10.2).
8
+ #
9
+ # Rust identifies candidate `describe(Const)` blocks on the shared walk.
10
+ # This wrapper locates the parser block node and runs stock's full
11
+ # detection + autocorrect VERBATIM: `find_usage` recursion with
12
+ # scope-change guards, `collapse_namespace`, `described_constant`,
13
+ # and all config axes (EnforcedStyle, SkipBlocks, OnlyStaticConstants).
14
+ #
15
+ # Probed quirks live as differential specs in
16
+ # described_class_edge_cases_spec.rb.
17
+ class DescribedClass < RuboCop::Cop::Base
18
+ extend RuboCop::Cop::AutoCorrector
19
+ include RuboCop::Cop::ConfigurableEnforcedStyle
20
+ include RuboCop::Cop::RSpec::Namespace
21
+ include RuboCop::RSpec::Language
22
+ include Shirobai::Cop::BundleEligible
23
+
24
+ DESCRIBED_CLASS = 'described_class'
25
+ MSG = 'Use `%<replacement>s` instead of `%<src>s`.'
26
+
27
+ def self.cop_name = "RSpec/DescribedClass"
28
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
29
+
30
+ # No bundle_args needed -- the Rust side does not pack config for
31
+ # this cop (all config axes are handled in the Ruby wrapper).
32
+ def self.bundle_args(_config)
33
+ []
34
+ end
35
+
36
+ # @!method common_instance_exec_closure?(node)
37
+ def_node_matcher :common_instance_exec_closure?, <<~PATTERN
38
+ (block
39
+ {
40
+ (send (const nil? {:Class :Module :Struct}) :new ...)
41
+ (send (const nil? :Data) :define ...)
42
+ (send _ {:class_eval :module_eval :instance_eval} ...)
43
+ (send _ {:class_exec :module_exec :instance_exec} ...)
44
+ }
45
+ ...
46
+ )
47
+ PATTERN
48
+
49
+ # @!method rspec_block?(node)
50
+ def_node_matcher :rspec_block?,
51
+ '(any_block (send #rspec? #ALL.all ...) ...)'
52
+
53
+ # @!method scope_changing_syntax?(node)
54
+ def_node_matcher :scope_changing_syntax?, '{def class module}'
55
+
56
+ # @!method described_constant(node)
57
+ def_node_matcher :described_constant, <<~PATTERN
58
+ (block (send _ :describe $(const ...) ...) (args) $_)
59
+ PATTERN
60
+
61
+ # @!method contains_described_class?(node)
62
+ def_node_search :contains_described_class?,
63
+ '(send nil? :described_class)'
64
+
65
+ def on_new_investigation
66
+ RuboCop::RSpec::Language.config = config["RSpec"]["Language"]
67
+ ranges = resolved_candidates
68
+ return if ranges.nil? || ranges.empty?
69
+
70
+ source = bundle_eligible? ? processed_source.raw_source : processed_source.buffer.source
71
+ off = SourceOffsets.for(source)
72
+ keys = ranges.map { |(start, fin)| [off[start], off[fin]] }
73
+ located = Shirobai::RSpec::NodeLocator.locate(processed_source, keys)
74
+
75
+ keys.each do |key|
76
+ node = located[key]
77
+ process_candidate(node) if node&.block_type?
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ # Stock's on_block logic, copied verbatim.
84
+ def process_candidate(node)
85
+ @described_class, body = described_constant(node)
86
+ return unless body
87
+
88
+ find_usage(body) do |match|
89
+ msg = message(match.const_name)
90
+ add_offense(match, message: msg) do |corrector|
91
+ autocorrect(corrector, match)
92
+ end
93
+ end
94
+ end
95
+
96
+ def autocorrect(corrector, match)
97
+ replacement = if style == :described_class
98
+ DESCRIBED_CLASS
99
+ else
100
+ @described_class.const_name
101
+ end
102
+
103
+ corrector.replace(match, replacement)
104
+ end
105
+
106
+ def find_usage(node, &block)
107
+ yield(node) if offensive?(node)
108
+ return if scope_change?(node) || allowed?(node)
109
+
110
+ node.each_child_node do |child|
111
+ find_usage(child, &block)
112
+ end
113
+ end
114
+
115
+ def allowed?(node)
116
+ node.const_type? && only_static_constants?
117
+ end
118
+
119
+ def message(offense)
120
+ if style == :described_class
121
+ format(MSG, replacement: DESCRIBED_CLASS, src: offense)
122
+ else
123
+ format(MSG, replacement: @described_class.const_name,
124
+ src: DESCRIBED_CLASS)
125
+ end
126
+ end
127
+
128
+ def scope_change?(node)
129
+ scope_changing_syntax?(node) ||
130
+ common_instance_exec_closure?(node) ||
131
+ skippable_block?(node)
132
+ end
133
+
134
+ def skippable_block?(node)
135
+ return false unless cop_config['SkipBlocks']
136
+
137
+ node.any_block_type? && !rspec_block?(node)
138
+ end
139
+
140
+ def only_static_constants?
141
+ cop_config.fetch('OnlyStaticConstants', true)
142
+ end
143
+
144
+ def offensive?(node)
145
+ if style == :described_class
146
+ offensive_described_class?(node)
147
+ else
148
+ node.send_type? && node.method?(:described_class)
149
+ end
150
+ end
151
+
152
+ def offensive_described_class?(node)
153
+ return false unless node.const_type?
154
+
155
+ # E.g. `described_class::CONSTANT`
156
+ return false if contains_described_class?(node)
157
+
158
+ nearest_described_class, = node.each_ancestor(:block)
159
+ .map { |ancestor| described_constant(ancestor) }.find(&:itself)
160
+
161
+ return false if nearest_described_class.equal?(node)
162
+
163
+ full_const_name(nearest_described_class) == full_const_name(node)
164
+ end
165
+
166
+ def full_const_name(node)
167
+ symbolized_namespace = namespace(node).map(&:to_sym)
168
+ collapse_namespace(symbolized_namespace, const_name(node))
169
+ end
170
+
171
+ # @param namespace [Array<Symbol>]
172
+ # @param const [Array<Symbol>]
173
+ # @return [Array<Symbol>]
174
+ def collapse_namespace(namespace, const)
175
+ return const if namespace.empty? || const.first.nil?
176
+
177
+ start = [0, (namespace.length - const.length)].max
178
+ max = namespace.length
179
+ intersection = (start..max).find do |shift|
180
+ namespace[shift, max - shift] == const[0, max - shift]
181
+ end
182
+ [*namespace[0, intersection], *const]
183
+ end
184
+
185
+ # @param node [RuboCop::AST::Node]
186
+ # @return [Array<Symbol>]
187
+ def const_name(node)
188
+ namespace = node.namespace
189
+ name = node.short_name
190
+ if !namespace
191
+ [name]
192
+ elsif namespace.const_type?
193
+ [*const_name(namespace), name]
194
+ elsif %i[lvar cbase send].include?(namespace.type)
195
+ [nil, name]
196
+ end
197
+ end
198
+
199
+ # --- Resolution ---
200
+
201
+ def resolved_candidates
202
+ if bundle_eligible?
203
+ ranges = Dispatch.offenses_for(processed_source, config, :rspec_described_class)
204
+ return ranges unless ranges.nil?
205
+ end
206
+ Shirobai.check_rspec_described_class(
207
+ processed_source.buffer.source, *Shirobai::RSpec.segment(config)
208
+ )
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module RSpec
6
+ # Drop-in Rust reimplementation of `RSpec/DuplicatedMetadata`
7
+ # (rubocop-rspec 3.10.2).
8
+ #
9
+ # Rust supplies the shared metadata-anchor block ranges; this wrapper
10
+ # relocates each parser block node and runs stock's `on_metadata` plus
11
+ # autocorrect VERBATIM, so structural `eql?` duplicate detection and the
12
+ # comma/space-aware removal match byte for byte.
13
+ class DuplicatedMetadata < RuboCop::Cop::Base
14
+ extend RuboCop::Cop::AutoCorrector
15
+
16
+ include RuboCop::RSpec::Language
17
+ include Shirobai::Cop::RSpec::MetadataSupport
18
+ include RuboCop::Cop::RangeHelp
19
+
20
+ MSG = "Avoid duplicated metadata."
21
+
22
+ def self.cop_name = "RSpec/DuplicatedMetadata"
23
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
24
+
25
+ # Config-less; the shared anchor list needs no per-cop settings.
26
+ def self.bundle_args(_config)
27
+ []
28
+ end
29
+
30
+ def metadata_slot = :rspec_duplicated_metadata
31
+
32
+ # --- stock's private methods, copied verbatim (rubocop-rspec 3.10.2) ---
33
+
34
+ def on_metadata(symbols, _hash)
35
+ symbols.each do |symbol|
36
+ on_metadata_symbol(symbol)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def on_metadata_symbol(node)
43
+ return unless duplicated?(node)
44
+
45
+ add_offense(node) do |corrector|
46
+ autocorrect(corrector, node)
47
+ end
48
+ end
49
+
50
+ def autocorrect(corrector, node)
51
+ corrector.remove(
52
+ range_with_surrounding_comma(
53
+ range_with_surrounding_space(
54
+ node.source_range,
55
+ side: :left
56
+ ),
57
+ :left
58
+ )
59
+ )
60
+ end
61
+
62
+ def duplicated?(node)
63
+ node.left_siblings.any? do |sibling|
64
+ sibling.eql?(node)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module RSpec
6
+ # Drop-in Rust reimplementation of `RSpec/EmptyExampleGroup`
7
+ # (rubocop-rspec 3.10.2).
8
+ #
9
+ # Architecture B (relocate-and-dispatch): Rust identifies candidate
10
+ # example-group blocks on the shared walk; this wrapper locates the
11
+ # parser-gem block node via `NodeLocator` and runs stock's detection
12
+ # logic VERBATIM. The mutually recursive `examples?` matcher is deep
13
+ # parser-AST structural matching that cannot be reproduced bytewise.
14
+ # The autocorrect is simple (remove whole lines via `range_by_whole_lines`).
15
+ #
16
+ # Stock entry method `on_block` is renamed to `process_candidate` so
17
+ # the Commissioner never dispatches a per-node `on_block`; only
18
+ # `on_new_investigation` is a real callback.
19
+ class EmptyExampleGroup < RuboCop::Cop::Base
20
+ extend RuboCop::Cop::AutoCorrector
21
+
22
+ include RuboCop::RSpec::Language
23
+ include RuboCop::Cop::RSpec::InsideExample
24
+ include RuboCop::Cop::RangeHelp
25
+ include Shirobai::Cop::BundleEligible
26
+
27
+ MSG = "Empty example group detected."
28
+
29
+ def self.cop_name = "RSpec/EmptyExampleGroup"
30
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
31
+
32
+ # Config-less (the segment's role lists cover everything).
33
+ def self.bundle_args(_config)
34
+ []
35
+ end
36
+
37
+ # --- stock node-pattern matchers, copied verbatim (rubocop-rspec 3.10.2) ---
38
+
39
+ # @!method example_group_body(node)
40
+ def_node_matcher :example_group_body, <<~PATTERN
41
+ (block (send #rspec? #ExampleGroups.all ...) args $_)
42
+ PATTERN
43
+
44
+ # @!method example_or_group_or_include?(node)
45
+ def_node_matcher :example_or_group_or_include?, <<~PATTERN
46
+ {
47
+ (block
48
+ (send #rspec? {#Examples.all #ExampleGroups.all #Includes.all} ...)
49
+ ...)
50
+ (send nil? {#Examples.all #Includes.all} ...)
51
+ }
52
+ PATTERN
53
+
54
+ # @!method examples_inside_block?(node)
55
+ def_node_matcher :examples_inside_block?, <<~PATTERN
56
+ (block !(send nil? #Hooks.all ...) _ #examples?)
57
+ PATTERN
58
+
59
+ # @!method examples_directly_or_in_block?(node)
60
+ def_node_matcher :examples_directly_or_in_block?, <<~PATTERN
61
+ {
62
+ #example_or_group_or_include?
63
+ #examples_inside_block?
64
+ }
65
+ PATTERN
66
+
67
+ # @!method examples?(node)
68
+ def_node_matcher :examples?, <<~PATTERN
69
+ {
70
+ #examples_directly_or_in_block?
71
+ #examples_in_branches?
72
+ (begin <#examples_directly_or_in_block? ...>)
73
+ (begin <#examples_in_branches? ...>)
74
+ }
75
+ PATTERN
76
+
77
+ def on_new_investigation
78
+ RuboCop::RSpec::Language.config = config["RSpec"]["Language"]
79
+ ranges = resolved_candidates
80
+ return if ranges.nil? || ranges.empty?
81
+
82
+ source = bundle_eligible? ? processed_source.raw_source : processed_source.buffer.source
83
+ off = SourceOffsets.for(source)
84
+ keys = ranges.map { |(start, fin)| [off[start], off[fin]] }
85
+ located = Shirobai::RSpec::NodeLocator.locate(processed_source, keys)
86
+
87
+ keys.each do |key|
88
+ node = located[key]
89
+ process_candidate(node) if node&.block_type?
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def resolved_candidates
96
+ if bundle_eligible?
97
+ candidates = Dispatch.offenses_for(processed_source, config, :rspec_empty_example_group)
98
+ return candidates unless candidates.nil?
99
+ end
100
+ Shirobai.check_rspec_empty_example_group(
101
+ processed_source.buffer.source, *Shirobai::RSpec.segment(config)
102
+ )
103
+ end
104
+
105
+ # Stock `on_block`, renamed so the Commissioner never dispatches it.
106
+ def process_candidate(node)
107
+ return if node.each_ancestor(:any_def).any?
108
+ return if inside_example?(node)
109
+
110
+ example_group_body(node) do |body|
111
+ next unless offensive?(body)
112
+
113
+ add_offense(node.send_node) do |corrector|
114
+ corrector.remove(removed_range(node))
115
+ end
116
+ end
117
+ end
118
+
119
+ # --- stock private methods, copied verbatim (rubocop-rspec 3.10.2) ---
120
+
121
+ def offensive?(body)
122
+ return true unless body
123
+ return false if conditionals_with_examples?(body)
124
+
125
+ if body.type?(:if, :case)
126
+ !examples_in_branches?(body)
127
+ else
128
+ !examples?(body)
129
+ end
130
+ end
131
+
132
+ def conditionals_with_examples?(body)
133
+ return false unless body.type?(:begin, :case)
134
+
135
+ body.each_descendant(:if, :case).any? do |condition_node|
136
+ examples_in_branches?(condition_node)
137
+ end
138
+ end
139
+
140
+ def examples_in_branches?(condition_node)
141
+ return false unless condition_node
142
+ return false unless condition_node.type?(:if, :case)
143
+
144
+ condition_node.branches.any? { |branch| examples?(branch) }
145
+ end
146
+
147
+ def removed_range(node)
148
+ range_by_whole_lines(
149
+ node.source_range,
150
+ include_final_newline: true
151
+ )
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module RSpec
6
+ # Drop-in Rust reimplementation of `RSpec/EmptyLineAfterExample`
7
+ # (rubocop-rspec 3.10.2).
8
+ #
9
+ # The Rust rule flags every plain-block example (`it`/`specify`/`its`/...)
10
+ # that has a following sibling in a `:begin` sequence, minus the allowed
11
+ # consecutive-one-liner shape (`AllowConsecutiveOneLiners`, default true).
12
+ # The shared support module replays stock's comment/blank/directive walk
13
+ # and autocorrect. Probed quirks live in
14
+ # empty_line_after_example_edge_cases_spec.rb.
15
+ class EmptyLineAfterExample < RuboCop::Cop::Base
16
+ extend RuboCop::Cop::AutoCorrector
17
+ include Shirobai::Cop::BundleEligible
18
+ include Shirobai::Cop::RSpec::EmptyLineSeparationSupport
19
+
20
+ MSG = "Add an empty line after `%<example>s`."
21
+
22
+ def self.cop_name = "RSpec/EmptyLineAfterExample"
23
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
24
+
25
+ # `AllowConsecutiveOneLiners` (default true) -> segment num.
26
+ def self.bundle_args(config)
27
+ [config.for_badge(badge).fetch("AllowConsecutiveOneLiners", true) ? 1 : 0]
28
+ end
29
+
30
+ def on_new_investigation
31
+ emit_empty_line_offenses(resolved_offenses) do |method|
32
+ format(MSG, example: method)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def resolved_offenses
39
+ if bundle_eligible?
40
+ offenses = Dispatch.offenses_for(processed_source, config, :rspec_empty_line_after_example)
41
+ return offenses unless offenses.nil?
42
+ end
43
+ Shirobai.check_rspec_empty_line_after_example(
44
+ processed_source.buffer.source, *Shirobai::RSpec.segment(config)
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module RSpec
6
+ # Drop-in Rust reimplementation of `RSpec/EmptyLineAfterExampleGroup`
7
+ # (rubocop-rspec 3.10.2).
8
+ #
9
+ # Flags every plain-block spec group (`describe`/`context`/... AND
10
+ # `shared_examples`/`shared_context` with an RSpec receiver) that has a
11
+ # following sibling in a `:begin` sequence. Config-less.
12
+ class EmptyLineAfterExampleGroup < RuboCop::Cop::Base
13
+ extend RuboCop::Cop::AutoCorrector
14
+ include Shirobai::Cop::BundleEligible
15
+ include Shirobai::Cop::RSpec::EmptyLineSeparationSupport
16
+
17
+ MSG = "Add an empty line after `%<example_group>s`."
18
+
19
+ def self.cop_name = "RSpec/EmptyLineAfterExampleGroup"
20
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
21
+
22
+ def self.bundle_args(_config)
23
+ []
24
+ end
25
+
26
+ def on_new_investigation
27
+ emit_empty_line_offenses(resolved_offenses) do |method|
28
+ format(MSG, example_group: method)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def resolved_offenses
35
+ if bundle_eligible?
36
+ offenses = Dispatch.offenses_for(processed_source, config, :rspec_empty_line_after_example_group)
37
+ return offenses unless offenses.nil?
38
+ end
39
+ Shirobai.check_rspec_empty_line_after_example_group(
40
+ processed_source.buffer.source, *Shirobai::RSpec.segment(config)
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module RSpec
6
+ # Drop-in Rust reimplementation of `RSpec/EmptyLineAfterFinalLet`
7
+ # (rubocop-rspec 3.10.2).
8
+ #
9
+ # For every plain-block example/shared group or `include_*` block, the
10
+ # Rust rule finds the last `let?` among its direct body children (block
11
+ # form or the `let(:x, &blk)` send form) and flags it when it is not the
12
+ # body's last child. Config-less.
13
+ class EmptyLineAfterFinalLet < RuboCop::Cop::Base
14
+ extend RuboCop::Cop::AutoCorrector
15
+ include Shirobai::Cop::BundleEligible
16
+ include Shirobai::Cop::RSpec::EmptyLineSeparationSupport
17
+
18
+ MSG = "Add an empty line after the last `%<let>s`."
19
+
20
+ def self.cop_name = "RSpec/EmptyLineAfterFinalLet"
21
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
22
+
23
+ def self.bundle_args(_config)
24
+ []
25
+ end
26
+
27
+ def on_new_investigation
28
+ emit_empty_line_offenses(resolved_offenses) do |method|
29
+ format(MSG, let: method)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def resolved_offenses
36
+ if bundle_eligible?
37
+ offenses = Dispatch.offenses_for(processed_source, config, :rspec_empty_line_after_final_let)
38
+ return offenses unless offenses.nil?
39
+ end
40
+ Shirobai.check_rspec_empty_line_after_final_let(
41
+ processed_source.buffer.source, *Shirobai::RSpec.segment(config)
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module RSpec
6
+ # Drop-in Rust reimplementation of `RSpec/EmptyLineAfterHook`
7
+ # (rubocop-rspec 3.10.2).
8
+ #
9
+ # Flags every hook (`before`/`after`/`around`/... in ANY block form —
10
+ # plain, numbered, `it`-param) that has a following sibling in a `:begin`
11
+ # sequence, minus the allowed consecutive-one-liner hook chain
12
+ # (`AllowConsecutiveOneLiners`, default true).
13
+ class EmptyLineAfterHook < RuboCop::Cop::Base
14
+ extend RuboCop::Cop::AutoCorrector
15
+ include Shirobai::Cop::BundleEligible
16
+ include Shirobai::Cop::RSpec::EmptyLineSeparationSupport
17
+
18
+ MSG = "Add an empty line after `%<hook>s`."
19
+
20
+ def self.cop_name = "RSpec/EmptyLineAfterHook"
21
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
22
+
23
+ # `AllowConsecutiveOneLiners` (default true) -> segment num.
24
+ def self.bundle_args(config)
25
+ [config.for_badge(badge).fetch("AllowConsecutiveOneLiners", true) ? 1 : 0]
26
+ end
27
+
28
+ def on_new_investigation
29
+ emit_empty_line_offenses(resolved_offenses) do |method|
30
+ format(MSG, hook: method)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def resolved_offenses
37
+ if bundle_eligible?
38
+ offenses = Dispatch.offenses_for(processed_source, config, :rspec_empty_line_after_hook)
39
+ return offenses unless offenses.nil?
40
+ end
41
+ Shirobai.check_rspec_empty_line_after_hook(
42
+ processed_source.buffer.source, *Shirobai::RSpec.segment(config)
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end