rubocop-flexport 0.5.0 → 0.10.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/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: []
|