rubocop-vicenzo 0.5.0 → 0.7.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 +3 -0
- data/CHANGELOG.md +16 -1
- data/CONTRIBUTING.md +100 -0
- data/README.md +23 -4
- data/config/default.yml +70 -0
- data/lib/rubocop/cop/vicenzo/rspec/competing_subjects.rb +149 -0
- data/lib/rubocop/cop/vicenzo/rspec/derived_premises.rb +75 -0
- data/lib/rubocop/cop/vicenzo/rspec/dynamic_example_generation.rb +3 -3
- data/lib/rubocop/cop/vicenzo/rspec/iteration_inside_example.rb +7 -7
- data/lib/rubocop/cop/vicenzo/rspec/let_is_method_result.rb +113 -0
- data/lib/rubocop/cop/vicenzo/rspec/mixin/described_method.rb +71 -0
- data/lib/rubocop/cop/vicenzo/rspec/mixin/expectation_target.rb +101 -0
- data/lib/rubocop/cop/vicenzo/rspec/mixin/premise_tracking.rb +117 -0
- data/lib/rubocop/cop/vicenzo/rspec/mutated_premises.rb +140 -0
- data/lib/rubocop/cop/vicenzo/rspec/subject_defined_as_let.rb +108 -0
- data/lib/rubocop/cop/vicenzo/rspec/subject_is_method_result.rb +86 -0
- data/lib/rubocop/cop/vicenzo_cops.rb +6 -0
- data/lib/rubocop/vicenzo/version.rb +1 -1
- data/release-please-config.json +15 -0
- metadata +14 -3
- data/rakelib/release.rake +0 -73
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4f2fb87b8d1bfd10a66d52fcd5ede5f1eea78c7c2a8b7b8ea8a692d0efce54b6
|
|
4
|
+
data.tar.gz: aa25bb974ce70ab9ef752a1e36b83d779abded472022258a86ff8c2fdc483d77
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57918353d578d8c2a5840617ccfefe7a996aad212dbce8921ae18a1bf302bf89fb6a5e40f6b9cdd97d082c4fb74d2e26fb2a9148d92e83fbb074e6c094da996b
|
|
7
|
+
data.tar.gz: be26bf4c722a8e351112c02d58bba96d06fd62fa230e8feb20e3f80675c82f6ca377875a53281df99d39ccabf8fc5de2822f9eff985a6c7a3cbe188dedd3ff6d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.7.0](https://github.com/bvicenzo/rubocop-vicenzo/compare/v0.6.0...v0.7.0) (2026-08-31)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add RSpec cops against subjects hidden in lets ([#30](https://github.com/bvicenzo/rubocop-vicenzo/issues/30)) ([9daf259](https://github.com/bvicenzo/rubocop-vicenzo/commit/9daf25972252fdfdb92a51bba9cfe70e63446d17))
|
|
9
|
+
* add Vicenzo/RSpec/SubjectIsMethodResult cop ([#28](https://github.com/bvicenzo/rubocop-vicenzo/issues/28)) ([b69245a](https://github.com/bvicenzo/rubocop-vicenzo/commit/b69245af973f3341bbe62bca628333c5acb909d7))
|
|
10
|
+
|
|
11
|
+
## [0.6.0](https://github.com/bvicenzo/rubocop-vicenzo/compare/v0.5.0...v0.6.0) (2026-08-19)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* add RSpec cops against mutated and derived premises ([#26](https://github.com/bvicenzo/rubocop-vicenzo/issues/26)) ([b138dd0](https://github.com/bvicenzo/rubocop-vicenzo/commit/b138dd00a76e1bcd62e95c1f2c51dca5b4232319))
|
|
2
17
|
|
|
3
18
|
## [0.5.0] - 2026-06-29
|
|
4
19
|
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Development setup
|
|
4
|
+
|
|
5
|
+
Run `bin/setup` once after cloning. It installs dependencies and points
|
|
6
|
+
`core.hooksPath` at `.githooks/`, enabling a `commit-msg` hook that enforces
|
|
7
|
+
Conventional Commits locally.
|
|
8
|
+
|
|
9
|
+
Run the test suite and the linter with the default task — the same one CI runs:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle exec rake
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Commit messages — Conventional Commits
|
|
16
|
+
|
|
17
|
+
This project follows [Conventional Commits](https://www.conventionalcommits.org/).
|
|
18
|
+
The version bump and the `CHANGELOG.md` are generated automatically from commit
|
|
19
|
+
messages, so the prefix matters:
|
|
20
|
+
|
|
21
|
+
| Type | When to use | Release effect |
|
|
22
|
+
|------|-------------|----------------|
|
|
23
|
+
| `feat:` | New cop or user-facing capability | minor bump |
|
|
24
|
+
| `fix:` | Bug fix in a cop or in the config | patch bump |
|
|
25
|
+
| `feat!:` or a `BREAKING CHANGE:` footer | Backwards-incompatible change | minor bump while `0.x`, major from `1.0` on |
|
|
26
|
+
| `chore:` `ci:` `docs:` `style:` `test:` `refactor:` | Maintenance | no release |
|
|
27
|
+
|
|
28
|
+
Title format: `type: short description` (e.g.
|
|
29
|
+
`feat: add Vicenzo/Style/JsonParseSymbolizeNames cop`).
|
|
30
|
+
|
|
31
|
+
Pull requests are squash-merged, so **the PR title becomes the commit message
|
|
32
|
+
release-please reads**. Get the title right even when the individual commits are
|
|
33
|
+
messy.
|
|
34
|
+
|
|
35
|
+
## Adding a new cop
|
|
36
|
+
|
|
37
|
+
Generate the cop, its spec and its `config/default.yml` entry with:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bundle exec rake 'new_cop[Vicenzo/Department/CopName]'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The generator writes `VersionAdded: '<<next>>'` into `config/default.yml`. Leave
|
|
44
|
+
that placeholder alone — never replace it with a version by hand. The release
|
|
45
|
+
workflow fills it in with the version being released (see below).
|
|
46
|
+
|
|
47
|
+
## Releasing (release-please)
|
|
48
|
+
|
|
49
|
+
Releases are automated with [release-please](https://github.com/googleapis/release-please):
|
|
50
|
+
|
|
51
|
+
1. Every push to `master` creates or updates a **release PR** titled
|
|
52
|
+
`chore(master): release X.Y.Z`. It bumps
|
|
53
|
+
`lib/rubocop/vicenzo/version.rb`, `.release-please-manifest.json`,
|
|
54
|
+
`Gemfile.lock` and the version in the `README.md` install snippet, and updates
|
|
55
|
+
`CHANGELOG.md` from the Conventional Commits made since the last release.
|
|
56
|
+
2. New cops are added with `VersionAdded: '<<next>>'`. The release workflow
|
|
57
|
+
replaces every `<<next>>` in `config/default.yml` with the version being
|
|
58
|
+
released, directly on the release PR — no manual editing needed. This matters
|
|
59
|
+
because `rake docs:generate` reads `VersionAdded` to build the "Version Added"
|
|
60
|
+
column of the documentation site.
|
|
61
|
+
3. **Merging the release PR** creates the git tag `vX.Y.Z` and a GitHub Release.
|
|
62
|
+
That in turn triggers two things automatically:
|
|
63
|
+
- `.github/workflows/publish.yml` publishes the gem to RubyGems via
|
|
64
|
+
[Trusted Publishing](https://guides.rubygems.org/trusted-publishing/) (OIDC,
|
|
65
|
+
no API key stored in the repository).
|
|
66
|
+
- `.github/workflows/docs.yml` rebuilds and deploys the documentation site to
|
|
67
|
+
GitHub Pages, because `lib/rubocop/vicenzo/version.rb` changed.
|
|
68
|
+
4. The release PR always reflects **everything on `master` since the last
|
|
69
|
+
release**. Cut releases promptly: merge the release PR before landing
|
|
70
|
+
unrelated new work you don't want included in that release.
|
|
71
|
+
|
|
72
|
+
Nothing about a release is done by hand — there is no `rake release` to run
|
|
73
|
+
locally and no version to edit yourself.
|
|
74
|
+
|
|
75
|
+
## If the release PR is failing
|
|
76
|
+
|
|
77
|
+
release-please only rebuilds the release branch when a **releasable** commit
|
|
78
|
+
(`feat:` / `fix:`) lands on `master`. Non-releasable fixes (`chore:`, `style:`,
|
|
79
|
+
`ci:`, `docs:`, `test:`) do **not** rebuild it, so the release branch can fall
|
|
80
|
+
behind `master` and its CI can run stale code.
|
|
81
|
+
|
|
82
|
+
When the release PR's CI is red:
|
|
83
|
+
|
|
84
|
+
1. Push the fix to `master` through a normal PR. **Never** push directly to the
|
|
85
|
+
`release-please--…` branch — release-please overwrites it.
|
|
86
|
+
2. If those fix commits are non-releasable (so the release PR won't refresh on
|
|
87
|
+
its own), **close the release PR and delete its branch**. On the next push to
|
|
88
|
+
`master`, release-please recreates the release PR from the current `master`
|
|
89
|
+
(now including the fix) and its CI passes.
|
|
90
|
+
3. Merge the freshly recreated release PR.
|
|
91
|
+
|
|
92
|
+
Never manually rebase or force-push the release-please branch.
|
|
93
|
+
|
|
94
|
+
## If the gem was not published
|
|
95
|
+
|
|
96
|
+
The tag and the GitHub Release already exist, so publishing is retryable: fix the
|
|
97
|
+
cause and use **Re-run jobs** on the failed `Publish gem` workflow run. An
|
|
98
|
+
authentication failure points at the trusted publisher registration on RubyGems
|
|
99
|
+
(the repository or the workflow filename must match `publish.yml`), not at the
|
|
100
|
+
workflow itself.
|
data/README.md
CHANGED
|
@@ -6,7 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Add it to the `development`/`test` group of your `Gemfile`:
|
|
10
|
+
|
|
11
|
+
<!-- x-release-please-start-version -->
|
|
12
|
+
```ruby
|
|
13
|
+
group :development, :test do
|
|
14
|
+
gem 'rubocop-vicenzo', '~> 0.7.0', require: false
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
<!-- x-release-please-end-version -->
|
|
18
|
+
|
|
19
|
+
> The version above is kept current automatically on every release; pin to
|
|
20
|
+
> whichever version you prefer.
|
|
21
|
+
|
|
22
|
+
Or let Bundler add the latest version for you:
|
|
10
23
|
|
|
11
24
|
```bash
|
|
12
25
|
bundle add rubocop-vicenzo --group=development --require=false
|
|
@@ -55,7 +68,13 @@ rubocop --plugin rubocop-vicenzo
|
|
|
55
68
|
|
|
56
69
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
57
70
|
|
|
58
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
71
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
72
|
+
|
|
73
|
+
Releases are automated: the version, the `CHANGELOG.md`, the git tag and the push
|
|
74
|
+
to [rubygems.org](https://rubygems.org) are all derived from
|
|
75
|
+
[Conventional Commits](https://www.conventionalcommits.org/) by release-please.
|
|
76
|
+
There is no version to edit and no release command to run by hand — see
|
|
77
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
59
78
|
|
|
60
79
|
### Documentation
|
|
61
80
|
|
|
@@ -92,7 +111,7 @@ bundle exec rake 'new_cop[Vicenzo/OptionalNamespace/CopName]'
|
|
|
92
111
|
|
|
93
112
|
## Contributing
|
|
94
113
|
|
|
95
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
114
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/bvicenzo/rubocop-vicenzo. Read [CONTRIBUTING.md](CONTRIBUTING.md) first — it covers the commit conventions the release automation depends on. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/bvicenzo/rubocop-vicenzo/blob/master/CODE_OF_CONDUCT.md).
|
|
96
115
|
|
|
97
116
|
## License
|
|
98
117
|
|
|
@@ -100,4 +119,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
100
119
|
|
|
101
120
|
## Code of Conduct
|
|
102
121
|
|
|
103
|
-
Everyone interacting in the Rubocop::Vicenzo project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
|
122
|
+
Everyone interacting in the Rubocop::Vicenzo project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/bvicenzo/rubocop-vicenzo/blob/master/CODE_OF_CONDUCT.md).
|
data/config/default.yml
CHANGED
|
@@ -11,6 +11,18 @@ Vicenzo/Rails/EnumInclusionOfValidation:
|
|
|
11
11
|
Severity: convention
|
|
12
12
|
VersionAdded: '0.1.0'
|
|
13
13
|
|
|
14
|
+
Vicenzo/RSpec/CompetingSubjects:
|
|
15
|
+
Description: 'Definitions the expectations assert on, two or more in a group declaring no subject, are either hidden contexts or a premise that belongs in a before.'
|
|
16
|
+
Enabled: true
|
|
17
|
+
Severity: convention
|
|
18
|
+
AllowedMethods: []
|
|
19
|
+
Include:
|
|
20
|
+
- '**/spec/**/*_spec.rb'
|
|
21
|
+
Exclude:
|
|
22
|
+
- '**/spec/support/**/*'
|
|
23
|
+
- '**/spec/factories/**/*'
|
|
24
|
+
VersionAdded: '0.7.0'
|
|
25
|
+
|
|
14
26
|
Vicenzo/RSpec/ConditionalInSpec:
|
|
15
27
|
Description: 'Do not use conditional logic in specs. Extract each branch into an explicit context instead.'
|
|
16
28
|
Enabled: true
|
|
@@ -22,6 +34,17 @@ Vicenzo/RSpec/ConditionalInSpec:
|
|
|
22
34
|
- '**/spec/factories/**/*'
|
|
23
35
|
VersionAdded: '0.4.0'
|
|
24
36
|
|
|
37
|
+
Vicenzo/RSpec/DerivedPremises:
|
|
38
|
+
Description: 'Do not derive a premise from another premise; declare the complete value where it is needed.'
|
|
39
|
+
Enabled: true
|
|
40
|
+
Severity: warning
|
|
41
|
+
Include:
|
|
42
|
+
- '**/spec/**/*_spec.rb'
|
|
43
|
+
Exclude:
|
|
44
|
+
- '**/spec/support/**/*'
|
|
45
|
+
- '**/spec/factories/**/*'
|
|
46
|
+
VersionAdded: '0.6.0'
|
|
47
|
+
|
|
25
48
|
Vicenzo/RSpec/DynamicExampleGeneration:
|
|
26
49
|
Description: 'Do not use iteration to dynamically generate example groups or examples.'
|
|
27
50
|
Enabled: true
|
|
@@ -40,6 +63,17 @@ Vicenzo/RSpec/IterationInsideExample:
|
|
|
40
63
|
Severity: warning
|
|
41
64
|
VersionAdded: '0.4.0'
|
|
42
65
|
|
|
66
|
+
Vicenzo/RSpec/MutatedPremises:
|
|
67
|
+
Description: 'Do not mutate the premises (let, let_it_be, subject) an example starts from.'
|
|
68
|
+
Enabled: true
|
|
69
|
+
Severity: warning
|
|
70
|
+
Include:
|
|
71
|
+
- '**/spec/**/*_spec.rb'
|
|
72
|
+
Exclude:
|
|
73
|
+
- '**/spec/support/**/*'
|
|
74
|
+
- '**/spec/factories/**/*'
|
|
75
|
+
VersionAdded: '0.6.0'
|
|
76
|
+
|
|
43
77
|
Vicenzo/RSpec/NestedContextImproperStart:
|
|
44
78
|
Description: 'Check if the nested context does not start as a root one.'
|
|
45
79
|
Enabled: true
|
|
@@ -69,6 +103,42 @@ Vicenzo/RSpec/LeakyDefinition:
|
|
|
69
103
|
- '**/spec/support/**/*'
|
|
70
104
|
- '**/spec/factories/**/*'
|
|
71
105
|
|
|
106
|
+
Vicenzo/RSpec/LetIsMethodResult:
|
|
107
|
+
Description: 'A let under an example group describing a method must not hold the result of calling that method on the subject.'
|
|
108
|
+
Enabled: true
|
|
109
|
+
Severity: convention
|
|
110
|
+
AllowedMethods: []
|
|
111
|
+
Include:
|
|
112
|
+
- '**/spec/**/*_spec.rb'
|
|
113
|
+
Exclude:
|
|
114
|
+
- '**/spec/support/**/*'
|
|
115
|
+
- '**/spec/factories/**/*'
|
|
116
|
+
VersionAdded: '0.7.0'
|
|
117
|
+
|
|
118
|
+
Vicenzo/RSpec/SubjectDefinedAsLet:
|
|
119
|
+
Description: 'A let the expectations assert on, in a group declaring no subject, must be declared as the subject.'
|
|
120
|
+
Enabled: true
|
|
121
|
+
Severity: convention
|
|
122
|
+
AllowedMethods: []
|
|
123
|
+
Include:
|
|
124
|
+
- '**/spec/**/*_spec.rb'
|
|
125
|
+
Exclude:
|
|
126
|
+
- '**/spec/support/**/*'
|
|
127
|
+
- '**/spec/factories/**/*'
|
|
128
|
+
VersionAdded: '0.7.0'
|
|
129
|
+
|
|
130
|
+
Vicenzo/RSpec/SubjectIsMethodResult:
|
|
131
|
+
Description: 'The subject of an example group describing a method must be the object under test, not the method result.'
|
|
132
|
+
Enabled: true
|
|
133
|
+
Severity: convention
|
|
134
|
+
AllowedMethods: []
|
|
135
|
+
Include:
|
|
136
|
+
- '**/spec/**/*_spec.rb'
|
|
137
|
+
Exclude:
|
|
138
|
+
- '**/spec/support/**/*'
|
|
139
|
+
- '**/spec/factories/**/*'
|
|
140
|
+
VersionAdded: '0.7.0'
|
|
141
|
+
|
|
72
142
|
Vicenzo/Style/JsonParseSymbolizeNames:
|
|
73
143
|
Description: 'Enforces passing `symbolize_names: true` to `JSON.parse` so keys are symbols.'
|
|
74
144
|
Enabled: true
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'mixin/described_method'
|
|
4
|
+
require_relative 'mixin/expectation_target'
|
|
5
|
+
require_relative 'mixin/premise_tracking'
|
|
6
|
+
|
|
7
|
+
module RuboCop
|
|
8
|
+
module Cop
|
|
9
|
+
module Vicenzo
|
|
10
|
+
module RSpec
|
|
11
|
+
# Two or more definitions the expectations assert on, in a group that declares no subject, are
|
|
12
|
+
# contexts that were never written.
|
|
13
|
+
#
|
|
14
|
+
# A group speaks about one subject. When its examples each assert on a different definition, the
|
|
15
|
+
# group is not one scenario with several facts: it is several scenarios sharing a roof, and the
|
|
16
|
+
# circumstance that tells them apart - the one a `context` would have named - lives only in the
|
|
17
|
+
# definitions' names and in the reader's head.
|
|
18
|
+
#
|
|
19
|
+
# The fix is structural, and it is never to pick one of them to promote. Which shape it takes
|
|
20
|
+
# depends on what the definitions are to this group, and working that out is the whole job -
|
|
21
|
+
# which is why the offense names what was found rather than prescribing a repair:
|
|
22
|
+
#
|
|
23
|
+
# 1. They are different scenarios. Give each its own `context`, saying out loud the circumstance
|
|
24
|
+
# it stands for, and let each declare the subject it is about. What was encoded in
|
|
25
|
+
# `record_1` / `record_2` becomes a sentence, and the example shrinks to the outcome alone.
|
|
26
|
+
# 2. One outcome covers them all. Then they were never subjects: they are the premise. Move them
|
|
27
|
+
# into a `before`, unnamed, and assert on the collection they belong to.
|
|
28
|
+
#
|
|
29
|
+
# Both are corrections to the spec. Neither is silencing the cop: a definition named
|
|
30
|
+
# `record_1` is the shape this smell takes, so renaming it settles nothing, and an inline
|
|
31
|
+
# disable directive keeps the hidden scenario hidden - which is the cost being paid here.
|
|
32
|
+
#
|
|
33
|
+
# A group that already declares a subject is left alone - there the subject is settled and the
|
|
34
|
+
# other definitions are premises, whatever their names suggest.
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# # bad - two subjects under one roof; the circumstances are only in the names
|
|
38
|
+
# describe '#approved?' do
|
|
39
|
+
# let!(:reviewed_post) { create(:post, reviewed_at: Time.current) }
|
|
40
|
+
# let!(:draft_post) { create(:post) }
|
|
41
|
+
#
|
|
42
|
+
# it { expect(reviewed_post).to be_approved }
|
|
43
|
+
# it { expect(draft_post).not_to be_approved }
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# # good - one context per scenario, each with its own subject
|
|
47
|
+
# describe '#approved?' do
|
|
48
|
+
# context 'when the post has been reviewed' do
|
|
49
|
+
# subject(:post) { create(:post, reviewed_at: Time.current) }
|
|
50
|
+
#
|
|
51
|
+
# it { is_expected.to be_approved }
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# context 'without a review' do
|
|
55
|
+
# subject(:post) { create(:post) }
|
|
56
|
+
#
|
|
57
|
+
# it { is_expected.not_to be_approved }
|
|
58
|
+
# end
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# @example when one outcome covers every record, they are the premise
|
|
62
|
+
# # bad - two records named as if each were a subject
|
|
63
|
+
# describe '#deactivate' do
|
|
64
|
+
# let!(:pending_alert) { create(:alert, :pending) }
|
|
65
|
+
# let!(:firing_alert) { create(:alert, :firing) }
|
|
66
|
+
#
|
|
67
|
+
# it 'dismisses every alert', :aggregate_failures do
|
|
68
|
+
# monitor.deactivate
|
|
69
|
+
#
|
|
70
|
+
# expect(pending_alert.reload).to be_dismissed
|
|
71
|
+
# expect(firing_alert.reload).to be_dismissed
|
|
72
|
+
# end
|
|
73
|
+
# end
|
|
74
|
+
#
|
|
75
|
+
# # good - the records are the premise, and the outcome is about the collection
|
|
76
|
+
# describe '#deactivate' do
|
|
77
|
+
# before do
|
|
78
|
+
# create(:alert, :pending)
|
|
79
|
+
# create(:alert, :firing)
|
|
80
|
+
# end
|
|
81
|
+
#
|
|
82
|
+
# it 'dismisses every alert' do
|
|
83
|
+
# monitor.deactivate
|
|
84
|
+
#
|
|
85
|
+
# expect(monitor.alerts).to contain_exactly(be_dismissed, be_dismissed)
|
|
86
|
+
# end
|
|
87
|
+
# end
|
|
88
|
+
#
|
|
89
|
+
# @example records a single expectation compares are not competing subjects
|
|
90
|
+
# # good - both records build the one answer the example is about
|
|
91
|
+
# describe '.recent' do
|
|
92
|
+
# let!(:recent_order) { create(:order, created_at: 1.day.ago) }
|
|
93
|
+
# let!(:old_order) { create(:order, created_at: 1.year.ago) }
|
|
94
|
+
#
|
|
95
|
+
# it { expect(described_class.recent).to contain_exactly(recent_order) }
|
|
96
|
+
# end
|
|
97
|
+
class CompetingSubjects < RuboCop::Cop::RSpec::Base
|
|
98
|
+
include DescribedMethod
|
|
99
|
+
include ExpectationTarget
|
|
100
|
+
include PremiseTracking
|
|
101
|
+
|
|
102
|
+
MSG = 'The expectations here are about %<names>s, so this group has no one subject. Work out ' \
|
|
103
|
+
'what these definitions are to it, and let the structure say so.'
|
|
104
|
+
|
|
105
|
+
def on_block(node)
|
|
106
|
+
return unless example_group?(node)
|
|
107
|
+
|
|
108
|
+
names = competing_names(node)
|
|
109
|
+
|
|
110
|
+
return if names.size < 2
|
|
111
|
+
|
|
112
|
+
add_offense(node.send_node, message: format(MSG, names: names.join(', ')))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
alias on_numblock on_block
|
|
116
|
+
alias on_itblock on_block
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def competing_names(group)
|
|
121
|
+
return [] if declares_subject?(group)
|
|
122
|
+
|
|
123
|
+
asserted_definitions(group).reject { |name| exempt?(group, name) }.map { |name| "`:#{name}`" }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def exempt?(group, name)
|
|
127
|
+
return false if allowed_methods.empty?
|
|
128
|
+
|
|
129
|
+
definition = definition_named(group, name)
|
|
130
|
+
|
|
131
|
+
!definition.nil? && !call_in_chain(definition.body) do |call|
|
|
132
|
+
allowed_methods.include?(call.method_name.to_s)
|
|
133
|
+
end.nil?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def definition_named(group, name)
|
|
137
|
+
found = nil
|
|
138
|
+
|
|
139
|
+
each_child_block(group) do |child|
|
|
140
|
+
found ||= child if premise?(child) && premise_name(child)&.to_sym == name
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
found
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'mixin/premise_tracking'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Vicenzo
|
|
8
|
+
module RSpec
|
|
9
|
+
# Do not derive a premise from another premise.
|
|
10
|
+
#
|
|
11
|
+
# `let(:params) { base_params.merge(age: 10) }` does not mutate anything, but it splits a single premise
|
|
12
|
+
# across two definitions: to know what `params` holds in a context you have to read another `let` — usually
|
|
13
|
+
# one that exists only to be modified. Declare the complete value in the context that needs it.
|
|
14
|
+
#
|
|
15
|
+
# Only premise definitions (`let`, `let_it_be`, `subject`) are inspected, and only when the copy starts from
|
|
16
|
+
# another premise: deriving from a factory or from a literal is untouched.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# # bad
|
|
20
|
+
#
|
|
21
|
+
# let(:base_params) { { name: 'Ada' } }
|
|
22
|
+
# let(:params) { base_params.merge(age: 10) }
|
|
23
|
+
#
|
|
24
|
+
# # good
|
|
25
|
+
#
|
|
26
|
+
# context 'when the age is informed' do
|
|
27
|
+
# let(:params) { { name: 'Ada', age: 10 } }
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# context 'when the age is unknown' do
|
|
31
|
+
# let(:params) { { name: 'Ada' } }
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# # good — the source is a factory, not another premise
|
|
36
|
+
#
|
|
37
|
+
# let(:params) { attributes_for(:order).merge(value: 10) }
|
|
38
|
+
class DerivedPremises < RuboCop::Cop::RSpec::Base
|
|
39
|
+
include PremiseTracking
|
|
40
|
+
|
|
41
|
+
MSG = 'Do not derive the premise `%<name>s` from `%<source>s`. ' \
|
|
42
|
+
'Declare the complete value in the context that needs it.'
|
|
43
|
+
|
|
44
|
+
DERIVING_METHODS = %i[merge deep_merge reverse_merge dup clone except slice].freeze
|
|
45
|
+
|
|
46
|
+
def on_block(node)
|
|
47
|
+
return unless example_group?(node) && outermost_example_group?(node)
|
|
48
|
+
|
|
49
|
+
walk_example_group(node, all_premises(node))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
alias on_numblock on_block
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def on_premise(node, premises)
|
|
57
|
+
name = premise_name(node)
|
|
58
|
+
|
|
59
|
+
mutating_calls(node).each do |call|
|
|
60
|
+
next unless DERIVING_METHODS.include?(call.method_name)
|
|
61
|
+
|
|
62
|
+
source = premise_receiver(call, premises)
|
|
63
|
+
|
|
64
|
+
next if source.nil?
|
|
65
|
+
|
|
66
|
+
add_offense(call, message: format(MSG, name:, source:))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def on_setup_hook(node, premises); end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -14,7 +14,7 @@ module RuboCop
|
|
|
14
14
|
# @example
|
|
15
15
|
# # bad
|
|
16
16
|
#
|
|
17
|
-
# [:admin, :
|
|
17
|
+
# [:admin, :editor].each do |role|
|
|
18
18
|
# context "when role is #{role}" do
|
|
19
19
|
# it 'does something' do
|
|
20
20
|
# ...
|
|
@@ -32,8 +32,8 @@ module RuboCop
|
|
|
32
32
|
# end
|
|
33
33
|
# end
|
|
34
34
|
#
|
|
35
|
-
# context 'when role is
|
|
36
|
-
# let(:role) { :
|
|
35
|
+
# context 'when role is editor' do
|
|
36
|
+
# let(:role) { :editor }
|
|
37
37
|
#
|
|
38
38
|
# it 'does something' do
|
|
39
39
|
# ...
|
|
@@ -16,27 +16,27 @@ module RuboCop
|
|
|
16
16
|
# @example
|
|
17
17
|
# # bad — expect is called inside the iteration
|
|
18
18
|
#
|
|
19
|
-
# it 'returns
|
|
20
|
-
# response_body[:
|
|
21
|
-
# expect(value).to eq(
|
|
19
|
+
# it 'returns product prices general values' do
|
|
20
|
+
# response_body[:product_prices].first.each do |attribute, value|
|
|
21
|
+
# expect(value).to eq(product_price.send(attribute).to_s)
|
|
22
22
|
# end
|
|
23
23
|
# end
|
|
24
24
|
#
|
|
25
25
|
# # good — iteration builds data, expect is called once outside
|
|
26
26
|
#
|
|
27
27
|
# it 'returns the expected column names' do
|
|
28
|
-
# columns =
|
|
29
|
-
# expect(response_body[:
|
|
28
|
+
# columns = ProductPrice.column_names.map { |column| column.gsub('_cents', '') }
|
|
29
|
+
# expect(response_body[:product_prices].first.keys).to match_array(columns.map(&:to_sym))
|
|
30
30
|
# end
|
|
31
31
|
#
|
|
32
32
|
# # good — each attribute has an explicit example
|
|
33
33
|
#
|
|
34
34
|
# it 'returns the correct name' do
|
|
35
|
-
# expect(response_body[:
|
|
35
|
+
# expect(response_body[:product_prices].first[:name]).to eq(product_price.name)
|
|
36
36
|
# end
|
|
37
37
|
#
|
|
38
38
|
# it 'returns the correct value' do
|
|
39
|
-
# expect(response_body[:
|
|
39
|
+
# expect(response_body[:product_prices].first[:value]).to eq(product_price.value.to_s)
|
|
40
40
|
# end
|
|
41
41
|
class IterationInsideExample < RuboCop::Cop::RSpec::Base
|
|
42
42
|
MSG = 'Do not call `expect` inside an iteration. ' \
|