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 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
@@ -0,0 +1,7 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-07-29
4
+
5
+ - Initial release
6
+ - See [README](README.md) for context
7
+ - Releases with only [3 cops](docs/modules/ROOT/pages/cops_thoughtbot.adoc) to get started. Expect more as we iterate
data/CODEOWNERS ADDED
@@ -0,0 +1,4 @@
1
+ # Defines who is responsible for code in the codebase
2
+ # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
3
+
4
+ * @jaredlt
@@ -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
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "thoughtbot/no_before"
4
+ require_relative "thoughtbot/no_let"
5
+ require_relative "thoughtbot/resourceful_actions"
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Thoughtbot
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "thoughtbot/version"
4
+
5
+ module RuboCop
6
+ module Thoughtbot
7
+ class Error < StandardError; end
8
+ # Your code goes here...
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ require_relative "rubocop/thoughtbot"
6
+ require_relative "rubocop/thoughtbot/version"
7
+ require_relative "rubocop/thoughtbot/plugin"
8
+
9
+ require_relative "rubocop/cop/thoughtbot_cops"
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: []