rubocop-guardrails 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/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +29 -0
- data/config/default.yml +18 -0
- data/lib/rubocop/cop/guardrails/no_service_objects.rb +35 -0
- data/lib/rubocop/cop/guardrails/restful_actions.rb +71 -0
- data/lib/rubocop/cop/guardrails/restful_routes.rb +68 -0
- data/lib/rubocop/cop/guardrails_cops.rb +5 -0
- data/lib/rubocop/guardrails/plugin.rb +31 -0
- data/lib/rubocop/guardrails/version.rb +7 -0
- data/lib/rubocop/guardrails.rb +9 -0
- data/lib/rubocop-guardrails.rb +9 -0
- metadata +86 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d8c264ff422c6fd3a97c216a8473d3a267c20e141f3ba5a7afba72ae37d06ed7
|
|
4
|
+
data.tar.gz: 99a6cfaa8055b4616badb1a37feb9e4882ca40491fbc175fe730ee58e453c6de
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9d7da48d6b71734a2db38215c9b9e443fd9232cc44b2b71d8f3af62013139a44a6fef26903795ecc1a12a77e639e7ba6d680dda8526cf6d6961f8522c8418dbe
|
|
7
|
+
data.tar.gz: 2e9828ef7340c968362b26088072752765dce52c391fa0162bccec8698a65a4cc6cb65419515064c1018ff446236f6a094598e2817e13691b2da244133223c86
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carl Dawson
|
|
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,45 @@
|
|
|
1
|
+
# rubocop-guardrails
|
|
2
|
+
|
|
3
|
+
RuboCop cops that keep Rails app on the golden path.
|
|
4
|
+
|
|
5
|
+
Rails is a convention-over-configuration framework, but it's easy to drift — especially when AI coding agents are involved. Agents pull from a broad training set and love to introduce service objects, decorators, form objects, and other patterns that aren't advocated for by the framework itself. These cops push back, keeping your app conventional: rich models, RESTful controllers, shallow jobs, and nothing in between.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add it to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "rubocop-guardrails", require: false
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### RuboCop plugin system (>= 1.72)
|
|
18
|
+
|
|
19
|
+
Add to your `.rubocop.yml`:
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
plugins:
|
|
23
|
+
- rubocop-guardrails
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Legacy
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
require:
|
|
30
|
+
- rubocop-guardrails
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Development
|
|
34
|
+
|
|
35
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
|
36
|
+
|
|
37
|
+
To generate a new cop:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bundle exec rake 'new_cop[Guardrails/CopName]'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
require 'rubocop/rake_task'
|
|
6
|
+
|
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
8
|
+
RuboCop::RakeTask.new
|
|
9
|
+
|
|
10
|
+
task default: %i[spec rubocop]
|
|
11
|
+
|
|
12
|
+
desc 'Generate a new cop with a template'
|
|
13
|
+
task :new_cop, [:cop] do |_task, args|
|
|
14
|
+
require 'rubocop'
|
|
15
|
+
|
|
16
|
+
cop_name = args.fetch(:cop) do
|
|
17
|
+
warn 'usage: bundle exec rake new_cop[Department/Name]'
|
|
18
|
+
exit!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
generator = RuboCop::Cop::Generator.new(cop_name)
|
|
22
|
+
|
|
23
|
+
generator.write_source
|
|
24
|
+
generator.write_spec
|
|
25
|
+
generator.inject_require(root_file_path: 'lib/rubocop/cop/guardrails_cops.rb')
|
|
26
|
+
generator.inject_config(config_file_path: 'config/default.yml')
|
|
27
|
+
|
|
28
|
+
puts generator.todo
|
|
29
|
+
end
|
data/config/default.yml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Guardrails/NoServiceObjects:
|
|
2
|
+
Enabled: true
|
|
3
|
+
Description: 'Disallows service objects.'
|
|
4
|
+
Include:
|
|
5
|
+
- 'app/services/**/*.rb'
|
|
6
|
+
|
|
7
|
+
Guardrails/RestfulActions:
|
|
8
|
+
Enabled: true
|
|
9
|
+
Description: 'Prevents non-RESTful controller actions.'
|
|
10
|
+
Include:
|
|
11
|
+
- 'app/controllers/**/*.rb'
|
|
12
|
+
|
|
13
|
+
Guardrails/RestfulRoutes:
|
|
14
|
+
Enabled: true
|
|
15
|
+
Description: 'Detects non-RESTful route definitions.'
|
|
16
|
+
Include:
|
|
17
|
+
- 'config/routes.rb'
|
|
18
|
+
- 'config/routes/**/*.rb'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Guardrails
|
|
6
|
+
# Disallows service objects.
|
|
7
|
+
#
|
|
8
|
+
# Service objects are a common pattern but they aren't the Rails way.
|
|
9
|
+
# Business logic belongs in your models — ActiveRecord models and
|
|
10
|
+
# plain old Ruby objects that live in `app/models`.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad - app/services/post_publisher.rb
|
|
14
|
+
# class PostPublisher
|
|
15
|
+
# def call
|
|
16
|
+
# post.update!(published: true)
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # good - logic belongs in the model
|
|
21
|
+
# class Post < ApplicationRecord
|
|
22
|
+
# def publish
|
|
23
|
+
# update!(published: true)
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
class NoServiceObjects < Base
|
|
27
|
+
MSG = 'Avoid service objects. This logic belongs in your models.'
|
|
28
|
+
|
|
29
|
+
def on_class(node)
|
|
30
|
+
add_offense(node.loc.name)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Guardrails
|
|
6
|
+
# Prevents non-RESTful actions in controllers.
|
|
7
|
+
#
|
|
8
|
+
# Rails advocates for RESTful resources. When you need a custom
|
|
9
|
+
# action, extract a new controller with standard RESTful actions.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad
|
|
13
|
+
# class PostsController < ApplicationController
|
|
14
|
+
# def publish
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # good - extract a new resource
|
|
19
|
+
# class Posts::PublicationsController < ApplicationController
|
|
20
|
+
# def create
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
class RestfulActions < Base
|
|
24
|
+
MSG = 'Non-RESTful action `%<method>s`. Add a new resource to represent this action.'
|
|
25
|
+
|
|
26
|
+
RESTFUL_ACTIONS = %i[index show new create edit update destroy].to_set.freeze
|
|
27
|
+
|
|
28
|
+
def on_def(node)
|
|
29
|
+
return if RESTFUL_ACTIONS.include?(node.method_name)
|
|
30
|
+
return unless in_class?(node)
|
|
31
|
+
return unless public_method?(node)
|
|
32
|
+
|
|
33
|
+
add_offense(node.loc.name, message: format(MSG, method: node.method_name))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def in_class?(node)
|
|
39
|
+
node.each_ancestor(:class).any?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def public_method?(node)
|
|
43
|
+
return false if inline_visibility_modifier?(node)
|
|
44
|
+
|
|
45
|
+
effective_visibility(node) == :public
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def inline_visibility_modifier?(node)
|
|
49
|
+
node.parent&.send_type? &&
|
|
50
|
+
%i[private protected].include?(node.parent.method_name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def effective_visibility(node)
|
|
54
|
+
body = node.parent
|
|
55
|
+
return :public unless body&.begin_type?
|
|
56
|
+
|
|
57
|
+
body.children
|
|
58
|
+
.take_while { |child| !child.equal?(node) }
|
|
59
|
+
.select { |child| visibility_modifier?(child) }
|
|
60
|
+
.last&.method_name || :public
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def visibility_modifier?(node)
|
|
64
|
+
node.send_type? &&
|
|
65
|
+
node.arguments.empty? &&
|
|
66
|
+
%i[private protected public].include?(node.method_name)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Guardrails
|
|
6
|
+
# Detects non-RESTful route definitions.
|
|
7
|
+
#
|
|
8
|
+
# Routes should use `resources` and `resource` exclusively. When you
|
|
9
|
+
# need a custom action, extract a new resource rather than adding
|
|
10
|
+
# bare HTTP verb routes or `member`/`collection` blocks.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# get '/posts/:id/publish', to: 'posts#publish'
|
|
15
|
+
#
|
|
16
|
+
# resources :posts do
|
|
17
|
+
# member do
|
|
18
|
+
# get :publish
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# # good - extract a new resource
|
|
23
|
+
# resources :posts do
|
|
24
|
+
# resource :publication, only: :create
|
|
25
|
+
# end
|
|
26
|
+
class RestfulRoutes < Base
|
|
27
|
+
MSG_VERB = 'Use `resources` or `resource` instead of bare HTTP verb routes.'
|
|
28
|
+
MSG_MEMBER = 'Use a new `resources` instead of `member` routes.'
|
|
29
|
+
MSG_COLLECTION = 'Use a new `resources` instead of `collection` routes.'
|
|
30
|
+
|
|
31
|
+
HTTP_VERBS = %i[get post put patch delete match].to_set.freeze
|
|
32
|
+
|
|
33
|
+
RESTRICT_ON_SEND = [*HTTP_VERBS, :member, :collection].freeze
|
|
34
|
+
|
|
35
|
+
# @!method route_draw_block?(node)
|
|
36
|
+
def_node_matcher :route_draw_block?, <<~PATTERN
|
|
37
|
+
(block (send (send (send (const nil? :Rails) :application) :routes) :draw) ...)
|
|
38
|
+
PATTERN
|
|
39
|
+
|
|
40
|
+
def on_send(node)
|
|
41
|
+
return unless inside_routes_draw?(node)
|
|
42
|
+
|
|
43
|
+
if HTTP_VERBS.include?(node.method_name)
|
|
44
|
+
return if inside_member_or_collection?(node)
|
|
45
|
+
|
|
46
|
+
add_offense(node.loc.selector, message: MSG_VERB)
|
|
47
|
+
elsif node.method_name == :member
|
|
48
|
+
add_offense(node.loc.selector, message: MSG_MEMBER)
|
|
49
|
+
elsif node.method_name == :collection
|
|
50
|
+
add_offense(node.loc.selector, message: MSG_COLLECTION)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def inside_routes_draw?(node)
|
|
57
|
+
node.each_ancestor(:block).any? { |block| route_draw_block?(block) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def inside_member_or_collection?(node)
|
|
61
|
+
node.each_ancestor(:block).any? do |block|
|
|
62
|
+
block.send_node.method_name == :member || block.send_node.method_name == :collection
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lint_roller'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Guardrails
|
|
7
|
+
# A plugin that integrates rubocop-guardrails with RuboCop's plugin system.
|
|
8
|
+
class Plugin < LintRoller::Plugin
|
|
9
|
+
def about
|
|
10
|
+
LintRoller::About.new(
|
|
11
|
+
name: 'rubocop-guardrails',
|
|
12
|
+
version: VERSION,
|
|
13
|
+
homepage: 'https://github.com/carldawson/rubocop-guardrails',
|
|
14
|
+
description: 'RuboCop cops that keep Rails apps on the golden path'
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def supported?(context)
|
|
19
|
+
context.engine == :rubocop
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def rules(_context)
|
|
23
|
+
LintRoller::Rules.new(
|
|
24
|
+
type: :path,
|
|
25
|
+
config_format: :rubocop,
|
|
26
|
+
value: Pathname.new(__dir__).join('../../../config/default.yml')
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rubocop-guardrails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Carl Dawson
|
|
8
|
+
bindir: bin
|
|
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
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 1.72.2
|
|
40
|
+
description: Opinionated cops for developers who prefer conventional Rails — rich
|
|
41
|
+
models, RESTful controllers, shallow jobs, no service objects.
|
|
42
|
+
email:
|
|
43
|
+
- carldawson@hey.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- LICENSE.txt
|
|
49
|
+
- README.md
|
|
50
|
+
- Rakefile
|
|
51
|
+
- config/default.yml
|
|
52
|
+
- lib/rubocop-guardrails.rb
|
|
53
|
+
- lib/rubocop/cop/guardrails/no_service_objects.rb
|
|
54
|
+
- lib/rubocop/cop/guardrails/restful_actions.rb
|
|
55
|
+
- lib/rubocop/cop/guardrails/restful_routes.rb
|
|
56
|
+
- lib/rubocop/cop/guardrails_cops.rb
|
|
57
|
+
- lib/rubocop/guardrails.rb
|
|
58
|
+
- lib/rubocop/guardrails/plugin.rb
|
|
59
|
+
- lib/rubocop/guardrails/version.rb
|
|
60
|
+
homepage: https://github.com/carldaws/rubocop-guardrails
|
|
61
|
+
licenses:
|
|
62
|
+
- MIT
|
|
63
|
+
metadata:
|
|
64
|
+
homepage_uri: https://github.com/carldaws/rubocop-guardrails
|
|
65
|
+
source_code_uri: https://github.com/carldaws/rubocop-guardrails
|
|
66
|
+
changelog_uri: https://github.com/carldaws/rubocop-guardrails/blob/main/CHANGELOG.md
|
|
67
|
+
default_lint_roller_plugin: RuboCop::Guardrails::Plugin
|
|
68
|
+
rubygems_mfa_required: 'true'
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: 3.1.0
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 4.0.6
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: RuboCop cops that keep Rails apps on the golden path
|
|
86
|
+
test_files: []
|