rubocop-thoughtbot 0.1.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 +7 -0
- data/.tool-versions +1 -0
- data/BELIEFS.md +37 -0
- data/CHANGELOG.md +7 -0
- data/CODEOWNERS +4 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +157 -0
- data/Rakefile +63 -0
- data/config/default.yml +26 -0
- data/docs/modules/ROOT/pages/cops.adoc +5 -0
- data/docs/modules/ROOT/pages/cops_thoughtbot.adoc +201 -0
- data/lib/rubocop/cop/thoughtbot/no_before.rb +49 -0
- data/lib/rubocop/cop/thoughtbot/no_let.rb +49 -0
- data/lib/rubocop/cop/thoughtbot/resourceful_actions.rb +118 -0
- data/lib/rubocop/cop/thoughtbot_cops.rb +5 -0
- data/lib/rubocop/thoughtbot/plugin.rb +30 -0
- data/lib/rubocop/thoughtbot/version.rb +7 -0
- data/lib/rubocop/thoughtbot.rb +10 -0
- data/lib/rubocop-thoughtbot.rb +9 -0
- metadata +98 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 56b7014c878946a04e2619ecb8a50cd7e2cd60199c4c4e3ea4fd362ca601b3e0
|
|
4
|
+
data.tar.gz: 964048a771865a0092dd7461cfe3205bc8365818c62fd10302065fee2665f4c8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ce3ee7c537f213881a88c69798f2ffd282a7a124446b96034b2715b09b8491246399fc8a584ff0810e065f7928213ccb1a530441784922c1dcdfe9df865d6737
|
|
7
|
+
data.tar.gz: ff3e7287274ab79cc20953993a44bd3edd2c456b7b529156b7b2ac26a54b22ab152a00dc2b41757901200a003507f91be650b27c4c9f271ba759fd194fd54b34
|
data/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 4.0.6
|
data/BELIEFS.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Beliefs
|
|
2
|
+
|
|
3
|
+
This gem is a set of opinions about writing Ruby and Rails, but this isn't where
|
|
4
|
+
those live (each cop carries its own argument, in its documentation).
|
|
5
|
+
|
|
6
|
+
This is the layer above: what we believe about building the cops, and about the
|
|
7
|
+
experience of the person on the receiving end of one.
|
|
8
|
+
|
|
9
|
+
Every entry states a belief, then how it applies. If an entry can't produce a
|
|
10
|
+
practical half, it isn't a belief we hold yet, it's a preference still looking
|
|
11
|
+
for its justification.
|
|
12
|
+
|
|
13
|
+
## Offence messages teach
|
|
14
|
+
|
|
15
|
+
The reader may be meeting this convention for the first time, and the message
|
|
16
|
+
may be the only documentation they ever read. Editors truncate, CI logs scroll,
|
|
17
|
+
and nobody clicks through to a rule reference they didn't go looking for. A
|
|
18
|
+
message that assumes you already know the convention only helps the people who
|
|
19
|
+
didn't need it.
|
|
20
|
+
|
|
21
|
+
So a message has to stand on its own: name what is wrong here, say what to do
|
|
22
|
+
about it, and leave the reader somewhere to go for the reasoning.
|
|
23
|
+
|
|
24
|
+
Complete beats short. But a message is paid for once per offence, and these cops
|
|
25
|
+
can fire several times in a single file. Every clause has to earn its place on
|
|
26
|
+
the twentieth line of a CI log.
|
|
27
|
+
|
|
28
|
+
**In practice**
|
|
29
|
+
|
|
30
|
+
- Name the specific code, interpolated — `` `edit_password` ``, not "this method".
|
|
31
|
+
- State the remedy, not just the violation. Where there is more than one, list them.
|
|
32
|
+
- Give the why in a clause, where one fits.
|
|
33
|
+
- Link the reasoning. If there's no article justifying the cop, write the article
|
|
34
|
+
before writing the cop.
|
|
35
|
+
- Address the code, never the developer. "Avoid `let`", not "don't use `let`".
|
|
36
|
+
- Put worked examples in the cop's `@example` blocks. A message is one line and
|
|
37
|
+
can't hold them.
|
data/CHANGELOG.md
ADDED
data/CODEOWNERS
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"rubocop-thoughtbot" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["jared.turner@thoughtbot.com"](mailto:"jared.turner@thoughtbot.com").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jared Turner
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# RuboCop::Thoughtbot
|
|
2
|
+
|
|
3
|
+
A [RuboCop] and [Standard] plugin based on thoughtbot's accumulated best-practices.
|
|
4
|
+
|
|
5
|
+
[RuboCop]: https://rubocop.org
|
|
6
|
+
[Standard]: https://github.com/standardrb/standard
|
|
7
|
+
|
|
8
|
+
## Why?
|
|
9
|
+
|
|
10
|
+
We believe there is more that linting can solve. And it's not just style debates
|
|
11
|
+
about quotes or spaces.
|
|
12
|
+
|
|
13
|
+
- The mystery guest `let` that lives 150 lines from the spec example, causing
|
|
14
|
+
[confusion and hours lost debugging]
|
|
15
|
+
- The controller that grows an `activate` action, then a `publish`, until
|
|
16
|
+
[nothing about the file is predictable]
|
|
17
|
+
|
|
18
|
+
These are patterns we see repeated from human developers and AIs alike. For humans,
|
|
19
|
+
we solve this during code review, but slowly, inconsistently, and one developer
|
|
20
|
+
at a time. For AI, we put [guidance] into `CLAUDE.md` or `AGENTS.md`. But steering
|
|
21
|
+
an LLM is probabilistic, whereas a linter is deterministic. Linters let us move the
|
|
22
|
+
feedback loop left, into the developer's editor and into the [agent's hooks]. That's
|
|
23
|
+
what guides a codebase towards good practices and good architecture (the things that
|
|
24
|
+
keep it clean and easy to maintain in 2 years time).
|
|
25
|
+
|
|
26
|
+
A faster loop only helps if the feedback teaches. The person reading a message may
|
|
27
|
+
be meeting the convention for the first time, and it may be the only documentation
|
|
28
|
+
they ever read. A message that assumes the convention is already understood only
|
|
29
|
+
helps the people who didn't need it. So each cop carries its argument with it: the
|
|
30
|
+
message names what's wrong, says what to do about it, and links the reasoning.
|
|
31
|
+
|
|
32
|
+
[confusion and hours lost debugging]: https://thoughtbot.com/blog/lets-not
|
|
33
|
+
[nothing about the file is predictable]: https://thoughtbot.com/blog/in-relentless-pursuit-of-rest-ish-routing
|
|
34
|
+
[guidance]: https://github.com/thoughtbot/guides/tree/main/rails/ai-rules
|
|
35
|
+
[agent's hooks]: https://thoughtbot.com/blog/enforcing-your-ruby-style-guide-on-ai-generated-code
|
|
36
|
+
|
|
37
|
+
## What to expect
|
|
38
|
+
|
|
39
|
+
These cops are our opinions on clean code and maintainable software. The intention
|
|
40
|
+
is for you to layer these on top of Standard, or your own RuboCop config.
|
|
41
|
+
|
|
42
|
+
In time, we might be able to upstream some of them, but we're releasing a separate
|
|
43
|
+
Gem to give us the freedom to rapidly experiment and iterate across a wide range of
|
|
44
|
+
codebases first.
|
|
45
|
+
|
|
46
|
+
Linting works great for things that are deterministic, but there are plenty of best
|
|
47
|
+
practices where "it depends". Where a rule is _generally_ true we think the linting
|
|
48
|
+
guidance outweighs the occasional false positive (and you can exclude the files where
|
|
49
|
+
it genuinely doesn't apply). We won't always get this right, and a cop that turns out
|
|
50
|
+
to be more trouble than it's worth will be removed.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
This gem is not published to RubyGems yet, so install it from GitHub. Add it to
|
|
55
|
+
your Gemfile:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
group :development, :test do
|
|
59
|
+
gem "rubocop-thoughtbot", github: "thoughtbot/rubocop-thoughtbot", require: false
|
|
60
|
+
# Or via SSH while the repo is private
|
|
61
|
+
# gem "rubocop-thoughtbot", git: "git@github.com:thoughtbot/rubocop-thoughtbot.git", require: false
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then run `bundle install`.
|
|
66
|
+
|
|
67
|
+
Next, tell your linter to load the plugin.
|
|
68
|
+
|
|
69
|
+
### RuboCop
|
|
70
|
+
|
|
71
|
+
In `.rubocop.yml`:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
plugins:
|
|
75
|
+
- rubocop-thoughtbot
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Standard
|
|
79
|
+
|
|
80
|
+
In `.standard.yml`:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
plugins:
|
|
84
|
+
- rubocop-thoughtbot
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
Run your linter as usual and the cops are applied:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
bundle exec rubocop
|
|
93
|
+
# or
|
|
94
|
+
bundle exec standardrb
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Show me the cops!
|
|
98
|
+
|
|
99
|
+
All cops are located under [`lib/rubocop/cop/thoughtbot`](lib/rubocop/cop/thoughtbot),
|
|
100
|
+
or you can [view the docs](docs/modules/ROOT/pages/cops_thoughtbot.adoc).
|
|
101
|
+
|
|
102
|
+
### Excluding files
|
|
103
|
+
|
|
104
|
+
Each cop is a single opinion and doesn't take options (the knobs it might expose
|
|
105
|
+
would only let you turn down the argument it's making). Where a convention
|
|
106
|
+
genuinely doesn't apply, exclude the file rather than reaching for a setting. An
|
|
107
|
+
OmniAuth callbacks controller is the usual example: its actions are named by the
|
|
108
|
+
provider, so the `ResourcefulActions` cop isn't helpful.
|
|
109
|
+
|
|
110
|
+
In `.rubocop.yml`:
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
Thoughtbot/ResourcefulActions:
|
|
114
|
+
Exclude:
|
|
115
|
+
- "app/controllers/users/omniauth_callbacks_controller.rb"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
In `.standard.yml`, exclusions go under `ignore` rather than under the cop:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
plugins:
|
|
122
|
+
- rubocop-thoughtbot
|
|
123
|
+
ignore:
|
|
124
|
+
- 'app/controllers/users/omniauth_callbacks_controller.rb':
|
|
125
|
+
- Thoughtbot/ResourcefulActions
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
For a single occurrence rather than a whole file, use an inline disable comment:
|
|
129
|
+
|
|
130
|
+
`# rubocop:disable Thoughtbot/ResourcefulActions`, or `# standard:disable Thoughtbot/ResourcefulActions`
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
|
|
136
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
137
|
+
|
|
138
|
+
### Commands
|
|
139
|
+
|
|
140
|
+
- `bundle exec rake 'new_cop[Thoughtbot/CopName]'` # Generate new cop (always run this rather than creating the files directly)
|
|
141
|
+
- `bundle exec rspec` # Tests
|
|
142
|
+
- `bundle exec rubocop` # Lint
|
|
143
|
+
- `bundle exec rubocop -a` # Auto-fix lint issues
|
|
144
|
+
- `bundle exec rake docs` # Re-generate docs
|
|
145
|
+
- `bundle exec rake` # Run all: Specs, lint, docs
|
|
146
|
+
|
|
147
|
+
## Contributing
|
|
148
|
+
|
|
149
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rubocop-thoughtbot. 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/[USERNAME]/rubocop-thoughtbot/blob/main/CODE_OF_CONDUCT.md).
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
154
|
+
|
|
155
|
+
## Code of Conduct
|
|
156
|
+
|
|
157
|
+
Everyone interacting in the Rubocop::Thoughtbot project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/rubocop-thoughtbot/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require "rubocop/rake_task"
|
|
6
|
+
require "yard"
|
|
7
|
+
|
|
8
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
|
9
|
+
task.verbose = false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
RuboCop::RakeTask.new
|
|
13
|
+
|
|
14
|
+
task default: %i[spec rubocop verify_docs]
|
|
15
|
+
|
|
16
|
+
YARD::Rake::YardocTask.new(:yard_registry) do |task|
|
|
17
|
+
task.files = ["lib/rubocop/cop/**/*.rb"]
|
|
18
|
+
task.options = ["--no-output", "--no-progress", "--no-stats"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc "Generate cop documentation from the cops' YARD comments"
|
|
22
|
+
task docs: :yard_registry do
|
|
23
|
+
require "rubocop"
|
|
24
|
+
require "rubocop/cops_documentation_generator"
|
|
25
|
+
|
|
26
|
+
CopsDocumentationGenerator.new(
|
|
27
|
+
departments: ["Thoughtbot"],
|
|
28
|
+
plugin_name: "rubocop-thoughtbot"
|
|
29
|
+
).call
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc "Check the committed cop documentation still matches the cops"
|
|
33
|
+
task verify_docs: :docs do
|
|
34
|
+
drift = `git status --porcelain -- docs`
|
|
35
|
+
|
|
36
|
+
unless drift.empty?
|
|
37
|
+
abort <<~MESSAGE
|
|
38
|
+
Generated cop documentation is out of date:
|
|
39
|
+
|
|
40
|
+
#{drift}
|
|
41
|
+
`rake docs` has already regenerated it. Review and commit `docs/`.
|
|
42
|
+
MESSAGE
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "Generate a new cop with a template"
|
|
47
|
+
task :new_cop, [:cop] do |_task, args|
|
|
48
|
+
require "rubocop"
|
|
49
|
+
|
|
50
|
+
cop_name = args.fetch(:cop) do
|
|
51
|
+
warn "usage: bundle exec rake new_cop[Department/Name]"
|
|
52
|
+
exit!
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
generator = RuboCop::Cop::Generator.new(cop_name)
|
|
56
|
+
|
|
57
|
+
generator.write_source
|
|
58
|
+
generator.write_spec
|
|
59
|
+
generator.inject_require(root_file_path: "lib/rubocop/cop/thoughtbot_cops.rb")
|
|
60
|
+
generator.inject_config(config_file_path: "config/default.yml")
|
|
61
|
+
|
|
62
|
+
puts generator.todo
|
|
63
|
+
end
|
data/config/default.yml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Thoughtbot:
|
|
2
|
+
DocumentationBaseURL: https://github.com/thoughtbot/rubocop-thoughtbot/blob/main/docs/modules/ROOT/pages
|
|
3
|
+
DocumentationExtension: '.adoc'
|
|
4
|
+
|
|
5
|
+
Thoughtbot/NoBefore:
|
|
6
|
+
Description: 'Checks for `before` hooks in specs.'
|
|
7
|
+
Enabled: true
|
|
8
|
+
VersionAdded: '0.1.0'
|
|
9
|
+
Include:
|
|
10
|
+
- '**/*_spec.rb'
|
|
11
|
+
- '**/spec/**/*.rb'
|
|
12
|
+
|
|
13
|
+
Thoughtbot/NoLet:
|
|
14
|
+
Description: 'Checks for `let` and `let!` declarations in specs.'
|
|
15
|
+
Enabled: true
|
|
16
|
+
VersionAdded: '0.1.0'
|
|
17
|
+
Include:
|
|
18
|
+
- '**/*_spec.rb'
|
|
19
|
+
- '**/spec/**/*.rb'
|
|
20
|
+
|
|
21
|
+
Thoughtbot/ResourcefulActions:
|
|
22
|
+
Description: 'Checks for public methods in Rails controllers outside the seven resourceful actions.'
|
|
23
|
+
Enabled: true
|
|
24
|
+
VersionAdded: '0.1.0'
|
|
25
|
+
Include:
|
|
26
|
+
- '**/app/controllers/**/*.rb'
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
=== Department xref:cops_thoughtbot.adoc[Thoughtbot]
|
|
2
|
+
|
|
3
|
+
* xref:cops_thoughtbot.adoc#thoughtbotnobefore[Thoughtbot/NoBefore]
|
|
4
|
+
* xref:cops_thoughtbot.adoc#thoughtbotnolet[Thoughtbot/NoLet]
|
|
5
|
+
* xref:cops_thoughtbot.adoc#thoughtbotresourcefulactions[Thoughtbot/ResourcefulActions]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
////
|
|
2
|
+
Do NOT edit this file by hand directly, as it is automatically generated.
|
|
3
|
+
|
|
4
|
+
Please make any necessary changes to the cop documentation within the source files themselves.
|
|
5
|
+
////
|
|
6
|
+
|
|
7
|
+
= Thoughtbot
|
|
8
|
+
|
|
9
|
+
[#thoughtbotnobefore]
|
|
10
|
+
== Thoughtbot/NoBefore
|
|
11
|
+
|
|
12
|
+
|===
|
|
13
|
+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
|
|
14
|
+
|
|
15
|
+
| Enabled
|
|
16
|
+
| Yes
|
|
17
|
+
| No
|
|
18
|
+
| 0.1.0
|
|
19
|
+
| -
|
|
20
|
+
|===
|
|
21
|
+
|
|
22
|
+
Checks for `before` hooks in specs.
|
|
23
|
+
|
|
24
|
+
Setup in a `before` sits away from the examples that use it, so a
|
|
25
|
+
reader has to jump around the file to work out what any one example is
|
|
26
|
+
actually doing (the `before` becomes a mystery guest). Setting data up
|
|
27
|
+
inside each example keeps the whole story of the test in one place,
|
|
28
|
+
avoids messy overrides for differing scenarios, and keeps the cost of
|
|
29
|
+
setup visible.
|
|
30
|
+
|
|
31
|
+
See https://thoughtbot.com/blog/lets-not
|
|
32
|
+
|
|
33
|
+
See https://thoughtbot.com/blog/the-arrange-act-assert-pattern
|
|
34
|
+
|
|
35
|
+
[#examples-thoughtbotnobefore]
|
|
36
|
+
=== Examples
|
|
37
|
+
|
|
38
|
+
[source,ruby]
|
|
39
|
+
----
|
|
40
|
+
# bad
|
|
41
|
+
before { @user = build(:user) }
|
|
42
|
+
|
|
43
|
+
it "is valid" do
|
|
44
|
+
expect(@user).to be_valid
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# good
|
|
48
|
+
it "is valid" do
|
|
49
|
+
user = build(:user)
|
|
50
|
+
|
|
51
|
+
expect(user).to be_valid
|
|
52
|
+
end
|
|
53
|
+
----
|
|
54
|
+
|
|
55
|
+
[#configurable-attributes-thoughtbotnobefore]
|
|
56
|
+
=== Configurable attributes
|
|
57
|
+
|
|
58
|
+
|===
|
|
59
|
+
| Name | Default value | Configurable values
|
|
60
|
+
|
|
61
|
+
| Include
|
|
62
|
+
| `+**/*_spec.rb+`, `+**/spec/**/*.rb+`
|
|
63
|
+
| Array
|
|
64
|
+
|===
|
|
65
|
+
|
|
66
|
+
[#thoughtbotnolet]
|
|
67
|
+
== Thoughtbot/NoLet
|
|
68
|
+
|
|
69
|
+
|===
|
|
70
|
+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
|
|
71
|
+
|
|
72
|
+
| Enabled
|
|
73
|
+
| Yes
|
|
74
|
+
| No
|
|
75
|
+
| 0.1.0
|
|
76
|
+
| -
|
|
77
|
+
|===
|
|
78
|
+
|
|
79
|
+
Checks for `let` and `let!` declarations in specs.
|
|
80
|
+
|
|
81
|
+
Data defined in a `let` sits away from the examples that use it, so a
|
|
82
|
+
reader has to jump around the file to work out what any one example is
|
|
83
|
+
actually doing (the `let` becomes a mystery guest). Setting data up
|
|
84
|
+
inside each example keeps the whole story of the test in one place,
|
|
85
|
+
avoids messy overrides for differing scenarios, and keeps the cost of
|
|
86
|
+
setup visible.
|
|
87
|
+
|
|
88
|
+
See https://thoughtbot.com/blog/lets-not
|
|
89
|
+
|
|
90
|
+
See https://thoughtbot.com/blog/the-arrange-act-assert-pattern
|
|
91
|
+
|
|
92
|
+
[#examples-thoughtbotnolet]
|
|
93
|
+
=== Examples
|
|
94
|
+
|
|
95
|
+
[source,ruby]
|
|
96
|
+
----
|
|
97
|
+
# bad
|
|
98
|
+
let(:user) { build(:user) }
|
|
99
|
+
|
|
100
|
+
it "is valid" do
|
|
101
|
+
expect(user).to be_valid
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# good
|
|
105
|
+
it "is valid" do
|
|
106
|
+
user = build(:user)
|
|
107
|
+
|
|
108
|
+
expect(user).to be_valid
|
|
109
|
+
end
|
|
110
|
+
----
|
|
111
|
+
|
|
112
|
+
[#configurable-attributes-thoughtbotnolet]
|
|
113
|
+
=== Configurable attributes
|
|
114
|
+
|
|
115
|
+
|===
|
|
116
|
+
| Name | Default value | Configurable values
|
|
117
|
+
|
|
118
|
+
| Include
|
|
119
|
+
| `+**/*_spec.rb+`, `+**/spec/**/*.rb+`
|
|
120
|
+
| Array
|
|
121
|
+
|===
|
|
122
|
+
|
|
123
|
+
[#thoughtbotresourcefulactions]
|
|
124
|
+
== Thoughtbot/ResourcefulActions
|
|
125
|
+
|
|
126
|
+
|===
|
|
127
|
+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
|
|
128
|
+
|
|
129
|
+
| Enabled
|
|
130
|
+
| Yes
|
|
131
|
+
| No
|
|
132
|
+
| 0.1.0
|
|
133
|
+
| -
|
|
134
|
+
|===
|
|
135
|
+
|
|
136
|
+
Checks for public methods in Rails controllers outside the seven
|
|
137
|
+
resourceful actions: `index`, `show`, `new`, `edit`, `create`,
|
|
138
|
+
`update` and `destroy`.
|
|
139
|
+
|
|
140
|
+
A custom action needs a custom route, and custom routes pick their
|
|
141
|
+
verbs ad hoc (`update-password`, `add-payment-method`, `activate`)
|
|
142
|
+
so no two read the same way. Sticking to the seven constrains you to
|
|
143
|
+
the standard HTTP verbs and pushes the naming into nouns, which are
|
|
144
|
+
far less ambiguous than invented verbs. It also keeps controllers
|
|
145
|
+
small, since the fix for a new action is a new controller.
|
|
146
|
+
|
|
147
|
+
Rails routes any public instance method on a controller, so this cop
|
|
148
|
+
also flags helpers left public. Those should be private.
|
|
149
|
+
|
|
150
|
+
Controllers dictated by a gem (`Devise::OmniauthCallbacksController`
|
|
151
|
+
with an action per provider, or Doorkeeper's OAuth endpoints) can't
|
|
152
|
+
follow the convention. Exclude them:
|
|
153
|
+
|
|
154
|
+
Thoughtbot/ResourcefulActions:
|
|
155
|
+
Exclude:
|
|
156
|
+
- "app/controllers/users/omniauth_callbacks_controller.rb"
|
|
157
|
+
|
|
158
|
+
See https://thoughtbot.com/blog/in-relentless-pursuit-of-rest-ish-routing
|
|
159
|
+
|
|
160
|
+
[#examples-thoughtbotresourcefulactions]
|
|
161
|
+
=== Examples
|
|
162
|
+
|
|
163
|
+
[source,ruby]
|
|
164
|
+
----
|
|
165
|
+
# bad
|
|
166
|
+
class UsersController < ApplicationController
|
|
167
|
+
def activate
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# good
|
|
172
|
+
class Users::ActivationsController < ApplicationController
|
|
173
|
+
def create
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# bad
|
|
178
|
+
class ApplicationController < ActionController::Base
|
|
179
|
+
def current_user
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# good
|
|
184
|
+
class ApplicationController < ActionController::Base
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def current_user
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
----
|
|
191
|
+
|
|
192
|
+
[#configurable-attributes-thoughtbotresourcefulactions]
|
|
193
|
+
=== Configurable attributes
|
|
194
|
+
|
|
195
|
+
|===
|
|
196
|
+
| Name | Default value | Configurable values
|
|
197
|
+
|
|
198
|
+
| Include
|
|
199
|
+
| `+**/app/controllers/**/*.rb+`
|
|
200
|
+
| Array
|
|
201
|
+
|===
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Thoughtbot
|
|
6
|
+
# Checks for `before` hooks in specs.
|
|
7
|
+
#
|
|
8
|
+
# Setup in a `before` sits away from the examples that use it, so a
|
|
9
|
+
# reader has to jump around the file to work out what any one example is
|
|
10
|
+
# actually doing (the `before` becomes a mystery guest). Setting data up
|
|
11
|
+
# inside each example keeps the whole story of the test in one place,
|
|
12
|
+
# avoids messy overrides for differing scenarios, and keeps the cost of
|
|
13
|
+
# setup visible.
|
|
14
|
+
#
|
|
15
|
+
# See https://thoughtbot.com/blog/lets-not
|
|
16
|
+
#
|
|
17
|
+
# See https://thoughtbot.com/blog/the-arrange-act-assert-pattern
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# # bad
|
|
21
|
+
# before { @user = build(:user) }
|
|
22
|
+
#
|
|
23
|
+
# it "is valid" do
|
|
24
|
+
# expect(@user).to be_valid
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # good
|
|
28
|
+
# it "is valid" do
|
|
29
|
+
# user = build(:user)
|
|
30
|
+
#
|
|
31
|
+
# expect(user).to be_valid
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
class NoBefore < Base
|
|
35
|
+
MSG = "Avoid `before` — set up test data inside each example so it " \
|
|
36
|
+
"doesn't become a mystery guest. See https://thoughtbot.com/blog/lets-not"
|
|
37
|
+
|
|
38
|
+
RESTRICT_ON_SEND = %i[before].freeze
|
|
39
|
+
|
|
40
|
+
def on_send(node)
|
|
41
|
+
return if node.receiver
|
|
42
|
+
|
|
43
|
+
add_offense(node.loc.selector)
|
|
44
|
+
end
|
|
45
|
+
alias_method :on_csend, :on_send
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Thoughtbot
|
|
6
|
+
# Checks for `let` and `let!` declarations in specs.
|
|
7
|
+
#
|
|
8
|
+
# Data defined in a `let` sits away from the examples that use it, so a
|
|
9
|
+
# reader has to jump around the file to work out what any one example is
|
|
10
|
+
# actually doing (the `let` becomes a mystery guest). Setting data up
|
|
11
|
+
# inside each example keeps the whole story of the test in one place,
|
|
12
|
+
# avoids messy overrides for differing scenarios, and keeps the cost of
|
|
13
|
+
# setup visible.
|
|
14
|
+
#
|
|
15
|
+
# See https://thoughtbot.com/blog/lets-not
|
|
16
|
+
#
|
|
17
|
+
# See https://thoughtbot.com/blog/the-arrange-act-assert-pattern
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# # bad
|
|
21
|
+
# let(:user) { build(:user) }
|
|
22
|
+
#
|
|
23
|
+
# it "is valid" do
|
|
24
|
+
# expect(user).to be_valid
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # good
|
|
28
|
+
# it "is valid" do
|
|
29
|
+
# user = build(:user)
|
|
30
|
+
#
|
|
31
|
+
# expect(user).to be_valid
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
class NoLet < Base
|
|
35
|
+
MSG = "Avoid `%<method>s` — set up test data inside each example so it " \
|
|
36
|
+
"doesn't become a mystery guest. See https://thoughtbot.com/blog/lets-not"
|
|
37
|
+
|
|
38
|
+
RESTRICT_ON_SEND = %i[let let!].freeze
|
|
39
|
+
|
|
40
|
+
def on_send(node)
|
|
41
|
+
return if node.receiver
|
|
42
|
+
|
|
43
|
+
add_offense(node.loc.selector, message: format(MSG, method: node.method_name))
|
|
44
|
+
end
|
|
45
|
+
alias_method :on_csend, :on_send
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Thoughtbot
|
|
6
|
+
# Checks for public methods in Rails controllers outside the seven
|
|
7
|
+
# resourceful actions: `index`, `show`, `new`, `edit`, `create`,
|
|
8
|
+
# `update` and `destroy`.
|
|
9
|
+
#
|
|
10
|
+
# A custom action needs a custom route, and custom routes pick their
|
|
11
|
+
# verbs ad hoc (`update-password`, `add-payment-method`, `activate`)
|
|
12
|
+
# so no two read the same way. Sticking to the seven constrains you to
|
|
13
|
+
# the standard HTTP verbs and pushes the naming into nouns, which are
|
|
14
|
+
# far less ambiguous than invented verbs. It also keeps controllers
|
|
15
|
+
# small, since the fix for a new action is a new controller.
|
|
16
|
+
#
|
|
17
|
+
# Rails routes any public instance method on a controller, so this cop
|
|
18
|
+
# also flags helpers left public. Those should be private.
|
|
19
|
+
#
|
|
20
|
+
# Controllers dictated by a gem (`Devise::OmniauthCallbacksController`
|
|
21
|
+
# with an action per provider, or Doorkeeper's OAuth endpoints) can't
|
|
22
|
+
# follow the convention. Exclude them:
|
|
23
|
+
#
|
|
24
|
+
# Thoughtbot/ResourcefulActions:
|
|
25
|
+
# Exclude:
|
|
26
|
+
# - "app/controllers/users/omniauth_callbacks_controller.rb"
|
|
27
|
+
#
|
|
28
|
+
# See https://thoughtbot.com/blog/in-relentless-pursuit-of-rest-ish-routing
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# # bad
|
|
32
|
+
# class UsersController < ApplicationController
|
|
33
|
+
# def activate
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# # good
|
|
38
|
+
# class Users::ActivationsController < ApplicationController
|
|
39
|
+
# def create
|
|
40
|
+
# end
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# # bad
|
|
44
|
+
# class ApplicationController < ActionController::Base
|
|
45
|
+
# def current_user
|
|
46
|
+
# end
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# # good
|
|
50
|
+
# class ApplicationController < ActionController::Base
|
|
51
|
+
# private
|
|
52
|
+
#
|
|
53
|
+
# def current_user
|
|
54
|
+
# end
|
|
55
|
+
# end
|
|
56
|
+
#
|
|
57
|
+
class ResourcefulActions < Base
|
|
58
|
+
MSG = "`%<name>s` is not one of the seven resourceful actions (index, " \
|
|
59
|
+
"show, new, edit, create, update, destroy) — rename it, extract " \
|
|
60
|
+
"another controller, or make it private if it isn't an action. " \
|
|
61
|
+
"See https://thoughtbot.com/blog/in-relentless-pursuit-of-rest-ish-routing"
|
|
62
|
+
|
|
63
|
+
RESOURCEFUL_ACTIONS = %i[index show new edit create update destroy].freeze
|
|
64
|
+
VISIBILITY_MODIFIERS = %i[public protected private].freeze
|
|
65
|
+
|
|
66
|
+
def on_class(node)
|
|
67
|
+
return unless controller?(node)
|
|
68
|
+
return unless node.body
|
|
69
|
+
|
|
70
|
+
non_resourceful_actions(node.body).each do |action|
|
|
71
|
+
add_offense(action.loc.name, message: format(MSG, name: action.method_name))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def controller?(node)
|
|
78
|
+
node.identifier.short_name.to_s.end_with?("Controller")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def non_resourceful_actions(body)
|
|
82
|
+
visibility = :public
|
|
83
|
+
public_defs = []
|
|
84
|
+
hidden = []
|
|
85
|
+
|
|
86
|
+
body_children(body).each do |child|
|
|
87
|
+
if visibility_modifier?(child)
|
|
88
|
+
child.arguments.empty? ? visibility = child.method_name : hidden.concat(hidden_names(child))
|
|
89
|
+
elsif child.def_type? && visibility == :public
|
|
90
|
+
public_defs << child
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
public_defs.reject do |definition|
|
|
95
|
+
RESOURCEFUL_ACTIONS.include?(definition.method_name) || hidden.include?(definition.method_name)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def body_children(body)
|
|
100
|
+
body.begin_type? ? body.children : [body]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def visibility_modifier?(node)
|
|
104
|
+
node.send_type? && node.receiver.nil? && VISIBILITY_MODIFIERS.include?(node.method_name)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# `private :current_user` hides an already-defined method. An inline
|
|
108
|
+
# `private def foo` passes a `def` rather than a symbol, and never
|
|
109
|
+
# reaches us as a body child anyway.
|
|
110
|
+
def hidden_names(node)
|
|
111
|
+
return [] if node.method?(:public)
|
|
112
|
+
|
|
113
|
+
node.arguments.select(&:sym_type?).map(&:value)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lint_roller"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Thoughtbot
|
|
7
|
+
class Plugin < LintRoller::Plugin
|
|
8
|
+
def about
|
|
9
|
+
LintRoller::About.new(
|
|
10
|
+
name: "rubocop-thoughtbot",
|
|
11
|
+
version: VERSION,
|
|
12
|
+
homepage: "https://github.com/thoughtbot/rubocop-thoughtbot",
|
|
13
|
+
description: "A RuboCop and Standard plugin based on thoughtbot's accumulated best-practices."
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def supported?(context)
|
|
18
|
+
context.engine == :rubocop
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def rules(_context)
|
|
22
|
+
LintRoller::Rules.new(
|
|
23
|
+
type: :path,
|
|
24
|
+
config_format: :rubocop,
|
|
25
|
+
value: Pathname.new(__dir__).join("../../../config/default.yml")
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rubocop-thoughtbot
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jared Turner
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: lint_roller
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rubocop
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.72.2
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '2'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: 1.72.2
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2'
|
|
46
|
+
description: A RuboCop Plugin based on thoughtbot's accumulated best-practices.
|
|
47
|
+
email:
|
|
48
|
+
- jared.turner@thoughtbot.com
|
|
49
|
+
executables: []
|
|
50
|
+
extensions: []
|
|
51
|
+
extra_rdoc_files: []
|
|
52
|
+
files:
|
|
53
|
+
- ".tool-versions"
|
|
54
|
+
- BELIEFS.md
|
|
55
|
+
- CHANGELOG.md
|
|
56
|
+
- CODEOWNERS
|
|
57
|
+
- CODE_OF_CONDUCT.md
|
|
58
|
+
- LICENSE.txt
|
|
59
|
+
- README.md
|
|
60
|
+
- Rakefile
|
|
61
|
+
- config/default.yml
|
|
62
|
+
- docs/modules/ROOT/pages/cops.adoc
|
|
63
|
+
- docs/modules/ROOT/pages/cops_thoughtbot.adoc
|
|
64
|
+
- lib/rubocop-thoughtbot.rb
|
|
65
|
+
- lib/rubocop/cop/thoughtbot/no_before.rb
|
|
66
|
+
- lib/rubocop/cop/thoughtbot/no_let.rb
|
|
67
|
+
- lib/rubocop/cop/thoughtbot/resourceful_actions.rb
|
|
68
|
+
- lib/rubocop/cop/thoughtbot_cops.rb
|
|
69
|
+
- lib/rubocop/thoughtbot.rb
|
|
70
|
+
- lib/rubocop/thoughtbot/plugin.rb
|
|
71
|
+
- lib/rubocop/thoughtbot/version.rb
|
|
72
|
+
homepage: http://github.com/thoughtbot/rubocop-thoughtbot
|
|
73
|
+
licenses:
|
|
74
|
+
- MIT
|
|
75
|
+
metadata:
|
|
76
|
+
homepage_uri: http://github.com/thoughtbot/rubocop-thoughtbot
|
|
77
|
+
source_code_uri: http://github.com/thoughtbot/rubocop-thoughtbot
|
|
78
|
+
changelog_uri: http://github.com/thoughtbot/rubocop-thoughtbot/blob/main/CHANGELOG.md
|
|
79
|
+
rubygems_mfa_required: 'true'
|
|
80
|
+
default_lint_roller_plugin: RuboCop::Thoughtbot::Plugin
|
|
81
|
+
rdoc_options: []
|
|
82
|
+
require_paths:
|
|
83
|
+
- lib
|
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 3.0.0
|
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0'
|
|
94
|
+
requirements: []
|
|
95
|
+
rubygems_version: 4.0.16
|
|
96
|
+
specification_version: 4
|
|
97
|
+
summary: A RuboCop Plugin based on thoughtbot's accumulated best-practices.
|
|
98
|
+
test_files: []
|