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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5609bd9d5a3b5e3fe6dded2031b9e86d4f51716b511a032205ed4e6564007c6a
4
- data.tar.gz: 0b28be74b5a9a19773bdab6bec25f8dd4031eb7ed4dd53451af5609a330fb595
3
+ metadata.gz: 7ca20c8ca0cc4d308bc2d9e9c45163cefa6b31d0fcd180bec82709909ad821fb
4
+ data.tar.gz: 1e9b7359fa2212617b2bcc3873d47703e36e40c01afba7ce22ddfcc07684162d
5
5
  SHA512:
6
- metadata.gz: fde9706801fcbd3f2eb95f1f02f8e85bda03a864a2e72e9b06ecf2b821b629f83e3b43cc6c8245db412afb30e0e425df79efb0bdfdd44b1b3ef34c66e7c52a9b
7
- data.tar.gz: c9bb0a94e4bae6ea981de96f42bfe7d3ae69aef0703ce9afe35f7c53d6396bb381a689da7e8745365c34a3207a7fb836d0185680716458b4b656086d22dc0cfb
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
@@ -1,3 +1,3 @@
1
1
  module GLRubocop
2
- VERSION = '0.5.5'.freeze
2
+ VERSION = '0.6.0'.freeze
3
3
  end
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.5.5
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: 2026-08-06 00:00:00.000000000 Z
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: 3.3.27
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: []