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 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: []