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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7bfc3b432b328659f2ac2dd842e48c05028482c7538fcd2f03a33dbbee4b561
4
- data.tar.gz: b9fade3c98f628825b648b184fba3c6317c94476f2c4de1791bdf2aab7e7b372
3
+ metadata.gz: 1111bda95a88ccde039176a94717252bb9828339ecc9ffef53134068ce9c119a
4
+ data.tar.gz: c93f725711efde4dd37de393c365c744545c4193e7cc8e433e58f23ca3915768
5
5
  SHA512:
6
- metadata.gz: 3a7687a4bf47c4f476006a00c7852d5c008daeb88693949b2ff182e28f46c6344fba487ed6e86cda52fc454a7cc5266ae33b47b98829494c96c8e902746c8faa
7
- data.tar.gz: 01561c084ee90550946391998f5a28bf0959a2758ece148762714ccb28bb6db2300c8829c13776544a969fab377b136b47b864a8fab76d50461f0d4af9d821f3
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 a `_whitelist.rb` file in `api/`. Modules listed in
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 %<engine>s engine. ' \
146
- 'Only access engine via %<engine>s::Api.'
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
- '%<engine>s engine disallowed because ' \
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 other engines is disallowed in this file because ' \
154
- 'it\'s in the %<engine>s engine, which ' \
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
- engine = extract_engine(node)
172
- return unless engine
173
- return if valid_engine_access?(node, engine)
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(node, engine))
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
- class_name_node = extract_class_name_node(assocation_hash_args)
181
- next if class_name_node.nil?
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
- add_offense(class_name_node, message: message(node, engine))
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(_node, engine)
198
- if strongly_protected_engine?(engine)
199
- format(STRONGLY_PROTECTED_MSG, engine: engine)
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(STRONGLY_PROTECTED_CURRENT_MSG, engine: current_engine)
238
+ format(
239
+ STRONGLY_PROTECTED_CURRENT_MSG,
240
+ accessed_engine: accessed_engine,
241
+ current_engine: current_engine
242
+ )
202
243
  else
203
- format(MSG, engine: engine)
244
+ format(MSG, accessed_engine: accessed_engine)
204
245
  end
205
246
  end
206
247
 
207
- def extract_engine(node)
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.send_type?
289
+ node.parent&.send_type?
240
290
  end
241
291
 
242
- def valid_engine_access?(node, engine)
243
- return true if in_engine_file?(engine)
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?(engine)
297
+ return false if strongly_protected_engine?(accessed_engine)
248
298
 
249
- valid_engine_api_access?(node, engine)
299
+ valid_engine_api_access?(node, accessed_engine)
250
300
  end
251
301
 
252
- def valid_engine_api_access?(node, engine)
302
+ def valid_engine_api_access?(node, accessed_engine)
253
303
  (
254
- in_legacy_dependent_file?(engine) ||
304
+ in_legacy_dependent_file?(accessed_engine) ||
255
305
  through_api?(node) ||
256
- whitelisted?(node, engine)
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 ||= begin
285
- file_path = processed_source.path
286
- if file_path&.include?(engines_path)
287
- parts = file_path.split(engines_path)
288
- engine_dir = parts.last.split('/').first
289
- ActiveSupport::Inflector.camelize(engine_dir) if engine_dir
290
- end
291
- end
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?(engine)
295
- current_engine == engine
345
+ def in_engine_file?(accessed_engine)
346
+ current_engine == accessed_engine
296
347
  end
297
348
 
298
- def in_legacy_dependent_file?(engine)
299
- legacy_dependents = read_api_file(engine, :legacy_dependents)
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 whitelisted?(node, engine)
315
- whitelist = read_api_file(engine, :whitelist)
316
- return false if whitelist.empty?
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.const_type? && depth < max_depth
372
+ while node&.const_type? && depth < max_depth
321
373
  full_const_name = remove_leading_colons(node.source)
322
- return true if whitelist.include?(full_const_name)
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
- module_name = node.parent.source
353
- module_names_allowed_by_override = overrides_by_engine[current_engine]
354
- return false unless module_names_allowed_by_override
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
- module_names_allowed_by_override.include?(module_name)
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
- class_name_node = extract_class_name_node(assocation_hash_args)
73
- class_name = class_name_node&.value
74
- next unless global_model?(class_name)
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
- add_offense(class_name_node, message: message(class_name))
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
- Digest::SHA1.hexdigest(model_dir_paths.join)
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
- file_path.include?(File.join(engines_path, e))
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'mixin/engine_api'
4
4
  require_relative 'mixin/engine_node_context'
5
+ require_relative 'mixin/factory_bot_usage'
5
6
 
6
7
  require_relative 'flexport/engine_api_boundary'
7
8
  require_relative 'flexport/global_model_access_from_engine'
@@ -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: :whitelist_array
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(file_path)
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 :whitelist_array, <<-PATTERN
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
 
@@ -15,7 +15,7 @@ module RuboCop
15
15
  def in_module_or_class_declaration?(node)
16
16
  depth = 0
17
17
  max_depth = 10
18
- while node.const_type? && depth < max_depth
18
+ while node.const_type? && node.parent && depth < max_depth
19
19
  node = node.parent
20
20
  depth += 1
21
21
  end
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Flexport
5
- VERSION = '0.5.0'
5
+ VERSION = '0.10.0'
6
6
  end
7
7
  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.5.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-01-30 00:00:00.000000000 Z
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
- rubyforge_project:
83
- rubygems_version: 2.7.6
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: []