rubocop-vicenzo 0.9.0 → 0.10.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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +14 -0
- data/README.md +1 -1
- data/config/default.yml +18 -0
- data/lib/rubocop/cop/vicenzo/rspec/let_aliasing_subject.rb +121 -0
- data/lib/rubocop/cop/vicenzo/rspec/let_is_method_result.rb +11 -30
- data/lib/rubocop/cop/vicenzo/rspec/mixin/described_method.rb +7 -21
- data/lib/rubocop/cop/vicenzo/rspec/mixin/subject_reference.rb +61 -0
- data/lib/rubocop/cop/vicenzo/rspec/returned_chain.rb +71 -0
- data/lib/rubocop/cop/vicenzo/rspec/subject_is_method_result.rb +37 -5
- data/lib/rubocop/cop/vicenzo/style/prefer_many_over_size_comparison.rb +77 -0
- data/lib/rubocop/cop/vicenzo_cops.rb +2 -0
- data/lib/rubocop/vicenzo/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4f7a8f58f53c6f2e6b50f1645a55cb401cd33b1f2654c793eaecd52cbf879153
|
|
4
|
+
data.tar.gz: f2eaba7a6bbb615190abf109a64681372988e3f8cea255c6055f6ae40698c7e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0258853f6e8d32fb88d7d4c53fb5520e2c828bd31d5968be91008ab34c13a729188c802e91539ef95eef937c56dc584cb59c7dbb9ddcc8c50e3cd6e991eb49a1'
|
|
7
|
+
data.tar.gz: c8a4a8d41af0c5d5f294acfd9361f1226f002590b051508b97a6fbcb9f3f36dbe695a6ae6efbccb7c122395bc9dd76e83f7cffc9a026f3d822954e20236483df
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.0](https://github.com/bvicenzo/rubocop-vicenzo/compare/v0.9.0...v0.10.0) (2026-10-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add Vicenzo/RSpec/LetAliasingSubject cop ([#44](https://github.com/bvicenzo/rubocop-vicenzo/issues/44)) ([a8cebd3](https://github.com/bvicenzo/rubocop-vicenzo/commit/a8cebd3c0237e8805a58ff9efa7ca34f6beb34b0)), closes [#42](https://github.com/bvicenzo/rubocop-vicenzo/issues/42)
|
|
9
|
+
* add Vicenzo/Style/PreferManyOverSizeComparison cop ([#38](https://github.com/bvicenzo/rubocop-vicenzo/issues/38)) ([910ea02](https://github.com/bvicenzo/rubocop-vicenzo/commit/910ea026f7e4c46f9de27db580e6330cf845bfe7))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* report subjects that derive from an allowed method in SubjectIsMethodResult ([#41](https://github.com/bvicenzo/rubocop-vicenzo/issues/41)) ([a5efd33](https://github.com/bvicenzo/rubocop-vicenzo/commit/a5efd3330b32dd24b4291121a64e378091297a57)), closes [#40](https://github.com/bvicenzo/rubocop-vicenzo/issues/40)
|
|
15
|
+
* see subjects declared in shared groups in LetIsMethodResult and LetAliasingSubject ([#45](https://github.com/bvicenzo/rubocop-vicenzo/issues/45)) ([a98039e](https://github.com/bvicenzo/rubocop-vicenzo/commit/a98039e2cbea568267d8b7832b4717b763678d14)), closes [#43](https://github.com/bvicenzo/rubocop-vicenzo/issues/43)
|
|
16
|
+
|
|
3
17
|
## [0.9.0](https://github.com/bvicenzo/rubocop-vicenzo/compare/v0.8.0...v0.9.0) (2026-09-23)
|
|
4
18
|
|
|
5
19
|
|
data/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Add it to the `development`/`test` group of your `Gemfile`:
|
|
|
11
11
|
<!-- x-release-please-start-version -->
|
|
12
12
|
```ruby
|
|
13
13
|
group :development, :test do
|
|
14
|
-
gem 'rubocop-vicenzo', '~> 0.
|
|
14
|
+
gem 'rubocop-vicenzo', '~> 0.10.0', require: false
|
|
15
15
|
end
|
|
16
16
|
```
|
|
17
17
|
<!-- x-release-please-end-version -->
|
data/config/default.yml
CHANGED
|
@@ -127,6 +127,17 @@ Vicenzo/RSpec/LeakyDefinition:
|
|
|
127
127
|
- '**/spec/support/**/*'
|
|
128
128
|
- '**/spec/factories/**/*'
|
|
129
129
|
|
|
130
|
+
Vicenzo/RSpec/LetAliasingSubject:
|
|
131
|
+
Description: 'A let must not hold the subject itself under another name.'
|
|
132
|
+
Enabled: true
|
|
133
|
+
Severity: convention
|
|
134
|
+
Include:
|
|
135
|
+
- '**/spec/**/*_spec.rb'
|
|
136
|
+
Exclude:
|
|
137
|
+
- '**/spec/support/**/*'
|
|
138
|
+
- '**/spec/factories/**/*'
|
|
139
|
+
VersionAdded: '0.10.0'
|
|
140
|
+
|
|
130
141
|
Vicenzo/RSpec/LetIsMethodResult:
|
|
131
142
|
Description: 'A let under an example group describing a method must not hold the result of calling that method on the subject.'
|
|
132
143
|
Enabled: true
|
|
@@ -244,3 +255,10 @@ Vicenzo/Style/MultilineMethodCallParentheses:
|
|
|
244
255
|
Enabled: true
|
|
245
256
|
AllowedMethods: []
|
|
246
257
|
VersionAdded: '0.3.0'
|
|
258
|
+
|
|
259
|
+
Vicenzo/Style/PreferManyOverSizeComparison:
|
|
260
|
+
Description: 'Use many? instead of comparing size or length with one.'
|
|
261
|
+
Enabled: true
|
|
262
|
+
Severity: convention
|
|
263
|
+
SafeAutoCorrect: false
|
|
264
|
+
VersionAdded: '0.10.0'
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'returned_chain'
|
|
4
|
+
require_relative 'mixin/premise_tracking'
|
|
5
|
+
require_relative 'mixin/subject_reference'
|
|
6
|
+
|
|
7
|
+
module RuboCop
|
|
8
|
+
module Cop
|
|
9
|
+
module Vicenzo
|
|
10
|
+
module RSpec
|
|
11
|
+
# A `let` whose value is the subject itself is the subject under another name. Refer to the subject by
|
|
12
|
+
# its own name instead.
|
|
13
|
+
#
|
|
14
|
+
# The subject is who the sentences are about. A second name for it splits that across two
|
|
15
|
+
# declarations: the examples reading `hunter` no longer say they are about the subject, and every cop
|
|
16
|
+
# that recognises the subject by its name stops recognising it there. In "the cat caught the ball",
|
|
17
|
+
# calling the cat `hunter` in half of the sentences does not add a character - it hides one.
|
|
18
|
+
#
|
|
19
|
+
# A subject that was declared without a name and is then given one by a `let` is the same thing the
|
|
20
|
+
# other way round: the name belongs to the subject's declaration, `subject(:hunter) { ... }`. Inside a
|
|
21
|
+
# shared group that declares no named subject, the declaration lives where the group is included, so
|
|
22
|
+
# the subject is referred to as `subject`.
|
|
23
|
+
#
|
|
24
|
+
# This composes with `Vicenzo/RSpec/LetIsMethodResult`: once the alias is gone, a `let` that called
|
|
25
|
+
# the described method through it calls it on the subject, and that cop reports it.
|
|
26
|
+
#
|
|
27
|
+
# Only a value that is the subject and nothing more counts. A `let` deriving something from the
|
|
28
|
+
# subject, such as `cat.name`, is a premise, not an alias.
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# # bad - `hunter` is the cat
|
|
32
|
+
# describe '#catch' do
|
|
33
|
+
# subject(:cat) { Cat.new(name: 'Bixano') }
|
|
34
|
+
#
|
|
35
|
+
# let(:hunter) { cat }
|
|
36
|
+
#
|
|
37
|
+
# it { expect(hunter.catch(object: Ball.new)).to eq(:success) }
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # good - one name for the subject
|
|
41
|
+
# describe '#catch' do
|
|
42
|
+
# subject(:cat) { Cat.new(name: 'Bixano') }
|
|
43
|
+
#
|
|
44
|
+
# it { expect(cat.catch(object: Ball.new)).to eq(:success) }
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# @example an anonymous subject named by a let
|
|
48
|
+
# # bad - the name belongs to the subject's declaration
|
|
49
|
+
# describe '#catch' do
|
|
50
|
+
# subject { Cat.new(name: 'Bixano') }
|
|
51
|
+
#
|
|
52
|
+
# let(:hunter) { subject }
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# # good
|
|
56
|
+
# describe '#catch' do
|
|
57
|
+
# subject(:hunter) { Cat.new(name: 'Bixano') }
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# @example a shared group naming the subject of whoever includes it
|
|
61
|
+
# # bad - the subject is declared where the group is included, under a name it cannot know
|
|
62
|
+
# shared_examples 'a hunter' do
|
|
63
|
+
# let(:hunter) { subject }
|
|
64
|
+
#
|
|
65
|
+
# it { expect(hunter.catch(object: Ball.new)).to eq(:success) }
|
|
66
|
+
# end
|
|
67
|
+
#
|
|
68
|
+
# # good - `subject` holds wherever the group is included
|
|
69
|
+
# shared_examples 'a hunter' do
|
|
70
|
+
# it { expect(subject.catch(object: Ball.new)).to eq(:success) }
|
|
71
|
+
# end
|
|
72
|
+
class LetAliasingSubject < RuboCop::Cop::RSpec::Base
|
|
73
|
+
include PremiseTracking
|
|
74
|
+
include SubjectReference
|
|
75
|
+
|
|
76
|
+
MSG = 'Let `:%<name>s` is the subject under another name. Refer to the subject as `%<subject>s`.'
|
|
77
|
+
UNNAMED_MSG = 'Let `:%<name>s` is the subject under another name. Name the subject `%<name>s` instead.'
|
|
78
|
+
|
|
79
|
+
def on_block(node)
|
|
80
|
+
return unless let?(node)
|
|
81
|
+
|
|
82
|
+
reference = sole_call(node.body)
|
|
83
|
+
return unless reference
|
|
84
|
+
|
|
85
|
+
names = subject_names(node)
|
|
86
|
+
return unless subject_reference?(reference, names)
|
|
87
|
+
|
|
88
|
+
add_offense(node.send_node, message: message(node, reference.method_name, names))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
alias on_numblock on_block
|
|
92
|
+
alias on_itblock on_block
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def sole_call(body)
|
|
97
|
+
calls = ReturnedChain.new(body).to_a
|
|
98
|
+
|
|
99
|
+
calls.first if calls.one?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# `subject` points at the nearest named subject when there is one; without it, the alias is the name
|
|
103
|
+
# the subject was missing. Inside a shared group the subject is declared where the group is included,
|
|
104
|
+
# under a name this group cannot know, so `subject` is the one name that holds everywhere.
|
|
105
|
+
def message(node, reference_name, names)
|
|
106
|
+
name = premise_name(node)
|
|
107
|
+
subject = reference_name == :subject ? names.find { |candidate| candidate != :subject } : reference_name
|
|
108
|
+
return format(MSG, name:, subject:) if subject
|
|
109
|
+
return format(MSG, name:, subject: :subject) if inside_shared_group?(node)
|
|
110
|
+
|
|
111
|
+
format(UNNAMED_MSG, name:)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def inside_shared_group?(node)
|
|
115
|
+
node.each_ancestor(:any_block).any? { |ancestor| shared_group?(ancestor) }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'mixin/described_method'
|
|
4
4
|
require_relative 'mixin/premise_tracking'
|
|
5
|
+
require_relative 'mixin/subject_reference'
|
|
5
6
|
|
|
6
7
|
module RuboCop
|
|
7
8
|
module Cop
|
|
@@ -21,8 +22,10 @@ module RuboCop
|
|
|
21
22
|
#
|
|
22
23
|
# Only calls on the subject count, which is what separates the result from the setup: a `let`
|
|
23
24
|
# calling the described method on some other object is building a premise, not the outcome the
|
|
24
|
-
# example is about. `
|
|
25
|
-
#
|
|
25
|
+
# example is about. A `let` that only renames the subject is `Vicenzo/RSpec/LetAliasingSubject`'s
|
|
26
|
+
# to report; once it is gone, the call goes through the subject's own name and is reported here.
|
|
27
|
+
# `AllowedMethods` is shared with the sibling cop and ships empty, for the layers whose convention
|
|
28
|
+
# is a single entry point.
|
|
26
29
|
#
|
|
27
30
|
# @example
|
|
28
31
|
# # bad - the expectation asserts on a name; the cat never catches anything in the example
|
|
@@ -51,6 +54,7 @@ module RuboCop
|
|
|
51
54
|
class LetIsMethodResult < RuboCop::Cop::RSpec::Base
|
|
52
55
|
include DescribedMethod
|
|
53
56
|
include PremiseTracking
|
|
57
|
+
include SubjectReference
|
|
54
58
|
|
|
55
59
|
MSG = 'Let `:%<name>s` holds what `%<method>s` returned. Undo it and call `%<method>s` on the ' \
|
|
56
60
|
'subject inside the expectation.'
|
|
@@ -72,39 +76,16 @@ module RuboCop
|
|
|
72
76
|
|
|
73
77
|
private
|
|
74
78
|
|
|
79
|
+
# The receiver is read from the chain itself, the call right after the invocation, so that a subject
|
|
80
|
+
# passed along through a local variable or parentheses is still the subject.
|
|
75
81
|
def invocation_on_subject(node, method_name)
|
|
76
82
|
names = subject_names(node)
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
call, = ReturnedChain.new(node.body).each_cons(2).find do |invocation, receiver|
|
|
85
|
+
invocation.method?(method_name) && subject_reference?(receiver, names)
|
|
80
86
|
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# `subject` always refers to the subject, declared or implicit; a named one answers to its name
|
|
84
|
-
# as well.
|
|
85
|
-
def subject_names(node)
|
|
86
|
-
names = Set[:subject]
|
|
87
|
-
|
|
88
|
-
node.each_ancestor(:any_block).each do |ancestor|
|
|
89
|
-
next unless example_group?(ancestor)
|
|
90
|
-
|
|
91
|
-
each_child_block(ancestor) { |child| names << subject_name(child) if subject?(child) }
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
names
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# An anonymous `subject!` is still read as `subject`.
|
|
98
|
-
def subject_name(node)
|
|
99
|
-
name = premise_name(node).to_sym
|
|
100
|
-
|
|
101
|
-
name == :subject! ? :subject : name
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def subject_reference?(receiver, names)
|
|
105
|
-
return false unless receiver&.send_type? && receiver.receiver.nil? && receiver.arguments.empty?
|
|
106
87
|
|
|
107
|
-
|
|
88
|
+
call
|
|
108
89
|
end
|
|
109
90
|
end
|
|
110
91
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../returned_chain'
|
|
4
|
+
|
|
3
5
|
module RuboCop
|
|
4
6
|
module Cop
|
|
5
7
|
module Vicenzo
|
|
@@ -42,27 +44,11 @@ module RuboCop
|
|
|
42
44
|
cop_config.fetch('AllowedMethods', [])
|
|
43
45
|
end
|
|
44
46
|
|
|
45
|
-
#
|
|
46
|
-
# `Cat.new.catch(ball).to_s` is
|
|
47
|
-
#
|
|
48
|
-
def call_in_chain(body)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
while node
|
|
52
|
-
node = node.send_node if node.any_block_type?
|
|
53
|
-
break unless node.type?(:call)
|
|
54
|
-
return node if yield(node)
|
|
55
|
-
|
|
56
|
-
node = node.receiver
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
nil
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def returned_expression(body)
|
|
63
|
-
return if body.nil?
|
|
64
|
-
|
|
65
|
-
body.begin_type? ? body.children.last : body
|
|
47
|
+
# The first call the value of a definition is built on that satisfies the block, seen from the outside in:
|
|
48
|
+
# `Cat.new.catch(ball).to_s` is built on `catch` while `Cat.new(catches: other.catch)` is not - there,
|
|
49
|
+
# nothing of `catch` reaches the value. See `ReturnedChain`.
|
|
50
|
+
def call_in_chain(body, &)
|
|
51
|
+
ReturnedChain.new(body).find(&)
|
|
66
52
|
end
|
|
67
53
|
end
|
|
68
54
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module RSpec
|
|
7
|
+
# Shared helpers for the cops that ask whether a definition reads the subject: which names answer to it
|
|
8
|
+
# where the definition sits, and whether a call is a bare reference to one of them.
|
|
9
|
+
#
|
|
10
|
+
# Meant for cops inheriting from `RuboCop::Cop::RSpec::Base` and including `PremiseTracking`:
|
|
11
|
+
# `spec_group?` and `subject?` come from the former, `each_child_block` and `premise_name` from the
|
|
12
|
+
# latter.
|
|
13
|
+
module SubjectReference
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# `subject` always refers to the subject, declared or implicit; a named one answers to its name
|
|
17
|
+
# as well. A definition is read in every group below the one declaring it, so the subjects
|
|
18
|
+
# declared down there - the "abstract let" shape - answer too. Names come nearest group first, after
|
|
19
|
+
# `subject`.
|
|
20
|
+
#
|
|
21
|
+
# Shared groups count as groups, around the definition and below it: RSpec only lets a shared group
|
|
22
|
+
# be included under the group defining it, and a subject declared in one is the subject wherever it
|
|
23
|
+
# is included.
|
|
24
|
+
def subject_names(node)
|
|
25
|
+
names = Set[:subject]
|
|
26
|
+
groups = node.each_ancestor(:any_block).select { |ancestor| spec_group?(ancestor) }
|
|
27
|
+
|
|
28
|
+
collect_subject_names(groups.first, names, nested: true) if groups.any?
|
|
29
|
+
groups.drop(1).each { |group| collect_subject_names(group, names) }
|
|
30
|
+
|
|
31
|
+
names
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def collect_subject_names(group, names, nested: false)
|
|
35
|
+
each_child_block(group) do |child|
|
|
36
|
+
if subject?(child)
|
|
37
|
+
names << subject_name(child)
|
|
38
|
+
elsif nested && spec_group?(child)
|
|
39
|
+
collect_subject_names(child, names, nested:)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# An anonymous `subject!` is still read as `subject`, so it never passes for a name the subject was
|
|
45
|
+
# given.
|
|
46
|
+
def subject_name(node)
|
|
47
|
+
name = premise_name(node).to_sym
|
|
48
|
+
|
|
49
|
+
name == :subject! ? :subject : name
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def subject_reference?(receiver, names)
|
|
53
|
+
return false unless receiver.send_type? && receiver.receiver.nil? && receiver.arguments.empty?
|
|
54
|
+
|
|
55
|
+
names.include?(receiver.method_name)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module RSpec
|
|
7
|
+
# The calls the value of a block body is built on, outermost first: `Cat.new.catch(ball).to_s` yields `to_s`,
|
|
8
|
+
# `catch` and `new`, while `Cat.new(catches: other.catch)` yields only `new` - nothing of `catch` reaches the
|
|
9
|
+
# value.
|
|
10
|
+
#
|
|
11
|
+
# The walk starts at the statement the body returns and follows its receivers, seeing through what only
|
|
12
|
+
# passes a value along: parentheses, an assignment, and a local variable, which answers to its latest
|
|
13
|
+
# assignment among the statements before the one reading it.
|
|
14
|
+
class ReturnedChain
|
|
15
|
+
include Enumerable
|
|
16
|
+
|
|
17
|
+
attr_reader :expression
|
|
18
|
+
|
|
19
|
+
def initialize(body)
|
|
20
|
+
@statements = statements_of(body)
|
|
21
|
+
@expression = @statements.last
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def each
|
|
25
|
+
node = expression
|
|
26
|
+
reach = @statements.size - 1
|
|
27
|
+
|
|
28
|
+
while node
|
|
29
|
+
node, reach = value_of(node, reach)
|
|
30
|
+
node = node.send_node if node&.any_block_type?
|
|
31
|
+
break unless node&.type?(:call)
|
|
32
|
+
|
|
33
|
+
yield node
|
|
34
|
+
node = node.receiver
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def statements_of(body)
|
|
41
|
+
return [] if body.nil?
|
|
42
|
+
|
|
43
|
+
body.begin_type? ? body.children : [body]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# `reach` is the index of the statement the node is read in: a variable only answers to an assignment made
|
|
47
|
+
# before it, which keeps `caught = caught.to_s` from answering to itself.
|
|
48
|
+
def value_of(node, reach)
|
|
49
|
+
loop do
|
|
50
|
+
case node&.type
|
|
51
|
+
when :begin then node = node.children.last
|
|
52
|
+
when :lvasgn then node = node.expression
|
|
53
|
+
when :lvar
|
|
54
|
+
reach = assignment_before(node.name, reach)
|
|
55
|
+
node = reach && @statements[reach]
|
|
56
|
+
else return [node, reach]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def assignment_before(name, reach)
|
|
62
|
+
(reach - 1).downto(0).find do |index|
|
|
63
|
+
statement = @statements[index]
|
|
64
|
+
statement.lvasgn_type? && statement.name == name
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -24,7 +24,11 @@ module RuboCop
|
|
|
24
24
|
# (a service object always invoked through `call`, for instance). `AllowedMethods` exists
|
|
25
25
|
# for those and ships empty: the convention belongs to the project that adopts it, not to
|
|
26
26
|
# this gem. Declaring it there - or excluding a whole directory through the standard
|
|
27
|
-
# `Exclude` - keeps the exemption visible and reviewable in `.rubocop.yml`.
|
|
27
|
+
# `Exclude` - keeps the exemption visible and reviewable in `.rubocop.yml`. The exemption
|
|
28
|
+
# covers the result itself, not what is derived from it: a subject that chains anything
|
|
29
|
+
# after the allowed call - a projection such as `map`, or unwrapping a monad through
|
|
30
|
+
# `value` - is still reported, since it drops the data the examples end up calling the
|
|
31
|
+
# method again to reach.
|
|
28
32
|
#
|
|
29
33
|
# An inline disable directive is not that: it is an exemption nobody reviewed. Dropping
|
|
30
34
|
# the `#` from the description is not either - the cop reads bare descriptions too, and the
|
|
@@ -59,26 +63,54 @@ module RuboCop
|
|
|
59
63
|
# describe '.call' do
|
|
60
64
|
# subject(:result) { described_class.(object: Ball.new) }
|
|
61
65
|
# end
|
|
66
|
+
#
|
|
67
|
+
# # bad - the subject is derived from the result, not the result itself
|
|
68
|
+
# describe '.call' do
|
|
69
|
+
# subject(:ids) { described_class.().map(&:id) }
|
|
70
|
+
# end
|
|
71
|
+
#
|
|
72
|
+
# # bad - unwrapping the monad keeps only a part of the result
|
|
73
|
+
# describe '.call' do
|
|
74
|
+
# subject(:value) { described_class.(object: Ball.new).value }
|
|
75
|
+
# end
|
|
62
76
|
class SubjectIsMethodResult < RuboCop::Cop::RSpec::Base
|
|
63
77
|
include DescribedMethod
|
|
64
78
|
|
|
65
79
|
MSG = 'Subject holds what `%<method>s` returned, not the object under test. ' \
|
|
66
80
|
'Make the subject the object that receives `%<method>s`, and call it inside the example.'
|
|
81
|
+
DERIVED_MSG = 'Subject derives from what `%<method>s` returned. ' \
|
|
82
|
+
'Keep the subject as the result of `%<method>s`, and derive from it inside the example.'
|
|
67
83
|
|
|
68
84
|
def on_block(node)
|
|
69
85
|
return unless subject?(node)
|
|
70
86
|
|
|
71
87
|
method_name = described_method_name(node)
|
|
72
|
-
return if method_name.nil?
|
|
88
|
+
return if method_name.nil?
|
|
73
89
|
|
|
74
|
-
|
|
75
|
-
|
|
90
|
+
chain = ReturnedChain.new(node.body)
|
|
91
|
+
invocations = chain.select { |call| call.method?(method_name) }
|
|
92
|
+
return if invocations.empty?
|
|
76
93
|
|
|
77
|
-
|
|
94
|
+
if allowed_methods.include?(method_name)
|
|
95
|
+
check_derivation(chain, invocations.last, method_name)
|
|
96
|
+
else
|
|
97
|
+
add_offense(invocations.first, message: format(MSG, method: method_name))
|
|
98
|
+
end
|
|
78
99
|
end
|
|
79
100
|
|
|
80
101
|
alias on_numblock on_block
|
|
81
102
|
alias on_itblock on_block
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# An allowed method may be the subject only as the returned expression itself: its innermost
|
|
107
|
+
# invocation has to be the outermost call of the chain. A block handed to it still belongs to the
|
|
108
|
+
# call; anything chained after it - calling the result again included - does not.
|
|
109
|
+
def check_derivation(chain, innermost_invocation, method_name)
|
|
110
|
+
return if chain.first.equal?(innermost_invocation)
|
|
111
|
+
|
|
112
|
+
add_offense(chain.expression, message: format(DERIVED_MSG, method: method_name))
|
|
113
|
+
end
|
|
82
114
|
end
|
|
83
115
|
end
|
|
84
116
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module Style
|
|
7
|
+
# Enforces `many?` over comparing `size` or `length` with one.
|
|
8
|
+
#
|
|
9
|
+
# Active Support's `Enumerable#many?` exists precisely to name the
|
|
10
|
+
# "more than one element" question that `size > 1` only spells out.
|
|
11
|
+
# RuboCop's own `Style/CollectionQuerying` already covers `count > 1`, but
|
|
12
|
+
# leaves `size` and `length` alone, which is the form most often written.
|
|
13
|
+
# This cop closes that gap.
|
|
14
|
+
#
|
|
15
|
+
# Only an explicit receiver calling `size` or `length` without arguments is
|
|
16
|
+
# flagged, compared with one through `>` or with two through `>=`, on
|
|
17
|
+
# either side of the operator. Like `Style/CollectionQuerying`, the cop
|
|
18
|
+
# only runs when `AllCops: ActiveSupportExtensionsEnabled` is on, since
|
|
19
|
+
# `many?` comes from Active Support. `rubocop-rails` turns it on.
|
|
20
|
+
#
|
|
21
|
+
# @safety
|
|
22
|
+
# The cop is unsafe because the receiver might not include `Enumerable`.
|
|
23
|
+
# `String`, for instance, responds to `size` and `length` but not to
|
|
24
|
+
# `many?`. The same holds for any object with its own `count`, which is
|
|
25
|
+
# why `Style/CollectionQuerying` is unsafe as well.
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# # bad
|
|
29
|
+
# items.size > 1
|
|
30
|
+
# items.length >= 2
|
|
31
|
+
# 1 < items.size
|
|
32
|
+
#
|
|
33
|
+
# # good
|
|
34
|
+
# items.many?
|
|
35
|
+
class PreferManyOverSizeComparison < Base
|
|
36
|
+
extend AutoCorrector
|
|
37
|
+
|
|
38
|
+
MSG = 'Use `many?` instead of `%<comparison>s`.'
|
|
39
|
+
|
|
40
|
+
RESTRICT_ON_SEND = %i[> >= < <=].freeze
|
|
41
|
+
|
|
42
|
+
# @!method size_comparison(node)
|
|
43
|
+
def_node_matcher :size_comparison, <<~PATTERN
|
|
44
|
+
{
|
|
45
|
+
(call $(call !nil? {:size :length}) {:> (int 1) | :>= (int 2)})
|
|
46
|
+
(call {(int 1) :< | (int 2) :<=} $(call !nil? {:size :length}))
|
|
47
|
+
}
|
|
48
|
+
PATTERN
|
|
49
|
+
|
|
50
|
+
def on_send(node)
|
|
51
|
+
return unless active_support_extensions_enabled?
|
|
52
|
+
return unless (size_node = size_comparison(node))
|
|
53
|
+
|
|
54
|
+
message = format(MSG, comparison: comparison_source(node, size_node))
|
|
55
|
+
|
|
56
|
+
add_offense(node, message: message) do |corrector|
|
|
57
|
+
corrector.replace(node, "#{size_node.receiver.source}#{size_node.loc.dot.source}many?")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
alias on_csend on_send
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def comparison_source(node, size_node)
|
|
65
|
+
size_source = size_node.method_name
|
|
66
|
+
|
|
67
|
+
if node.receiver.equal?(size_node)
|
|
68
|
+
"#{size_source} #{node.method_name} #{node.first_argument.source}"
|
|
69
|
+
else
|
|
70
|
+
"#{node.receiver.source} #{node.method_name} #{size_source}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -13,6 +13,7 @@ require_relative 'vicenzo/rspec/nested_context_improper_start'
|
|
|
13
13
|
require_relative 'vicenzo/rspec/nested_let_redefinition'
|
|
14
14
|
require_relative 'vicenzo/rspec/nested_subject_redefinition'
|
|
15
15
|
require_relative 'vicenzo/rspec/leaky_definition'
|
|
16
|
+
require_relative 'vicenzo/rspec/let_aliasing_subject'
|
|
16
17
|
require_relative 'vicenzo/rspec/subject_defined_as_let'
|
|
17
18
|
require_relative 'vicenzo/rspec/subject_is_method_result'
|
|
18
19
|
require_relative 'vicenzo/rspec/top_level_context'
|
|
@@ -27,3 +28,4 @@ require_relative 'vicenzo/naming/anonymous_unused_name'
|
|
|
27
28
|
require_relative 'vicenzo/naming/short_name'
|
|
28
29
|
require_relative 'vicenzo/style/json_parse_symbolize_names'
|
|
29
30
|
require_relative 'vicenzo/style/multiline_method_call_parentheses'
|
|
31
|
+
require_relative 'vicenzo/style/prefer_many_over_size_comparison'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-vicenzo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bruno Vicenzo
|
|
@@ -86,15 +86,18 @@ files:
|
|
|
86
86
|
- lib/rubocop/cop/vicenzo/rspec/inconsistent_sibling_structure.rb
|
|
87
87
|
- lib/rubocop/cop/vicenzo/rspec/iteration_inside_example.rb
|
|
88
88
|
- lib/rubocop/cop/vicenzo/rspec/leaky_definition.rb
|
|
89
|
+
- lib/rubocop/cop/vicenzo/rspec/let_aliasing_subject.rb
|
|
89
90
|
- lib/rubocop/cop/vicenzo/rspec/let_is_method_result.rb
|
|
90
91
|
- lib/rubocop/cop/vicenzo/rspec/mixin/described_method.rb
|
|
91
92
|
- lib/rubocop/cop/vicenzo/rspec/mixin/expectation_target.rb
|
|
92
93
|
- lib/rubocop/cop/vicenzo/rspec/mixin/premise_tracking.rb
|
|
94
|
+
- lib/rubocop/cop/vicenzo/rspec/mixin/subject_reference.rb
|
|
93
95
|
- lib/rubocop/cop/vicenzo/rspec/mixin/top_level_definition.rb
|
|
94
96
|
- lib/rubocop/cop/vicenzo/rspec/mutated_premises.rb
|
|
95
97
|
- lib/rubocop/cop/vicenzo/rspec/nested_context_improper_start.rb
|
|
96
98
|
- lib/rubocop/cop/vicenzo/rspec/nested_let_redefinition.rb
|
|
97
99
|
- lib/rubocop/cop/vicenzo/rspec/nested_subject_redefinition.rb
|
|
100
|
+
- lib/rubocop/cop/vicenzo/rspec/returned_chain.rb
|
|
98
101
|
- lib/rubocop/cop/vicenzo/rspec/subject_defined_as_let.rb
|
|
99
102
|
- lib/rubocop/cop/vicenzo/rspec/subject_is_method_result.rb
|
|
100
103
|
- lib/rubocop/cop/vicenzo/rspec/top_level_context.rb
|
|
@@ -105,6 +108,7 @@ files:
|
|
|
105
108
|
- lib/rubocop/cop/vicenzo/rspec/unnested_context_improper_start.rb
|
|
106
109
|
- lib/rubocop/cop/vicenzo/style/json_parse_symbolize_names.rb
|
|
107
110
|
- lib/rubocop/cop/vicenzo/style/multiline_method_call_parentheses.rb
|
|
111
|
+
- lib/rubocop/cop/vicenzo/style/prefer_many_over_size_comparison.rb
|
|
108
112
|
- lib/rubocop/cop/vicenzo_cops.rb
|
|
109
113
|
- lib/rubocop/vicenzo.rb
|
|
110
114
|
- lib/rubocop/vicenzo/plugin.rb
|