gl_rubocop 0.5.5 → 0.6.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/default.yml +32 -1
- data/lib/gl_rubocop/gl_cops/no_controllers_outside_packs.rb +47 -0
- data/lib/gl_rubocop/gl_cops/no_model_usage_in_controllers.rb +110 -0
- data/lib/gl_rubocop/gl_cops/no_routes_outside_packs.rb +211 -0
- data/lib/gl_rubocop/helpers/active_record_model_registry.rb +138 -0
- data/lib/gl_rubocop/helpers/pack_controller_registry.rb +132 -0
- data/lib/gl_rubocop/version.rb +1 -1
- metadata +8 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ca20c8ca0cc4d308bc2d9e9c45163cefa6b31d0fcd180bec82709909ad821fb
|
|
4
|
+
data.tar.gz: 1e9b7359fa2212617b2bcc3873d47703e36e40c01afba7ce22ddfcc07684162d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 962abd8241aec7d0c7f31ddf561c71c5237b502b59fe83e7902d3bd0e6d5920d29a2a17fcef81c4a9e11b1d0520c3e12f3ce91ba0f83fb6bb0ffa06ea6f595f0
|
|
7
|
+
data.tar.gz: 93079a33074a9dd74dda1a17aaee79452000cd40495ce03f9570260c9ad7aed86d88e8c68b09cd35b5f7937edfc6ccf30475402ffc698ffb568b0c41413d4e56
|
data/default.yml
CHANGED
|
@@ -12,6 +12,9 @@ require:
|
|
|
12
12
|
- ./lib/gl_rubocop/gl_cops/consolidate_request_system_specs.rb
|
|
13
13
|
- ./lib/gl_rubocop/gl_cops/interactor_inherits_from_interactor_base.rb
|
|
14
14
|
- ./lib/gl_rubocop/gl_cops/limit_flash_options.rb
|
|
15
|
+
- ./lib/gl_rubocop/gl_cops/no_controllers_outside_packs.rb
|
|
16
|
+
- ./lib/gl_rubocop/gl_cops/no_model_usage_in_controllers.rb
|
|
17
|
+
- ./lib/gl_rubocop/gl_cops/no_routes_outside_packs.rb
|
|
15
18
|
- ./lib/gl_rubocop/gl_cops/no_stubbing_env.rb
|
|
16
19
|
- ./lib/gl_rubocop/gl_cops/no_stubbing_perform_async.rb
|
|
17
20
|
- ./lib/gl_rubocop/gl_cops/no_transaction_in_controllers.rb
|
|
@@ -34,7 +37,14 @@ AllCops:
|
|
|
34
37
|
SuggestExtensions: false
|
|
35
38
|
NewCops: enable
|
|
36
39
|
Exclude:
|
|
37
|
-
- "config
|
|
40
|
+
- "config/application.rb"
|
|
41
|
+
- "config/boot.rb"
|
|
42
|
+
- "config/environment.rb"
|
|
43
|
+
- "config/environments/**/*"
|
|
44
|
+
- "config/initializers/**/*"
|
|
45
|
+
- "config/locales/**/*"
|
|
46
|
+
- "config/puma.rb"
|
|
47
|
+
- "config/spring.rb"
|
|
38
48
|
- "db/**/*"
|
|
39
49
|
- "log/**/*"
|
|
40
50
|
- "public/**/*"
|
|
@@ -67,6 +77,12 @@ GLCops/InteractorInheritsFromInteractorBase:
|
|
|
67
77
|
GLCops/LimitFlashOptions:
|
|
68
78
|
Enabled: true
|
|
69
79
|
|
|
80
|
+
GLCops/NoControllersOutsidePacks:
|
|
81
|
+
Enabled: true
|
|
82
|
+
Include:
|
|
83
|
+
- "app/controllers/**/*"
|
|
84
|
+
- "packs/**/app/controllers/**/*"
|
|
85
|
+
|
|
70
86
|
GLCops/NoHardcodedStringAssignmentToTextOrContentVariable:
|
|
71
87
|
Enabled: true
|
|
72
88
|
Include:
|
|
@@ -77,6 +93,21 @@ GLCops/NoHardcodedStringsInErbTextElements:
|
|
|
77
93
|
Include:
|
|
78
94
|
- "app/components/**/*.erb"
|
|
79
95
|
|
|
96
|
+
GLCops/NoModelUsageInControllers:
|
|
97
|
+
Enabled: true
|
|
98
|
+
Include:
|
|
99
|
+
- "app/controllers/**/*"
|
|
100
|
+
- "packs/**/app/controllers/**/*"
|
|
101
|
+
|
|
102
|
+
GLCops/NoRoutesOutsidePacks:
|
|
103
|
+
Enabled: true
|
|
104
|
+
Include:
|
|
105
|
+
- "config/routes.rb"
|
|
106
|
+
- "config/routes/**/*.rb"
|
|
107
|
+
- "packs/*/config/routes.rb"
|
|
108
|
+
- "packs/*/config/routes/**/*.rb"
|
|
109
|
+
- "packs/*/public/config/routes.rb"
|
|
110
|
+
|
|
80
111
|
GLCops/NoStubbingEnv:
|
|
81
112
|
Enabled: true
|
|
82
113
|
Include:
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module GLRubocop
|
|
2
|
+
module GLCops
|
|
3
|
+
# This cop bans new controllers from being defined outside of a pack.
|
|
4
|
+
#
|
|
5
|
+
# Per our controller standards RFC, all new controllers must live within a pack
|
|
6
|
+
# (e.g. packs/my_pack/app/controllers) rather than the top-level app/controllers
|
|
7
|
+
# directory, so related functionality stays grouped together as we migrate the
|
|
8
|
+
# codebase to packs.
|
|
9
|
+
#
|
|
10
|
+
# The offense is determined by the file's own location (not just the `Include`
|
|
11
|
+
# config), so this cop only flags controllers whose file path does not contain a
|
|
12
|
+
# `packs/` segment. Existing controllers that predate this rule should be
|
|
13
|
+
# grandfathered in via an `Exclude` list in the consuming project's RuboCop config,
|
|
14
|
+
# so only newly added controllers are flagged.
|
|
15
|
+
#
|
|
16
|
+
# Good:
|
|
17
|
+
# # packs/donations/app/controllers/donations_controller.rb
|
|
18
|
+
# module Donations
|
|
19
|
+
# class DonationsController < ApplicationController
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# Bad:
|
|
24
|
+
# # app/controllers/donations_controller.rb
|
|
25
|
+
# class DonationsController < ApplicationController
|
|
26
|
+
# end
|
|
27
|
+
class NoControllersOutsidePacks < RuboCop::Cop::Base
|
|
28
|
+
MSG = 'New controllers must be placed within a pack ' \
|
|
29
|
+
'(e.g. packs/my_pack/app/controllers), not app/controllers.'.freeze
|
|
30
|
+
|
|
31
|
+
PACK_PATH_SEGMENT = %r{(^|/)packs/}
|
|
32
|
+
|
|
33
|
+
def on_class(node)
|
|
34
|
+
return unless node.identifier.short_name.to_s.end_with?('Controller')
|
|
35
|
+
return if within_pack?
|
|
36
|
+
|
|
37
|
+
add_offense(node)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def within_pack?
|
|
43
|
+
PACK_PATH_SEGMENT.match?(processed_source.buffer.name.to_s)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require_relative '../helpers/active_record_model_registry'
|
|
2
|
+
|
|
3
|
+
module GLRubocop
|
|
4
|
+
module GLCops
|
|
5
|
+
# This cop bans direct ActiveRecord model usage (queries, persistence, and CRUD)
|
|
6
|
+
# inside controllers.
|
|
7
|
+
#
|
|
8
|
+
# Per our controller standards RFC, calling model methods directly in a controller
|
|
9
|
+
# entangles the networking/HTTP layer with the data layer. Query and persistence
|
|
10
|
+
# logic belongs in a GLCommand.
|
|
11
|
+
#
|
|
12
|
+
# Only constants confirmed (by scanning the app's model files) to transitively
|
|
13
|
+
# inherit from ApplicationRecord/ActiveRecord::Base are flagged, so plain Ruby
|
|
14
|
+
# objects that happen to live under app/models (wrappers, value objects, etc.) are
|
|
15
|
+
# not flagged.
|
|
16
|
+
#
|
|
17
|
+
# Good:
|
|
18
|
+
# result = FindNonprofit.call(id: params[:id])
|
|
19
|
+
#
|
|
20
|
+
# Bad:
|
|
21
|
+
# nonprofit = Nonprofit.find(params[:id])
|
|
22
|
+
# nonprofit.update!(name: params[:name])
|
|
23
|
+
class NoModelUsageInControllers < RuboCop::Cop::Base
|
|
24
|
+
MSG = 'Do not use the `%<model>s` model directly in a controller. Move this ' \
|
|
25
|
+
'logic into a GLCommand.'.freeze
|
|
26
|
+
|
|
27
|
+
CLASS_LEVEL_METHODS = %i[
|
|
28
|
+
find find_by find_by! find_or_create_by find_or_create_by!
|
|
29
|
+
where create create! new all order includes joins pluck
|
|
30
|
+
exists? count first last update_all destroy_all
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
INSTANCE_LEVEL_METHODS = %i[save save! update update! destroy destroy!].freeze
|
|
34
|
+
|
|
35
|
+
def on_send(node)
|
|
36
|
+
receiver = node.receiver
|
|
37
|
+
return unless receiver
|
|
38
|
+
|
|
39
|
+
if receiver.const_type?
|
|
40
|
+
check_class_level_usage(node, receiver)
|
|
41
|
+
elsif receiver.lvar_type?
|
|
42
|
+
check_instance_level_usage(node, receiver)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def on_lvasgn(node)
|
|
47
|
+
var_name, value = *node
|
|
48
|
+
model_vars[var_name] = model_name_for(value.receiver) if assigns_model?(value)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def on_def(_node)
|
|
52
|
+
model_vars.clear
|
|
53
|
+
end
|
|
54
|
+
alias_method :on_defs, :on_def
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def check_class_level_usage(node, receiver)
|
|
59
|
+
return unless CLASS_LEVEL_METHODS.include?(node.method_name)
|
|
60
|
+
|
|
61
|
+
model = model_name_for(receiver)
|
|
62
|
+
return unless model
|
|
63
|
+
|
|
64
|
+
add_offense(node, message: format(MSG, model:))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def check_instance_level_usage(node, receiver)
|
|
68
|
+
return unless INSTANCE_LEVEL_METHODS.include?(node.method_name)
|
|
69
|
+
|
|
70
|
+
model = model_vars[receiver.node_parts.first]
|
|
71
|
+
return unless model
|
|
72
|
+
|
|
73
|
+
add_offense(node, message: format(MSG, model:))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def assigns_model?(value)
|
|
77
|
+
value.respond_to?(:send_type?) && value.send_type? && value.receiver&.const_type?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def model_name_for(const_node)
|
|
81
|
+
name = full_const_name(const_node)
|
|
82
|
+
name if model_names.include?(name)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def full_const_name(node)
|
|
86
|
+
return nil unless node&.const_type?
|
|
87
|
+
|
|
88
|
+
namespace, name = *node
|
|
89
|
+
[namespace && full_const_name(namespace), name].compact.join('::')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def model_vars
|
|
93
|
+
@model_vars ||= {}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def model_names
|
|
97
|
+
GLRubocop::Helpers::ActiveRecordModelRegistry.for(project_root:,
|
|
98
|
+
globs: model_globs)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def project_root
|
|
102
|
+
RuboCop::ConfigFinder.project_root
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def model_globs
|
|
106
|
+
cop_config.fetch('ModelGlobs', GLRubocop::Helpers::ActiveRecordModelRegistry::DEFAULT_GLOBS)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
require_relative '../helpers/pack_controller_registry'
|
|
2
|
+
|
|
3
|
+
module GLRubocop
|
|
4
|
+
module GLCops
|
|
5
|
+
# This cop flags route declarations for a pack-based controller when those routes
|
|
6
|
+
# are not defined within that pack's own routes file.
|
|
7
|
+
#
|
|
8
|
+
# Per our controller standards RFC, route declarations should live alongside the
|
|
9
|
+
# controllers they serve, so the relationship between routes and controllers stays
|
|
10
|
+
# clear and the codebase is easier to navigate.
|
|
11
|
+
#
|
|
12
|
+
# Only routes that resolve (via an explicit `to:`/`controller:` option, or an
|
|
13
|
+
# implicit `resources`/`resource` name) to a controller that lives in a pack are
|
|
14
|
+
# checked; routes for controllers outside of packs are always ignored. Pre-existing
|
|
15
|
+
# violations can be grandfathered in via the `Grandfathered` option (a list of
|
|
16
|
+
# controller paths, e.g. `receipt_copy/receipts`) so only newly introduced
|
|
17
|
+
# violations are flagged.
|
|
18
|
+
#
|
|
19
|
+
# Good:
|
|
20
|
+
# # packs/tracking/config/routes/tracking.rb (packs/tracking/app/controllers/events_controller.rb)
|
|
21
|
+
# namespace :tracking do
|
|
22
|
+
# resources :events, only: :create
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# Bad:
|
|
26
|
+
# # config/routes.rb, for a controller defined in packs/tracking
|
|
27
|
+
# namespace :tracking do
|
|
28
|
+
# resources :events, only: :create
|
|
29
|
+
# end
|
|
30
|
+
class NoRoutesOutsidePacks < RuboCop::Cop::Base
|
|
31
|
+
MSG = 'Routes for the `%<controller>s` controller must be defined in ' \
|
|
32
|
+
'%<expected_path>s (packs/%<pack>s), not here.'.freeze
|
|
33
|
+
|
|
34
|
+
NAMESPACING_METHODS = %i[namespace scope].freeze
|
|
35
|
+
RESOURCE_METHODS = %i[resources resource].freeze
|
|
36
|
+
MEMBER_ROUTE_METHODS = %i[get post put patch delete match].freeze
|
|
37
|
+
|
|
38
|
+
DEFAULT_ROUTES_PATH_TEMPLATE = 'packs/%<pack>s/public/config/routes.rb'.freeze
|
|
39
|
+
NON_PLURALIZING_Y_SUFFIXES = %w[ay ey iy oy uy].freeze
|
|
40
|
+
|
|
41
|
+
def on_new_investigation
|
|
42
|
+
walk(processed_source.ast, [])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def walk(node, namespace)
|
|
48
|
+
return unless node.is_a?(RuboCop::AST::Node)
|
|
49
|
+
|
|
50
|
+
if node.block_type?
|
|
51
|
+
walk_block(node, namespace)
|
|
52
|
+
elsif node.send_type?
|
|
53
|
+
check_route(node, namespace)
|
|
54
|
+
walk_children(node, namespace)
|
|
55
|
+
else
|
|
56
|
+
walk_children(node, namespace)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def walk_children(node, namespace)
|
|
61
|
+
node.children.each { |child| walk(child, namespace) if child.is_a?(RuboCop::AST::Node) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def walk_block(node, namespace)
|
|
65
|
+
send_node = node.send_node
|
|
66
|
+
check_route(send_node, namespace)
|
|
67
|
+
|
|
68
|
+
child_namespace = namespace + Array(namespace_segment_for(send_node))
|
|
69
|
+
walk(node.body, child_namespace) if node.body
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def check_route(send_node, namespace)
|
|
73
|
+
return unless send_node&.send_type?
|
|
74
|
+
|
|
75
|
+
if RESOURCE_METHODS.include?(send_node.method_name)
|
|
76
|
+
check_resource(send_node, namespace)
|
|
77
|
+
elsif MEMBER_ROUTE_METHODS.include?(send_node.method_name)
|
|
78
|
+
check_member_route(send_node, namespace)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def check_member_route(send_node, namespace)
|
|
83
|
+
to_value = keyword_value(send_node, :to)
|
|
84
|
+
return unless to_value&.str_type?
|
|
85
|
+
|
|
86
|
+
controller_part = to_value.value.to_s.split('#').first
|
|
87
|
+
# rubocop:disable Rails/Blank -- avoid depending on ActiveSupport, which
|
|
88
|
+
# gl_rubocop does not declare as a direct gem dependency.
|
|
89
|
+
return if controller_part.nil? || controller_part.empty?
|
|
90
|
+
# rubocop:enable Rails/Blank
|
|
91
|
+
|
|
92
|
+
check_controller_path(send_node, build_controller_path(namespace, controller_part))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def check_resource(send_node, namespace)
|
|
96
|
+
controller_part = explicit_controller_option(send_node) || resource_name_for(send_node,
|
|
97
|
+
namespace)
|
|
98
|
+
return unless controller_part
|
|
99
|
+
|
|
100
|
+
check_controller_path(send_node, build_controller_path(namespace, controller_part))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def explicit_controller_option(send_node)
|
|
104
|
+
string_or_symbol_value(keyword_value(send_node, :controller))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def resource_name_for(send_node, namespace)
|
|
108
|
+
name = string_or_symbol_value(send_node.arguments.first)
|
|
109
|
+
return nil unless name
|
|
110
|
+
return name unless send_node.method_name == :resource
|
|
111
|
+
|
|
112
|
+
# `resource :checkout` conventionally maps to a pluralized controller
|
|
113
|
+
# (CheckoutsController), but some routes already pass the plural form
|
|
114
|
+
# directly (`resource :checkouts`) — prefer the name as-given when it
|
|
115
|
+
# already resolves to a known pack controller, to avoid double-pluralizing
|
|
116
|
+
# it (e.g. "checkouts" -> "checkoutses").
|
|
117
|
+
as_given_path = build_controller_path(namespace, name)
|
|
118
|
+
return name if controller_packs.key?(as_given_path)
|
|
119
|
+
|
|
120
|
+
pluralize(name)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def namespace_segment_for(send_node)
|
|
124
|
+
return nil unless send_node&.send_type?
|
|
125
|
+
|
|
126
|
+
return namespace_segment_from_arg(send_node) if send_node.method_name == :namespace
|
|
127
|
+
return namespace_segment_from_module_option(send_node) if send_node.method_name == :scope
|
|
128
|
+
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def namespace_segment_from_arg(send_node)
|
|
133
|
+
string_or_symbol_value(send_node.arguments.first)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def namespace_segment_from_module_option(send_node)
|
|
137
|
+
string_or_symbol_value(keyword_value(send_node, :module))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def string_or_symbol_value(node)
|
|
141
|
+
return nil unless node && (node.str_type? || node.sym_type?)
|
|
142
|
+
|
|
143
|
+
node.value.to_s
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def keyword_value(send_node, key)
|
|
147
|
+
hash_arg = send_node.arguments.find(&:hash_type?)
|
|
148
|
+
return nil unless hash_arg
|
|
149
|
+
|
|
150
|
+
pair = hash_arg.pairs.find { |p| p.key.sym_type? && p.key.value == key }
|
|
151
|
+
pair&.value
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def build_controller_path(namespace, controller_part)
|
|
155
|
+
return controller_part.delete_prefix('/') if controller_part.start_with?('/')
|
|
156
|
+
|
|
157
|
+
(namespace + [controller_part]).join('/')
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def check_controller_path(node, controller_path)
|
|
161
|
+
pack_name = controller_packs[controller_path]
|
|
162
|
+
return unless pack_name
|
|
163
|
+
return if grandfathered.include?(controller_path)
|
|
164
|
+
return if defined_in_owning_pack_routes_file?(pack_name)
|
|
165
|
+
|
|
166
|
+
add_offense(node, message: format(MSG,
|
|
167
|
+
controller: controller_path,
|
|
168
|
+
pack: pack_name,
|
|
169
|
+
expected_path: format(routes_path_template,
|
|
170
|
+
pack: pack_name)))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def defined_in_owning_pack_routes_file?(pack_name)
|
|
174
|
+
expected_suffix = format(routes_path_template, pack: pack_name)
|
|
175
|
+
processed_source.path.to_s.end_with?(expected_suffix)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def pluralize(word)
|
|
179
|
+
return "#{word.delete_suffix('y')}ies" if pluralizes_to_ies?(word)
|
|
180
|
+
return "#{word}es" if word.end_with?('s', 'x', 'z', 'ch', 'sh')
|
|
181
|
+
|
|
182
|
+
"#{word}s"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def pluralizes_to_ies?(word)
|
|
186
|
+
word.end_with?('y') && NON_PLURALIZING_Y_SUFFIXES.none? { |suffix| word.end_with?(suffix) }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def controller_packs
|
|
190
|
+
GLRubocop::Helpers::PackControllerRegistry.for(project_root:, globs: controller_globs)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def project_root
|
|
194
|
+
RuboCop::ConfigFinder.project_root
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def controller_globs
|
|
198
|
+
cop_config.fetch('ControllerGlobs',
|
|
199
|
+
GLRubocop::Helpers::PackControllerRegistry::DEFAULT_GLOBS)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def routes_path_template
|
|
203
|
+
cop_config.fetch('RoutesPathTemplate', DEFAULT_ROUTES_PATH_TEMPLATE)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def grandfathered
|
|
207
|
+
cop_config.fetch('Grandfathered', [])
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
require 'parser/current'
|
|
3
|
+
|
|
4
|
+
module GLRubocop
|
|
5
|
+
module Helpers
|
|
6
|
+
class ActiveRecordModelRegistry
|
|
7
|
+
DEFAULT_GLOBS = [
|
|
8
|
+
'app/models/**/*.rb',
|
|
9
|
+
'packs/*/app/models/**/*.rb'
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
AR_BASE_CLASSES = ['ApplicationRecord', 'ActiveRecord::Base'].freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def for(project_root:, globs: DEFAULT_GLOBS)
|
|
16
|
+
@cache ||= {}
|
|
17
|
+
@cache[[project_root,
|
|
18
|
+
globs]] ||= new(project_root:, globs:).model_names
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def reset_cache!
|
|
22
|
+
@cache = {}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(project_root:, globs:)
|
|
27
|
+
@project_root = project_root
|
|
28
|
+
@globs = globs
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def model_names
|
|
32
|
+
parents = collect_class_parents
|
|
33
|
+
Set.new(parents.keys.select { |name| ar_derived?(name, parents) })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :project_root, :globs
|
|
39
|
+
|
|
40
|
+
def collect_class_parents
|
|
41
|
+
parents = {}
|
|
42
|
+
globs.each do |glob|
|
|
43
|
+
Dir.glob(File.join(project_root, glob)).each do |path|
|
|
44
|
+
next unless File.file?(path)
|
|
45
|
+
|
|
46
|
+
walk_classes(parse(path), [], parents)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
parents
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse(path)
|
|
53
|
+
Parser::CurrentRuby.parse(File.read(path))
|
|
54
|
+
rescue Parser::SyntaxError, EncodingError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def walk_classes(node, namespace, parents)
|
|
59
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
60
|
+
|
|
61
|
+
case node.type
|
|
62
|
+
when :module
|
|
63
|
+
walk_module(node, namespace, parents)
|
|
64
|
+
when :class
|
|
65
|
+
walk_class(node, namespace, parents)
|
|
66
|
+
else
|
|
67
|
+
walk_children(node, namespace, parents)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def walk_module(node, namespace, parents)
|
|
72
|
+
walk_body(node.children[1], namespace + [const_name(node.children[0])], parents)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def walk_class(node, namespace, parents)
|
|
76
|
+
name = namespace + [const_name(node.children[0])]
|
|
77
|
+
parent = node.children[1] && const_name(node.children[1])
|
|
78
|
+
parents[name.join('::')] = { parent:, namespace: }
|
|
79
|
+
walk_body(node.children[2], name, parents)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def walk_children(node, namespace, parents)
|
|
83
|
+
node.children.each do |child|
|
|
84
|
+
walk_classes(child, namespace, parents) if child.is_a?(Parser::AST::Node)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def walk_body(body, namespace, parents)
|
|
89
|
+
return unless body
|
|
90
|
+
|
|
91
|
+
if body.type == :begin
|
|
92
|
+
body.children.each { |child| walk_classes(child, namespace, parents) }
|
|
93
|
+
else
|
|
94
|
+
walk_classes(body, namespace, parents)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def const_name(node)
|
|
99
|
+
return nil unless node
|
|
100
|
+
|
|
101
|
+
parts = []
|
|
102
|
+
current = node
|
|
103
|
+
while current&.type == :const
|
|
104
|
+
parts.unshift(current.children[1].to_s)
|
|
105
|
+
current = current.children[0]
|
|
106
|
+
end
|
|
107
|
+
parts.join('::')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def ar_derived?(name, parents, seen = {})
|
|
111
|
+
return seen[name] if seen.key?(name)
|
|
112
|
+
return true if AR_BASE_CLASSES.include?(name)
|
|
113
|
+
|
|
114
|
+
info = parents[name]
|
|
115
|
+
return false unless info
|
|
116
|
+
|
|
117
|
+
seen[name] = false
|
|
118
|
+
parent = info[:parent]
|
|
119
|
+
return false unless parent
|
|
120
|
+
|
|
121
|
+
resolved_parent = resolve_const(parent, info[:namespace], parents)
|
|
122
|
+
result = ar_derived?(resolved_parent, parents, seen)
|
|
123
|
+
seen[name] = result
|
|
124
|
+
result
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def resolve_const(name, namespace, parents)
|
|
128
|
+
return name if parents.key?(name)
|
|
129
|
+
|
|
130
|
+
namespace.size.downto(0) do |i|
|
|
131
|
+
candidate = (namespace[0...i] + [name]).join('::')
|
|
132
|
+
return candidate if parents.key?(candidate)
|
|
133
|
+
end
|
|
134
|
+
name
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require 'parser/current'
|
|
2
|
+
|
|
3
|
+
module GLRubocop
|
|
4
|
+
module Helpers
|
|
5
|
+
# Scans a project's packs for controller classes and maps each controller's Rails
|
|
6
|
+
# controller path (e.g. `'receipt_copy/receipts'`) to the name of the pack that
|
|
7
|
+
# defines it. Controller paths are derived from the controller's fully namespaced
|
|
8
|
+
# class name (mirroring how Rails computes `controller_path`), not from the file's
|
|
9
|
+
# location on disk, since some packs place controllers under `app/public` instead
|
|
10
|
+
# of `app/controllers`.
|
|
11
|
+
class PackControllerRegistry
|
|
12
|
+
DEFAULT_GLOBS = [
|
|
13
|
+
'packs/*/app/controllers/**/*.rb',
|
|
14
|
+
'packs/*/app/public/**/*_controller.rb'
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def for(project_root:, globs: DEFAULT_GLOBS)
|
|
19
|
+
@cache ||= {}
|
|
20
|
+
@cache[[project_root, globs]] ||= new(project_root:, globs:).controller_packs
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def reset_cache!
|
|
24
|
+
@cache = {}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(project_root:, globs:)
|
|
29
|
+
@project_root = project_root
|
|
30
|
+
@globs = globs
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns a Hash of controller_path (String) => pack_name (String)
|
|
34
|
+
def controller_packs
|
|
35
|
+
mapping = {}
|
|
36
|
+
globs.each do |glob|
|
|
37
|
+
Dir.glob(File.join(project_root, glob)).each { |path| collect(path, mapping) }
|
|
38
|
+
end
|
|
39
|
+
mapping
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :project_root, :globs
|
|
45
|
+
|
|
46
|
+
def collect(path, mapping)
|
|
47
|
+
return unless File.file?(path)
|
|
48
|
+
|
|
49
|
+
pack_name = pack_name_for(path)
|
|
50
|
+
return unless pack_name
|
|
51
|
+
|
|
52
|
+
controller_paths_in(path).each { |controller_path| mapping[controller_path] = pack_name }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def pack_name_for(path)
|
|
56
|
+
relative = path.delete_prefix("#{project_root}/")
|
|
57
|
+
match = relative.match(%r{\Apacks/([^/]+)/})
|
|
58
|
+
match && match[1]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def controller_paths_in(path)
|
|
62
|
+
ast = parse(path)
|
|
63
|
+
return [] unless ast
|
|
64
|
+
|
|
65
|
+
paths = []
|
|
66
|
+
walk_classes(ast, [], paths)
|
|
67
|
+
paths
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse(path)
|
|
71
|
+
Parser::CurrentRuby.parse(File.read(path))
|
|
72
|
+
rescue Parser::SyntaxError, EncodingError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def walk_classes(node, namespace, paths)
|
|
77
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
78
|
+
|
|
79
|
+
case node.type
|
|
80
|
+
when :module
|
|
81
|
+
walk_body(node.children[1], namespace + [const_name(node.children[0])], paths)
|
|
82
|
+
when :class
|
|
83
|
+
walk_class(node, namespace, paths)
|
|
84
|
+
else
|
|
85
|
+
node.children.each do |child|
|
|
86
|
+
walk_classes(child, namespace, paths) if child.is_a?(Parser::AST::Node)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def walk_class(node, namespace, paths)
|
|
92
|
+
full_name = (namespace + [const_name(node.children[0])]).join('::')
|
|
93
|
+
paths << controller_path_for(full_name) if full_name.end_with?('Controller')
|
|
94
|
+
walk_body(node.children[2], namespace + [const_name(node.children[0])], paths)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def walk_body(body, namespace, paths)
|
|
98
|
+
return unless body
|
|
99
|
+
|
|
100
|
+
if body.type == :begin
|
|
101
|
+
body.children.each { |child| walk_classes(child, namespace, paths) }
|
|
102
|
+
else
|
|
103
|
+
walk_classes(body, namespace, paths)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def const_name(node)
|
|
108
|
+
return nil unless node
|
|
109
|
+
|
|
110
|
+
parts = []
|
|
111
|
+
current = node
|
|
112
|
+
while current&.type == :const
|
|
113
|
+
parts.unshift(current.children[1].to_s)
|
|
114
|
+
current = current.children[0]
|
|
115
|
+
end
|
|
116
|
+
parts.join('::')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def controller_path_for(full_class_name)
|
|
120
|
+
underscore(full_class_name.delete_suffix('Controller'))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def underscore(camel_cased_word)
|
|
124
|
+
camel_cased_word.gsub('::', '/')
|
|
125
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
126
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
127
|
+
.tr('-', '_')
|
|
128
|
+
.downcase
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
data/lib/gl_rubocop/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gl_rubocop
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Give Lively
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rubocop
|
|
@@ -150,8 +149,6 @@ dependencies:
|
|
|
150
149
|
- - ">="
|
|
151
150
|
- !ruby/object:Gem::Version
|
|
152
151
|
version: '0'
|
|
153
|
-
description:
|
|
154
|
-
email:
|
|
155
152
|
executables: []
|
|
156
153
|
extensions: []
|
|
157
154
|
extra_rdoc_files:
|
|
@@ -166,8 +163,11 @@ files:
|
|
|
166
163
|
- lib/gl_rubocop/gl_cops/consolidate_request_system_specs.rb
|
|
167
164
|
- lib/gl_rubocop/gl_cops/interactor_inherits_from_interactor_base.rb
|
|
168
165
|
- lib/gl_rubocop/gl_cops/limit_flash_options.rb
|
|
166
|
+
- lib/gl_rubocop/gl_cops/no_controllers_outside_packs.rb
|
|
169
167
|
- lib/gl_rubocop/gl_cops/no_hardcoded_string_assignment_to_text_or_content_variable.rb
|
|
170
168
|
- lib/gl_rubocop/gl_cops/no_hardcoded_strings_in_erb_text_elements.rb
|
|
169
|
+
- lib/gl_rubocop/gl_cops/no_model_usage_in_controllers.rb
|
|
170
|
+
- lib/gl_rubocop/gl_cops/no_routes_outside_packs.rb
|
|
171
171
|
- lib/gl_rubocop/gl_cops/no_stubbing_env.rb
|
|
172
172
|
- lib/gl_rubocop/gl_cops/no_stubbing_perform_async.rb
|
|
173
173
|
- lib/gl_rubocop/gl_cops/no_transaction_in_controllers.rb
|
|
@@ -183,15 +183,16 @@ files:
|
|
|
183
183
|
- lib/gl_rubocop/gl_cops/view_component_directory_structure.rb
|
|
184
184
|
- lib/gl_rubocop/gl_cops/view_component_inheritance.rb
|
|
185
185
|
- lib/gl_rubocop/gl_cops/view_component_initialize_keyword_args.rb
|
|
186
|
+
- lib/gl_rubocop/helpers/active_record_model_registry.rb
|
|
186
187
|
- lib/gl_rubocop/helpers/erb_content_helper.rb
|
|
187
188
|
- lib/gl_rubocop/helpers/haml_content_helper.rb
|
|
189
|
+
- lib/gl_rubocop/helpers/pack_controller_registry.rb
|
|
188
190
|
- lib/gl_rubocop/version.rb
|
|
189
191
|
homepage: https://github.com/givelively/gl_rubocop
|
|
190
192
|
licenses:
|
|
191
193
|
- MIT
|
|
192
194
|
metadata:
|
|
193
195
|
rubygems_mfa_required: 'true'
|
|
194
|
-
post_install_message:
|
|
195
196
|
rdoc_options: []
|
|
196
197
|
require_paths:
|
|
197
198
|
- lib
|
|
@@ -206,8 +207,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
206
207
|
- !ruby/object:Gem::Version
|
|
207
208
|
version: '0'
|
|
208
209
|
requirements: []
|
|
209
|
-
rubygems_version:
|
|
210
|
-
signing_key:
|
|
210
|
+
rubygems_version: 4.0.10
|
|
211
211
|
specification_version: 4
|
|
212
212
|
summary: A shareable configuration of Give Lively's rubocop rules.
|
|
213
213
|
test_files: []
|