rubocop-flexport 0.6.0 → 0.10.1
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/config/default.yml +4 -0
- data/lib/rubocop/cop/flexport/engine_api_boundary.rb +103 -28
- data/lib/rubocop/cop/flexport/global_model_access_from_engine.rb +59 -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 +208 -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: b6abf69d264c77fa4c789bd8923604ad86b42e583c0ada5b24737cf4d9277d41
|
4
|
+
data.tar.gz: d198d6b916aa148e53f127ed5b74a5d1542c0fb1e9a36907a0e4d08bfc950640
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e121c375c8ee83debb69988c0e60c1014e7b7d136c88a4756bbf81cdf97791824eba9b5c75d91aa2c4b06443b555110e0f8d84462fd856f9a573fef8b4fb3736
|
7
|
+
data.tar.gz: 5e871c97c14af47395c634b9f58fce03e1cdd61eb7a0f47f981e63fd2e208043261edcb55ad2fa2c21c7b75446abe59edc8afac7040f5cfb396ff701e80995d5
|
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
|
|
data/config/default.yml
CHANGED
@@ -6,6 +6,8 @@ Flexport/EngineApiBoundary:
|
|
6
6
|
UnprotectedEngines: []
|
7
7
|
StronglyProtectedEngines: []
|
8
8
|
EngineSpecificOverrides: []
|
9
|
+
FactoryBotEnabled: false
|
10
|
+
FactoryBotOutboundAccessAllowedEngines: []
|
9
11
|
|
10
12
|
Flexport/GlobalModelAccessFromEngine:
|
11
13
|
Description: 'Do not directly access global models from within Rails Engines.'
|
@@ -16,6 +18,8 @@ Flexport/GlobalModelAccessFromEngine:
|
|
16
18
|
GlobalModelsPath: app/models/
|
17
19
|
DisabledEngines: []
|
18
20
|
AllowedGlobalModels: []
|
21
|
+
FactoryBotEnabled: false
|
22
|
+
FactoryBotGlobalAccessAllowedEngines: []
|
19
23
|
Include:
|
20
24
|
- '**/*.rb'
|
21
25
|
|
@@ -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 `FactoryBotOutboundAccessAllowedEngines` 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,6 +148,7 @@ module RuboCop
|
|
141
148
|
class EngineApiBoundary < Cop
|
142
149
|
include EngineApi
|
143
150
|
include EngineNodeContext
|
151
|
+
include FactoryBotUsage
|
144
152
|
|
145
153
|
MSG = 'Direct access of %<accessed_engine>s engine. ' \
|
146
154
|
'Only access engine via %<accessed_engine>s::Api.'
|
@@ -179,19 +187,42 @@ module RuboCop
|
|
179
187
|
|
180
188
|
def on_send(node)
|
181
189
|
rails_association_hash_args(node) do |assocation_hash_args|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
accessed_engine = extract_model_engine(class_name_node)
|
186
|
-
next if accessed_engine.nil?
|
187
|
-
next if valid_engine_access?(node, accessed_engine)
|
190
|
+
check_for_cross_engine_rails_association(node, assocation_hash_args)
|
191
|
+
end
|
192
|
+
return unless check_for_cross_engine_factory_bot?
|
188
193
|
|
189
|
-
|
194
|
+
factory_bot_usage(node) do |factory_node|
|
195
|
+
check_for_cross_engine_factory_bot_usage(node, factory_node)
|
190
196
|
end
|
191
197
|
end
|
192
198
|
|
199
|
+
def check_for_cross_engine_rails_association(node, assocation_hash_args)
|
200
|
+
class_name_node = extract_class_name_node(assocation_hash_args)
|
201
|
+
return if class_name_node.nil?
|
202
|
+
|
203
|
+
accessed_engine = extract_model_engine(class_name_node)
|
204
|
+
return if accessed_engine.nil?
|
205
|
+
return if valid_engine_access?(node, accessed_engine)
|
206
|
+
|
207
|
+
add_offense(class_name_node, message: message(accessed_engine))
|
208
|
+
end
|
209
|
+
|
210
|
+
def check_for_cross_engine_factory_bot_usage(node, factory_node)
|
211
|
+
factory = factory_node.children[0]
|
212
|
+
accessed_engine, model_class_name = factory_engines[factory]
|
213
|
+
return if accessed_engine.nil? || !protected_engines.include?(accessed_engine)
|
214
|
+
|
215
|
+
model_class_node = parse_ast(model_class_name)
|
216
|
+
return if valid_engine_access?(model_class_node, accessed_engine)
|
217
|
+
|
218
|
+
add_offense(node, message: message(accessed_engine))
|
219
|
+
end
|
220
|
+
|
193
221
|
def external_dependency_checksum
|
194
|
-
engine_api_files_modified_time_checksum(engines_path)
|
222
|
+
checksum = engine_api_files_modified_time_checksum(engines_path)
|
223
|
+
return checksum unless check_for_cross_engine_factory_bot?
|
224
|
+
|
225
|
+
checksum + spec_factories_modified_time_checksum
|
195
226
|
end
|
196
227
|
|
197
228
|
private
|
@@ -251,7 +282,7 @@ module RuboCop
|
|
251
282
|
end
|
252
283
|
|
253
284
|
def sending_method_to_namespace_itself?(node)
|
254
|
-
node.parent
|
285
|
+
node.parent&.send_type?
|
255
286
|
end
|
256
287
|
|
257
288
|
def valid_engine_access?(node, accessed_engine)
|
@@ -268,7 +299,7 @@ module RuboCop
|
|
268
299
|
(
|
269
300
|
in_legacy_dependent_file?(accessed_engine) ||
|
270
301
|
through_api?(node) ||
|
271
|
-
|
302
|
+
allowlisted?(node, accessed_engine)
|
272
303
|
)
|
273
304
|
end
|
274
305
|
|
@@ -296,14 +327,15 @@ module RuboCop
|
|
296
327
|
end
|
297
328
|
|
298
329
|
def current_engine
|
299
|
-
@current_engine ||=
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
330
|
+
@current_engine ||= engine_name_from_path(processed_source.path)
|
331
|
+
end
|
332
|
+
|
333
|
+
def engine_name_from_path(file_path)
|
334
|
+
return nil unless file_path&.include?(engines_path)
|
335
|
+
|
336
|
+
parts = file_path.split(engines_path)
|
337
|
+
engine_dir = parts.last.split('/').first
|
338
|
+
ActiveSupport::Inflector.camelize(engine_dir) if engine_dir
|
307
339
|
end
|
308
340
|
|
309
341
|
def in_engine_file?(accessed_engine)
|
@@ -326,15 +358,16 @@ module RuboCop
|
|
326
358
|
node.parent&.const_type? && node.parent.children.last == :Api
|
327
359
|
end
|
328
360
|
|
329
|
-
def
|
330
|
-
|
331
|
-
|
361
|
+
def allowlisted?(node, engine)
|
362
|
+
allowlist = read_api_file(engine, :allowlist)
|
363
|
+
allowlist = read_api_file(engine, :whitelist) if allowlist.empty?
|
364
|
+
return false if allowlist.empty?
|
332
365
|
|
333
366
|
depth = 0
|
334
367
|
max_depth = 5
|
335
|
-
while node
|
368
|
+
while node&.const_type? && depth < max_depth
|
336
369
|
full_const_name = remove_leading_colons(node.source)
|
337
|
-
return true if
|
370
|
+
return true if allowlist.include?(full_const_name)
|
338
371
|
|
339
372
|
node = node.parent
|
340
373
|
depth += 1
|
@@ -364,11 +397,22 @@ module RuboCop
|
|
364
397
|
end
|
365
398
|
|
366
399
|
def engine_specific_override?(node)
|
367
|
-
|
368
|
-
module_names_allowed_by_override = overrides_by_engine[current_engine]
|
369
|
-
return false unless module_names_allowed_by_override
|
400
|
+
return false unless overrides_for_current_engine
|
370
401
|
|
371
|
-
|
402
|
+
depth = 0
|
403
|
+
max_depth = 5
|
404
|
+
while node&.const_type? && depth < max_depth
|
405
|
+
module_name = node.source
|
406
|
+
return true if overrides_for_current_engine.include?(module_name)
|
407
|
+
|
408
|
+
node = node.parent
|
409
|
+
depth += 1
|
410
|
+
end
|
411
|
+
false
|
412
|
+
end
|
413
|
+
|
414
|
+
def overrides_for_current_engine
|
415
|
+
overrides_by_engine[current_engine]
|
372
416
|
end
|
373
417
|
|
374
418
|
def strongly_protected_engines
|
@@ -381,6 +425,37 @@ module RuboCop
|
|
381
425
|
def strongly_protected_engine?(engine)
|
382
426
|
strongly_protected_engines.include?(engine)
|
383
427
|
end
|
428
|
+
|
429
|
+
def factory_bot_outbound_access_allowed_engines
|
430
|
+
@factory_bot_outbound_access_allowed_engines ||=
|
431
|
+
camelize_all(cop_config['FactoryBotOutboundAccessAllowedEngines'] || [])
|
432
|
+
end
|
433
|
+
|
434
|
+
def factory_bot_enabled?
|
435
|
+
cop_config['FactoryBotEnabled']
|
436
|
+
end
|
437
|
+
|
438
|
+
def check_for_cross_engine_factory_bot?
|
439
|
+
spec_file? &&
|
440
|
+
factory_bot_enabled? &&
|
441
|
+
!factory_bot_outbound_access_allowed_engines.include?(current_engine)
|
442
|
+
end
|
443
|
+
|
444
|
+
# Maps factories to the engine where they are defined.
|
445
|
+
def factory_engines
|
446
|
+
@factory_engines ||= find_factories.each_with_object({}) do |factory_file, h|
|
447
|
+
path, factories = factory_file
|
448
|
+
engine_name = engine_name_from_path(path)
|
449
|
+
factories.each do |factory, model_class_name|
|
450
|
+
h[factory] = [engine_name, model_class_name]
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
def spec_factories_modified_time_checksum
|
456
|
+
mtimes = factory_files.sort.map { |f| File.mtime(f) }
|
457
|
+
Digest::SHA1.hexdigest(mtimes.join)
|
458
|
+
end
|
384
459
|
end
|
385
460
|
end
|
386
461
|
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 `FactoryBotGlobalAccessAllowedEngines` 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.'
|
@@ -69,19 +75,41 @@ module RuboCop
|
|
69
75
|
return unless in_enforced_engine_file?
|
70
76
|
|
71
77
|
rails_association_hash_args(node) do |assocation_hash_args|
|
72
|
-
|
73
|
-
|
74
|
-
|
78
|
+
check_for_rails_association_with_global_model(assocation_hash_args)
|
79
|
+
end
|
80
|
+
|
81
|
+
return unless check_for_global_factory_bot?
|
75
82
|
|
76
|
-
|
83
|
+
factory_bot_usage(node) do |factory_node|
|
84
|
+
check_for_global_factory_bot_usage(node, factory_node)
|
77
85
|
end
|
78
86
|
end
|
79
87
|
|
88
|
+
def check_for_rails_association_with_global_model(assocation_hash_args)
|
89
|
+
class_name_node = extract_class_name_node(assocation_hash_args)
|
90
|
+
class_name = class_name_node&.value
|
91
|
+
return unless global_model?(class_name)
|
92
|
+
|
93
|
+
add_offense(class_name_node, message: message(class_name))
|
94
|
+
end
|
95
|
+
|
96
|
+
def check_for_global_factory_bot_usage(node, factory_node)
|
97
|
+
factory = factory_node.children[0]
|
98
|
+
return unless global_factory?(factory)
|
99
|
+
|
100
|
+
model_class_name = global_factories[factory]
|
101
|
+
add_offense(node, message: message(model_class_name))
|
102
|
+
end
|
103
|
+
|
80
104
|
# Because this cop's behavior depends on the state of external files,
|
81
105
|
# we override this method to bust the RuboCop cache when those files
|
82
106
|
# change.
|
83
107
|
def external_dependency_checksum
|
84
|
-
|
108
|
+
if check_for_global_factory_bot?
|
109
|
+
Digest::SHA1.hexdigest((model_dir_paths + global_factories.keys.sort).join)
|
110
|
+
else
|
111
|
+
Digest::SHA1.hexdigest(model_dir_paths.join)
|
112
|
+
end
|
85
113
|
end
|
86
114
|
|
87
115
|
private
|
@@ -94,6 +122,11 @@ module RuboCop
|
|
94
122
|
@global_model_names ||= calculate_global_models
|
95
123
|
end
|
96
124
|
|
125
|
+
def global_factories
|
126
|
+
@global_factories ||=
|
127
|
+
find_factories.reject { |path| path.start_with?(engines_path) }.values.reduce(:merge)
|
128
|
+
end
|
129
|
+
|
97
130
|
def model_dir_paths
|
98
131
|
Dir[File.join(global_models_path, '**/*.rb')]
|
99
132
|
end
|
@@ -127,7 +160,15 @@ module RuboCop
|
|
127
160
|
|
128
161
|
def in_disabled_engine?(file_path)
|
129
162
|
disabled_engines.any? do |e|
|
130
|
-
|
163
|
+
# Add trailing / to engine path to avoid incorrectly
|
164
|
+
# matching engines with similar names
|
165
|
+
file_path.include?(File.join(engines_path, e, ''))
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def check_for_global_factory_bot?
|
170
|
+
spec_file? && factory_bot_enabled? && factory_bot_global_access_allowed_engines.none? do |engine|
|
171
|
+
processed_source.path.include?(File.join(engines_path, engine, ''))
|
131
172
|
end
|
132
173
|
end
|
133
174
|
|
@@ -142,6 +183,10 @@ module RuboCop
|
|
142
183
|
global_model_names.include?(class_name)
|
143
184
|
end
|
144
185
|
|
186
|
+
def global_factory?(factory_name)
|
187
|
+
global_factories.include?(factory_name)
|
188
|
+
end
|
189
|
+
|
145
190
|
def child_of_const?(node)
|
146
191
|
node.parent.const_type?
|
147
192
|
end
|
@@ -163,6 +208,14 @@ module RuboCop
|
|
163
208
|
end
|
164
209
|
end
|
165
210
|
|
211
|
+
def factory_bot_global_access_allowed_engines
|
212
|
+
cop_config['FactoryBotGlobalAccessAllowedEngines'] || []
|
213
|
+
end
|
214
|
+
|
215
|
+
def factory_bot_enabled?
|
216
|
+
cop_config['FactoryBotEnabled']
|
217
|
+
end
|
218
|
+
|
166
219
|
def allowed_global_models
|
167
220
|
cop_config['AllowedGlobalModels'] || []
|
168
221
|
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,208 @@
|
|
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
|
+
# rubocop:disable Metrics/ModuleLength
|
9
|
+
module FactoryBotUsage
|
10
|
+
extend NodePattern::Macros
|
11
|
+
|
12
|
+
Factory = Struct.new('Factory', :name, :aliases, :parent, :model_class_name)
|
13
|
+
|
14
|
+
FACTORY_BOT_METHODS = %i[
|
15
|
+
attributes_for
|
16
|
+
attributes_for_list
|
17
|
+
build
|
18
|
+
build_list
|
19
|
+
build_pair
|
20
|
+
build_stubbed
|
21
|
+
build_stubbed_list
|
22
|
+
create
|
23
|
+
create_list
|
24
|
+
create_pair
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
def_node_matcher :factory_bot_usage, <<~PATTERN
|
28
|
+
(send _ {#{FACTORY_BOT_METHODS.map(&:inspect).join(' ')}} $sym)
|
29
|
+
PATTERN
|
30
|
+
|
31
|
+
# Cache factories at the class level so that we don't have to fetch them
|
32
|
+
# again for every file we lint. We use class variables here so that the
|
33
|
+
# cache can be shared by all cops that include this module.
|
34
|
+
# rubocop:disable Style/ClassVars
|
35
|
+
@@factories = nil
|
36
|
+
|
37
|
+
def self.factories_cache
|
38
|
+
@@factories
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.factories_cache=(factories)
|
42
|
+
@@factories = factories
|
43
|
+
end
|
44
|
+
# rubocop:enable Style/ClassVars
|
45
|
+
|
46
|
+
def spec_file?
|
47
|
+
processed_source&.path&.match?(/_spec\.rb$/) || false
|
48
|
+
end
|
49
|
+
|
50
|
+
# Parses factory definition files, returning a hash mapping factory names
|
51
|
+
# to model class names for each file.
|
52
|
+
def find_factories
|
53
|
+
RuboCop::Cop::FactoryBotUsage.factories_cache ||= begin
|
54
|
+
# We'll add factories here as we parse the factory files.
|
55
|
+
@factories = {}
|
56
|
+
|
57
|
+
# We'll add factories that specify a parent here, so we can resolve the
|
58
|
+
# reference to the parent after we have finished parsing all the files.
|
59
|
+
@parents = {}
|
60
|
+
|
61
|
+
# Parse the factory files, then resolve any parent references.
|
62
|
+
traverse_factory_files
|
63
|
+
resolve_parents
|
64
|
+
|
65
|
+
@factories
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def factory_files
|
70
|
+
@factory_files ||= Dir['spec/factories/**/*.rb'] + Dir["#{engines_path}*/spec/factories/**/*.rb"]
|
71
|
+
end
|
72
|
+
|
73
|
+
def engines_path
|
74
|
+
raise NotImplementedError
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def traverse_factory_files
|
80
|
+
factory_files.each do |path|
|
81
|
+
@factories[path] = {}
|
82
|
+
@parents[path] = {}
|
83
|
+
|
84
|
+
source_code = File.read(path)
|
85
|
+
source = RuboCop::ProcessedSource.new(source_code, RUBY_VERSION.to_f)
|
86
|
+
traverse_node(source.ast, path)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def resolve_parents
|
91
|
+
all_factories = @factories.values.reduce(:merge)
|
92
|
+
all_parents = @parents.values.reduce(:merge)
|
93
|
+
@parents.each do |path, parents|
|
94
|
+
parents.each do |factory, parent|
|
95
|
+
parent = all_parents[parent] while all_parents[parent]
|
96
|
+
model_class_name = all_factories[parent]
|
97
|
+
next unless model_class_name
|
98
|
+
|
99
|
+
@factories[path][factory] = model_class_name
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def traverse_node(node, path, parent = nil, model_class_name = nil)
|
105
|
+
return unless node.is_a?(Parser::AST::Node)
|
106
|
+
|
107
|
+
factory_node = extract_factory_node(node)
|
108
|
+
if factory_node
|
109
|
+
factory = parse_factory_node(factory_node)
|
110
|
+
parent = determine_parent(factory, parent)
|
111
|
+
model_class_name = determine_model_class_name(factory, model_class_name)
|
112
|
+
if factory_node?(node)
|
113
|
+
register_factory(path, factory.name, factory.aliases, parent, model_class_name)
|
114
|
+
return
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
node.children.each { |child| traverse_node(child, path, parent, model_class_name) }
|
119
|
+
end
|
120
|
+
|
121
|
+
def extract_factory_node(node)
|
122
|
+
return node.children[0] if factory_block?(node)
|
123
|
+
return node if factory_node?(node)
|
124
|
+
end
|
125
|
+
|
126
|
+
def register_factory(path, factory_name, aliases, parent, model_class_name)
|
127
|
+
([factory_name] + aliases).each do |name|
|
128
|
+
if parent
|
129
|
+
@parents[path][name] = parent
|
130
|
+
else
|
131
|
+
@factories[path][name] = model_class_name
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def factory_block?(node)
|
137
|
+
return false if node&.type != :block
|
138
|
+
|
139
|
+
factory_node?(node.children[0])
|
140
|
+
end
|
141
|
+
|
142
|
+
def factory_node?(node)
|
143
|
+
node&.type == :send && node.children[1] == :factory
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse_factory_node(node)
|
147
|
+
factory_name_node, factory_config_node = node.children[2..3]
|
148
|
+
|
149
|
+
name = factory_name_node.children[0]
|
150
|
+
aliases = extract_aliases(factory_config_node)
|
151
|
+
parent = extract_parent(factory_config_node)
|
152
|
+
model_class_name = extract_model_class_name(factory_config_node)
|
153
|
+
|
154
|
+
Factory.new(name, aliases, parent, model_class_name)
|
155
|
+
end
|
156
|
+
|
157
|
+
def extract_aliases(factory_config_hash_node)
|
158
|
+
aliases_array = extract_hash_value(factory_config_hash_node, :aliases)
|
159
|
+
return [] if aliases_array&.type != :array
|
160
|
+
|
161
|
+
aliases_array.children.map(&:value)
|
162
|
+
end
|
163
|
+
|
164
|
+
def extract_parent(factory_config_hash_node)
|
165
|
+
parent_node = extract_hash_value(factory_config_hash_node, :parent)
|
166
|
+
parent_node&.value
|
167
|
+
end
|
168
|
+
|
169
|
+
def extract_model_class_name(factory_config_hash_node)
|
170
|
+
model_class_name_node = extract_hash_value(factory_config_hash_node, :class)
|
171
|
+
|
172
|
+
case model_class_name_node&.type
|
173
|
+
when :const
|
174
|
+
model_class_name_node.source.sub(/^::/, '')
|
175
|
+
when :str
|
176
|
+
model_class_name_node.value.sub(/^::/, '')
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def extract_hash_value(node, hash_key)
|
181
|
+
return nil if node&.type != :hash
|
182
|
+
|
183
|
+
pairs = node.children.select { |child| child.type == :pair }
|
184
|
+
pairs.each do |pair|
|
185
|
+
key, value = pair.children
|
186
|
+
return value if key.value == hash_key
|
187
|
+
end
|
188
|
+
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
|
192
|
+
def determine_parent(factory, parent_from_surrounding_block)
|
193
|
+
# If the factory specifies an explicit model class name, we don't need
|
194
|
+
# to resolve the parent to determine the model class name.
|
195
|
+
return nil if factory.model_class_name
|
196
|
+
|
197
|
+
factory.parent || parent_from_surrounding_block
|
198
|
+
end
|
199
|
+
|
200
|
+
def determine_model_class_name(factory, model_class_name_from_surrounding_block)
|
201
|
+
factory.model_class_name ||
|
202
|
+
model_class_name_from_surrounding_block ||
|
203
|
+
ActiveSupport::Inflector.camelize(factory.name)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
# rubocop:enable Metrics/ModuleLength
|
207
|
+
end
|
208
|
+
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.1
|
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-31 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: []
|