rubocop-flexport 0.5.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/rubocop/cop/flexport/engine_api_boundary.rb +147 -53
- data/lib/rubocop/cop/flexport/global_model_access_from_engine.rb +70 -6
- data/lib/rubocop/cop/flexport_cops.rb +1 -0
- data/lib/rubocop/cop/mixin/engine_api.rb +8 -5
- data/lib/rubocop/cop/mixin/engine_node_context.rb +1 -1
- data/lib/rubocop/cop/mixin/factory_bot_usage.rb +112 -0
- data/lib/rubocop/flexport/version.rb +1 -1
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1111bda95a88ccde039176a94717252bb9828339ecc9ffef53134068ce9c119a
|
4
|
+
data.tar.gz: c93f725711efde4dd37de393c365c744545c4193e7cc8e433e58f23ca3915768
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8428aff55f6b2bc1bd5b9657942d5f11eb84deb9d98b8e7441db6ff2a6161169e80cfa088b8c0a05e0d54c84643b04b5bc623fa7db1bd728c3f2be17f760d11
|
7
|
+
data.tar.gz: 44ba932445440f9a79e1d5169a62ba41221529d6e5ca3ca69f538776f86fd80727371ad8a7df57045d583f45097ce9024b69d761bb1be756e5a805bda02533be
|
data/README.md
CHANGED
@@ -43,7 +43,7 @@ like below and then run `bundle install`:
|
|
43
43
|
gem "rubocop-flexport", path: "/Users/<user>/rubocop-flexport"
|
44
44
|
```
|
45
45
|
|
46
|
-
To release a new version, update the version number in `version.rb`, and then
|
46
|
+
To release a new version, update the version number in `lib/rubocop/flexport/version.rb`, and then
|
47
47
|
run `bundle exec rake release`, which will create a git tag for the version,
|
48
48
|
push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
49
49
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'digest/sha1'
|
4
|
+
|
3
5
|
# rubocop:disable Metrics/ClassLength
|
4
6
|
module RuboCop
|
5
7
|
module Cop
|
@@ -17,7 +19,7 @@ module RuboCop
|
|
17
19
|
# will be accessible outside your engine. For example, adding
|
18
20
|
# `api/foo_service.rb` will allow code outside your engine to
|
19
21
|
# invoke eg `MyEngine::Api::FooService.bar(baz)`.
|
20
|
-
# - Create
|
22
|
+
# - Create an `_allowlist.rb` or `_whitelist.rb` file in `api/`. Modules listed in
|
21
23
|
# this file are accessible to code outside the engine. The file
|
22
24
|
# must have this name and a particular format (see below).
|
23
25
|
#
|
@@ -39,6 +41,11 @@ module RuboCop
|
|
39
41
|
# The cop detects cross-engine associations as well as cross-engine
|
40
42
|
# module access.
|
41
43
|
#
|
44
|
+
# The cop will complain if you use FactoryBot factories defined in other
|
45
|
+
# engines in your engine's specs. You can disable this check by adding
|
46
|
+
# the engine name to `AllowCrossEngineFactoryBotFromEngines` in
|
47
|
+
# .rubocop.yml.
|
48
|
+
#
|
42
49
|
# # Isolation guarantee
|
43
50
|
#
|
44
51
|
# This cop can be easily circumvented with metaprogramming, so it cannot
|
@@ -141,23 +148,30 @@ module RuboCop
|
|
141
148
|
class EngineApiBoundary < Cop
|
142
149
|
include EngineApi
|
143
150
|
include EngineNodeContext
|
151
|
+
include FactoryBotUsage
|
144
152
|
|
145
|
-
MSG = 'Direct access of %<
|
146
|
-
'Only access engine via %<
|
153
|
+
MSG = 'Direct access of %<accessed_engine>s engine. ' \
|
154
|
+
'Only access engine via %<accessed_engine>s::Api.'
|
147
155
|
|
148
156
|
STRONGLY_PROTECTED_MSG = 'All direct access of ' \
|
149
|
-
'%<
|
157
|
+
'%<accessed_engine>s engine disallowed because ' \
|
150
158
|
'it is in StronglyProtectedEngines list.'
|
151
159
|
|
152
160
|
STRONGLY_PROTECTED_CURRENT_MSG = 'Direct ' \
|
153
|
-
'access of
|
154
|
-
'it\'s in the %<
|
161
|
+
'access of %<accessed_engine>s is disallowed in this file ' \
|
162
|
+
'because it\'s in the %<current_engine>s engine, which ' \
|
155
163
|
'is in the StronglyProtectedEngines list.'
|
156
164
|
|
165
|
+
MAIN_APP_NAME = 'MainApp::EngineApi'
|
166
|
+
|
157
167
|
def_node_matcher :rails_association_hash_args, <<-PATTERN
|
158
168
|
(send _ {:belongs_to :has_one :has_many} sym $hash)
|
159
169
|
PATTERN
|
160
170
|
|
171
|
+
class << self
|
172
|
+
attr_accessor :factory_engines_cache
|
173
|
+
end
|
174
|
+
|
161
175
|
def on_const(node)
|
162
176
|
return if in_module_or_class_declaration?(node)
|
163
177
|
# There might be value objects that are named
|
@@ -168,48 +182,84 @@ module RuboCop
|
|
168
182
|
# We don't want to warn on these cases either.
|
169
183
|
return if sending_method_to_namespace_itself?(node)
|
170
184
|
|
171
|
-
|
172
|
-
return unless
|
173
|
-
return if valid_engine_access?(node,
|
185
|
+
accessed_engine = extract_accessed_engine(node)
|
186
|
+
return unless accessed_engine
|
187
|
+
return if valid_engine_access?(node, accessed_engine)
|
174
188
|
|
175
|
-
add_offense(node, message: message(
|
189
|
+
add_offense(node, message: message(accessed_engine))
|
176
190
|
end
|
177
191
|
|
178
192
|
def on_send(node)
|
179
193
|
rails_association_hash_args(node) do |assocation_hash_args|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
engine = extract_model_engine(class_name_node)
|
184
|
-
next if engine.nil?
|
185
|
-
next if valid_engine_access?(node, engine)
|
194
|
+
check_for_cross_engine_rails_association(node, assocation_hash_args)
|
195
|
+
end
|
196
|
+
return unless check_for_cross_engine_factory_bot?
|
186
197
|
|
187
|
-
|
198
|
+
factory_bot_usage(node) do |factory_node|
|
199
|
+
check_for_cross_engine_factory_bot_usage(node, factory_node)
|
188
200
|
end
|
189
201
|
end
|
190
202
|
|
203
|
+
def check_for_cross_engine_rails_association(node, assocation_hash_args)
|
204
|
+
class_name_node = extract_class_name_node(assocation_hash_args)
|
205
|
+
return if class_name_node.nil?
|
206
|
+
|
207
|
+
accessed_engine = extract_model_engine(class_name_node)
|
208
|
+
return if accessed_engine.nil?
|
209
|
+
return if valid_engine_access?(node, accessed_engine)
|
210
|
+
|
211
|
+
add_offense(class_name_node, message: message(accessed_engine))
|
212
|
+
end
|
213
|
+
|
214
|
+
def check_for_cross_engine_factory_bot_usage(node, factory_node)
|
215
|
+
factory = factory_node.children[0]
|
216
|
+
accessed_engine, model_class_name = factory_engines[factory]
|
217
|
+
return if accessed_engine.nil?
|
218
|
+
|
219
|
+
model_class_node = parse_ast(model_class_name)
|
220
|
+
return if valid_engine_access?(model_class_node, accessed_engine)
|
221
|
+
|
222
|
+
add_offense(node, message: message(accessed_engine))
|
223
|
+
end
|
224
|
+
|
191
225
|
def external_dependency_checksum
|
192
|
-
engine_api_files_modified_time_checksum(engines_path)
|
226
|
+
checksum = engine_api_files_modified_time_checksum(engines_path)
|
227
|
+
return checksum unless check_for_cross_engine_factory_bot?
|
228
|
+
|
229
|
+
checksum + spec_factories_modified_time_checksum
|
193
230
|
end
|
194
231
|
|
195
232
|
private
|
196
233
|
|
197
|
-
def message(
|
198
|
-
if strongly_protected_engine?(
|
199
|
-
format(STRONGLY_PROTECTED_MSG,
|
234
|
+
def message(accessed_engine)
|
235
|
+
if strongly_protected_engine?(accessed_engine)
|
236
|
+
format(STRONGLY_PROTECTED_MSG, accessed_engine: accessed_engine)
|
200
237
|
elsif strongly_protected_engine?(current_engine)
|
201
|
-
format(
|
238
|
+
format(
|
239
|
+
STRONGLY_PROTECTED_CURRENT_MSG,
|
240
|
+
accessed_engine: accessed_engine,
|
241
|
+
current_engine: current_engine
|
242
|
+
)
|
202
243
|
else
|
203
|
-
format(MSG,
|
244
|
+
format(MSG, accessed_engine: accessed_engine)
|
204
245
|
end
|
205
246
|
end
|
206
247
|
|
207
|
-
def
|
248
|
+
def extract_accessed_engine(node)
|
249
|
+
return MAIN_APP_NAME if disallowed_main_app_access?(node)
|
208
250
|
return nil unless protected_engines.include?(node.const_name)
|
209
251
|
|
210
252
|
node.const_name
|
211
253
|
end
|
212
254
|
|
255
|
+
def disallowed_main_app_access?(node)
|
256
|
+
strongly_protected_engine?(current_engine) && main_app_access?(node)
|
257
|
+
end
|
258
|
+
|
259
|
+
def main_app_access?(node)
|
260
|
+
node.const_name.start_with?(MAIN_APP_NAME)
|
261
|
+
end
|
262
|
+
|
213
263
|
def engines_path
|
214
264
|
path = cop_config['EnginesPath']
|
215
265
|
path += '/' unless path.end_with?('/')
|
@@ -236,24 +286,24 @@ module RuboCop
|
|
236
286
|
end
|
237
287
|
|
238
288
|
def sending_method_to_namespace_itself?(node)
|
239
|
-
node.parent
|
289
|
+
node.parent&.send_type?
|
240
290
|
end
|
241
291
|
|
242
|
-
def valid_engine_access?(node,
|
243
|
-
return true if in_engine_file?(
|
292
|
+
def valid_engine_access?(node, accessed_engine)
|
293
|
+
return true if in_engine_file?(accessed_engine)
|
244
294
|
return true if engine_specific_override?(node)
|
245
295
|
|
246
296
|
return false if strongly_protected_engine?(current_engine)
|
247
|
-
return false if strongly_protected_engine?(
|
297
|
+
return false if strongly_protected_engine?(accessed_engine)
|
248
298
|
|
249
|
-
valid_engine_api_access?(node,
|
299
|
+
valid_engine_api_access?(node, accessed_engine)
|
250
300
|
end
|
251
301
|
|
252
|
-
def valid_engine_api_access?(node,
|
302
|
+
def valid_engine_api_access?(node, accessed_engine)
|
253
303
|
(
|
254
|
-
in_legacy_dependent_file?(
|
304
|
+
in_legacy_dependent_file?(accessed_engine) ||
|
255
305
|
through_api?(node) ||
|
256
|
-
|
306
|
+
allowlisted?(node, accessed_engine)
|
257
307
|
)
|
258
308
|
end
|
259
309
|
|
@@ -281,22 +331,23 @@ module RuboCop
|
|
281
331
|
end
|
282
332
|
|
283
333
|
def current_engine
|
284
|
-
@current_engine ||=
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
334
|
+
@current_engine ||= engine_name_from_path(processed_source.path)
|
335
|
+
end
|
336
|
+
|
337
|
+
def engine_name_from_path(file_path)
|
338
|
+
return nil unless file_path&.include?(engines_path)
|
339
|
+
|
340
|
+
parts = file_path.split(engines_path)
|
341
|
+
engine_dir = parts.last.split('/').first
|
342
|
+
ActiveSupport::Inflector.camelize(engine_dir) if engine_dir
|
292
343
|
end
|
293
344
|
|
294
|
-
def in_engine_file?(
|
295
|
-
current_engine ==
|
345
|
+
def in_engine_file?(accessed_engine)
|
346
|
+
current_engine == accessed_engine
|
296
347
|
end
|
297
348
|
|
298
|
-
def in_legacy_dependent_file?(
|
299
|
-
legacy_dependents = read_api_file(
|
349
|
+
def in_legacy_dependent_file?(accessed_engine)
|
350
|
+
legacy_dependents = read_api_file(accessed_engine, :legacy_dependents)
|
300
351
|
# The file names are strings so we need to remove the escaped quotes
|
301
352
|
# on either side from the source code.
|
302
353
|
legacy_dependents = legacy_dependents.map do |source|
|
@@ -311,15 +362,16 @@ module RuboCop
|
|
311
362
|
node.parent&.const_type? && node.parent.children.last == :Api
|
312
363
|
end
|
313
364
|
|
314
|
-
def
|
315
|
-
|
316
|
-
|
365
|
+
def allowlisted?(node, engine)
|
366
|
+
allowlist = read_api_file(engine, :allowlist)
|
367
|
+
allowlist = read_api_file(engine, :whitelist) if allowlist.empty?
|
368
|
+
return false if allowlist.empty?
|
317
369
|
|
318
370
|
depth = 0
|
319
371
|
max_depth = 5
|
320
|
-
while node
|
372
|
+
while node&.const_type? && depth < max_depth
|
321
373
|
full_const_name = remove_leading_colons(node.source)
|
322
|
-
return true if
|
374
|
+
return true if allowlist.include?(full_const_name)
|
323
375
|
|
324
376
|
node = node.parent
|
325
377
|
depth += 1
|
@@ -349,11 +401,22 @@ module RuboCop
|
|
349
401
|
end
|
350
402
|
|
351
403
|
def engine_specific_override?(node)
|
352
|
-
|
353
|
-
|
354
|
-
|
404
|
+
return false unless overrides_for_current_engine
|
405
|
+
|
406
|
+
depth = 0
|
407
|
+
max_depth = 5
|
408
|
+
while node&.const_type? && depth < max_depth
|
409
|
+
module_name = node.source
|
410
|
+
return true if overrides_for_current_engine.include?(module_name)
|
411
|
+
|
412
|
+
node = node.parent
|
413
|
+
depth += 1
|
414
|
+
end
|
415
|
+
false
|
416
|
+
end
|
355
417
|
|
356
|
-
|
418
|
+
def overrides_for_current_engine
|
419
|
+
overrides_by_engine[current_engine]
|
357
420
|
end
|
358
421
|
|
359
422
|
def strongly_protected_engines
|
@@ -366,6 +429,37 @@ module RuboCop
|
|
366
429
|
def strongly_protected_engine?(engine)
|
367
430
|
strongly_protected_engines.include?(engine)
|
368
431
|
end
|
432
|
+
|
433
|
+
def allow_cross_engine_factory_bot_from_engines
|
434
|
+
@allow_cross_engine_factory_bot_from_engines ||=
|
435
|
+
camelize_all(cop_config['AllowCrossEngineFactoryBotFromEngines'] || [])
|
436
|
+
end
|
437
|
+
|
438
|
+
def check_for_cross_engine_factory_bot?
|
439
|
+
spec_file? && !allow_cross_engine_factory_bot_from_engines.include?(current_engine)
|
440
|
+
end
|
441
|
+
|
442
|
+
# Maps factories to the engine where they are defined.
|
443
|
+
def factory_engines
|
444
|
+
# Cache factories at the class level so that we don't have to fetch
|
445
|
+
# them again for every file we lint.
|
446
|
+
self.class.factory_engines_cache ||= spec_factory_paths.each_with_object({}) do |path, h|
|
447
|
+
engine_name = engine_name_from_path(path)
|
448
|
+
ast = parse_ast(File.read(path))
|
449
|
+
find_factories(ast).each do |factory, model_class_name|
|
450
|
+
h[factory] = [engine_name, model_class_name]
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
def spec_factory_paths
|
456
|
+
@spec_factory_paths ||= Dir["#{engines_path}*/spec/factories/**/*.rb"]
|
457
|
+
end
|
458
|
+
|
459
|
+
def spec_factories_modified_time_checksum
|
460
|
+
mtimes = spec_factory_paths.sort.map { |f| File.mtime(f) }
|
461
|
+
Digest::SHA1.hexdigest(mtimes.join)
|
462
|
+
end
|
369
463
|
end
|
370
464
|
end
|
371
465
|
end
|
@@ -45,8 +45,14 @@ module RuboCop
|
|
45
45
|
# # No direct association to global models.
|
46
46
|
# end
|
47
47
|
#
|
48
|
+
# This cop will also complain if you try to use global FactoryBot
|
49
|
+
# factories in your engine's specs. To disable this behavior for your
|
50
|
+
# engine, add it to the `AllowGlobalFactoryBotFromEngines` list in
|
51
|
+
# .rubocop.yml.
|
52
|
+
#
|
48
53
|
class GlobalModelAccessFromEngine < Cop
|
49
54
|
include EngineNodeContext
|
55
|
+
include FactoryBotUsage
|
50
56
|
|
51
57
|
MSG = 'Direct access of global model `%<model>s` ' \
|
52
58
|
'from within Rails Engine.'
|
@@ -55,6 +61,10 @@ module RuboCop
|
|
55
61
|
(send _ {:belongs_to :has_one :has_many} sym $hash)
|
56
62
|
PATTERN
|
57
63
|
|
64
|
+
class << self
|
65
|
+
attr_accessor :global_factories_cache
|
66
|
+
end
|
67
|
+
|
58
68
|
def on_const(node)
|
59
69
|
return unless in_enforced_engine_file?
|
60
70
|
return unless global_model_const?(node)
|
@@ -69,19 +79,41 @@ module RuboCop
|
|
69
79
|
return unless in_enforced_engine_file?
|
70
80
|
|
71
81
|
rails_association_hash_args(node) do |assocation_hash_args|
|
72
|
-
|
73
|
-
|
74
|
-
|
82
|
+
check_for_rails_association_with_global_model(assocation_hash_args)
|
83
|
+
end
|
84
|
+
|
85
|
+
return unless check_for_global_factory_bot?
|
75
86
|
|
76
|
-
|
87
|
+
factory_bot_usage(node) do |factory_node|
|
88
|
+
check_for_global_factory_bot_usage(node, factory_node)
|
77
89
|
end
|
78
90
|
end
|
79
91
|
|
92
|
+
def check_for_rails_association_with_global_model(assocation_hash_args)
|
93
|
+
class_name_node = extract_class_name_node(assocation_hash_args)
|
94
|
+
class_name = class_name_node&.value
|
95
|
+
return unless global_model?(class_name)
|
96
|
+
|
97
|
+
add_offense(class_name_node, message: message(class_name))
|
98
|
+
end
|
99
|
+
|
100
|
+
def check_for_global_factory_bot_usage(node, factory_node)
|
101
|
+
factory = factory_node.children[0]
|
102
|
+
return unless global_factory?(factory)
|
103
|
+
|
104
|
+
model_class_name = global_factories[factory]
|
105
|
+
add_offense(node, message: message(model_class_name))
|
106
|
+
end
|
107
|
+
|
80
108
|
# Because this cop's behavior depends on the state of external files,
|
81
109
|
# we override this method to bust the RuboCop cache when those files
|
82
110
|
# change.
|
83
111
|
def external_dependency_checksum
|
84
|
-
|
112
|
+
if check_for_global_factory_bot?
|
113
|
+
Digest::SHA1.hexdigest((model_dir_paths + global_factories.keys.sort).join)
|
114
|
+
else
|
115
|
+
Digest::SHA1.hexdigest(model_dir_paths.join)
|
116
|
+
end
|
85
117
|
end
|
86
118
|
|
87
119
|
private
|
@@ -94,10 +126,26 @@ module RuboCop
|
|
94
126
|
@global_model_names ||= calculate_global_models
|
95
127
|
end
|
96
128
|
|
129
|
+
def global_factories
|
130
|
+
# Cache factories at the class level so that we don't have to fetch
|
131
|
+
# them again for every file we lint.
|
132
|
+
self.class.global_factories_cache ||= spec_factory_paths.each_with_object({}) do |path, h|
|
133
|
+
source_code = File.read(path)
|
134
|
+
source = RuboCop::ProcessedSource.new(source_code, RUBY_VERSION.to_f)
|
135
|
+
find_factories(source.ast).each do |factory, model_class_name|
|
136
|
+
h[factory] = model_class_name
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
97
141
|
def model_dir_paths
|
98
142
|
Dir[File.join(global_models_path, '**/*.rb')]
|
99
143
|
end
|
100
144
|
|
145
|
+
def spec_factory_paths
|
146
|
+
@spec_factory_paths ||= Dir['spec/factories/**/*.rb']
|
147
|
+
end
|
148
|
+
|
101
149
|
def calculate_global_models
|
102
150
|
all_model_paths = model_dir_paths.reject do |path|
|
103
151
|
path.include?('/concerns/')
|
@@ -127,7 +175,15 @@ module RuboCop
|
|
127
175
|
|
128
176
|
def in_disabled_engine?(file_path)
|
129
177
|
disabled_engines.any? do |e|
|
130
|
-
|
178
|
+
# Add trailing / to engine path to avoid incorrectly
|
179
|
+
# matching engines with similar names
|
180
|
+
file_path.include?(File.join(engines_path, e, ''))
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def check_for_global_factory_bot?
|
185
|
+
spec_file? && allow_global_factory_bot_from_engines.none? do |engine|
|
186
|
+
processed_source.path.include?(File.join(engines_path, engine, ''))
|
131
187
|
end
|
132
188
|
end
|
133
189
|
|
@@ -142,6 +198,10 @@ module RuboCop
|
|
142
198
|
global_model_names.include?(class_name)
|
143
199
|
end
|
144
200
|
|
201
|
+
def global_factory?(factory_name)
|
202
|
+
global_factories.include?(factory_name)
|
203
|
+
end
|
204
|
+
|
145
205
|
def child_of_const?(node)
|
146
206
|
node.parent.const_type?
|
147
207
|
end
|
@@ -163,6 +223,10 @@ module RuboCop
|
|
163
223
|
end
|
164
224
|
end
|
165
225
|
|
226
|
+
def allow_global_factory_bot_from_engines
|
227
|
+
cop_config['AllowGlobalFactoryBotFromEngines'] || []
|
228
|
+
end
|
229
|
+
|
166
230
|
def allowed_global_models
|
167
231
|
cop_config['AllowedGlobalModels'] || []
|
168
232
|
end
|
@@ -10,9 +10,13 @@ module RuboCop
|
|
10
10
|
extend NodePattern::Macros
|
11
11
|
|
12
12
|
API_FILE_DETAILS = {
|
13
|
+
allowlist: {
|
14
|
+
file_basename: '_allowlist.rb',
|
15
|
+
array_matcher: :allowlist_array
|
16
|
+
},
|
13
17
|
whitelist: {
|
14
18
|
file_basename: '_whitelist.rb',
|
15
|
-
array_matcher: :
|
19
|
+
array_matcher: :allowlist_array
|
16
20
|
},
|
17
21
|
legacy_dependents: {
|
18
22
|
file_basename: '_legacy_dependents.rb',
|
@@ -58,8 +62,7 @@ module RuboCop
|
|
58
62
|
File.join(engines_path, "#{raw_name}/app/api/#{raw_name}/api/")
|
59
63
|
end
|
60
64
|
|
61
|
-
def parse_ast(
|
62
|
-
source_code = File.read(file_path)
|
65
|
+
def parse_ast(source_code)
|
63
66
|
source = RuboCop::ProcessedSource.new(source_code, RUBY_VERSION.to_f)
|
64
67
|
source.ast
|
65
68
|
end
|
@@ -99,7 +102,7 @@ module RuboCop
|
|
99
102
|
# s(:const, nil, :Trucking), :LoadTypes)), :freeze)))
|
100
103
|
#
|
101
104
|
# We want the :begin in the 2nd case, the :module in the 1st case.
|
102
|
-
module_node = parse_ast(path)
|
105
|
+
module_node = parse_ast(File.read(path))
|
103
106
|
module_block_node = module_node&.children&.[](1)
|
104
107
|
if module_block_node&.begin_type?
|
105
108
|
module_block_node
|
@@ -108,7 +111,7 @@ module RuboCop
|
|
108
111
|
end
|
109
112
|
end
|
110
113
|
|
111
|
-
def_node_matcher :
|
114
|
+
def_node_matcher :allowlist_array, <<-PATTERN
|
112
115
|
(casgn nil? {:PUBLIC_MODULES :PUBLIC_SERVICES :PUBLIC_CONSTANTS :PUBLIC_TYPES} {$array (send $array ...)})
|
113
116
|
PATTERN
|
114
117
|
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/inflector'
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
# Helpers for detecting FactoryBot usage.
|
8
|
+
module FactoryBotUsage
|
9
|
+
extend NodePattern::Macros
|
10
|
+
|
11
|
+
FACTORY_BOT_METHODS = %i[
|
12
|
+
attributes_for
|
13
|
+
attributes_for_list
|
14
|
+
build
|
15
|
+
build_list
|
16
|
+
build_pair
|
17
|
+
build_stubbed
|
18
|
+
build_stubbed_list
|
19
|
+
create
|
20
|
+
create_list
|
21
|
+
create_pair
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
def_node_matcher :factory_bot_usage, <<~PATTERN
|
25
|
+
(send _ {#{FACTORY_BOT_METHODS.map(&:inspect).join(' ')}} $sym)
|
26
|
+
PATTERN
|
27
|
+
|
28
|
+
def spec_file?
|
29
|
+
processed_source&.path&.match?(/_spec\.rb$/) || false
|
30
|
+
end
|
31
|
+
|
32
|
+
# Recursively traverses a Parser::AST::Node, returning an array of
|
33
|
+
# [factory_name, model_class_name] 2-tuples.
|
34
|
+
def find_factories(node, model_class_name = nil)
|
35
|
+
factories = []
|
36
|
+
return factories unless node.is_a?(Parser::AST::Node)
|
37
|
+
|
38
|
+
factory_node = extract_factory_node(node)
|
39
|
+
if factory_node
|
40
|
+
factory_name, aliases, model_class_name = parse_factory_node(factory_node, model_class_name)
|
41
|
+
if factory_node?(node)
|
42
|
+
([factory_name] + aliases).each do |name|
|
43
|
+
factories << [name, model_class_name]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
factories + node.children.flat_map { |child| find_factories(child, model_class_name) }
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def extract_factory_node(node)
|
54
|
+
return node.children[0] if factory_block?(node)
|
55
|
+
return node if factory_node?(node)
|
56
|
+
end
|
57
|
+
|
58
|
+
def factory_block?(node)
|
59
|
+
return false if node&.type != :block
|
60
|
+
|
61
|
+
factory_node?(node.children[0])
|
62
|
+
end
|
63
|
+
|
64
|
+
def factory_node?(node)
|
65
|
+
node&.type == :send && node.children[1] == :factory
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_factory_node(node, model_class_name_from_parent_factory = nil)
|
69
|
+
factory_name_node, factory_config_node = node.children[2..3]
|
70
|
+
|
71
|
+
factory_name = factory_name_node.children[0]
|
72
|
+
aliases = extract_aliases(factory_config_node)
|
73
|
+
explicit_model_class_name = extract_model_class_name(factory_config_node)
|
74
|
+
model_class_name = explicit_model_class_name ||
|
75
|
+
model_class_name_from_parent_factory ||
|
76
|
+
ActiveSupport::Inflector.camelize(factory_name)
|
77
|
+
|
78
|
+
[factory_name, aliases, model_class_name]
|
79
|
+
end
|
80
|
+
|
81
|
+
def extract_aliases(factory_config_hash_node)
|
82
|
+
aliases_array = extract_hash_value(factory_config_hash_node, :aliases)
|
83
|
+
return [] if aliases_array&.type != :array
|
84
|
+
|
85
|
+
aliases_array.children.map(&:value)
|
86
|
+
end
|
87
|
+
|
88
|
+
def extract_model_class_name(factory_config_hash_node)
|
89
|
+
model_class_name_node = extract_hash_value(factory_config_hash_node, :class)
|
90
|
+
|
91
|
+
case model_class_name_node&.type
|
92
|
+
when :const
|
93
|
+
model_class_name_node.source.sub(/^::/, '')
|
94
|
+
when :str
|
95
|
+
model_class_name_node.value.sub(/^::/, '')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def extract_hash_value(node, hash_key)
|
100
|
+
return nil if node&.type != :hash
|
101
|
+
|
102
|
+
pairs = node.children.select { |child| child.type == :pair }
|
103
|
+
pairs.each do |pair|
|
104
|
+
key, value = pair.children
|
105
|
+
return value if key.value == hash_key
|
106
|
+
end
|
107
|
+
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubocop-flexport
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Flexport Engineering
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-07-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -57,6 +57,7 @@ files:
|
|
57
57
|
- lib/rubocop/cop/flexport_cops.rb
|
58
58
|
- lib/rubocop/cop/mixin/engine_api.rb
|
59
59
|
- lib/rubocop/cop/mixin/engine_node_context.rb
|
60
|
+
- lib/rubocop/cop/mixin/factory_bot_usage.rb
|
60
61
|
- lib/rubocop/flexport.rb
|
61
62
|
- lib/rubocop/flexport/inject.rb
|
62
63
|
- lib/rubocop/flexport/version.rb
|
@@ -64,7 +65,7 @@ homepage: https://github.com/flexport/rubocop-flexport
|
|
64
65
|
licenses:
|
65
66
|
- MIT
|
66
67
|
metadata: {}
|
67
|
-
post_install_message:
|
68
|
+
post_install_message:
|
68
69
|
rdoc_options: []
|
69
70
|
require_paths:
|
70
71
|
- lib
|
@@ -79,9 +80,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
80
|
- !ruby/object:Gem::Version
|
80
81
|
version: '0'
|
81
82
|
requirements: []
|
82
|
-
|
83
|
-
|
84
|
-
signing_key:
|
83
|
+
rubygems_version: 3.1.2
|
84
|
+
signing_key:
|
85
85
|
specification_version: 4
|
86
86
|
summary: RuboCop cops used at Flexport.
|
87
87
|
test_files: []
|