rubocop-flexport 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1111bda95a88ccde039176a94717252bb9828339ecc9ffef53134068ce9c119a
4
- data.tar.gz: c93f725711efde4dd37de393c365c744545c4193e7cc8e433e58f23ca3915768
3
+ metadata.gz: b6abf69d264c77fa4c789bd8923604ad86b42e583c0ada5b24737cf4d9277d41
4
+ data.tar.gz: d198d6b916aa148e53f127ed5b74a5d1542c0fb1e9a36907a0e4d08bfc950640
5
5
  SHA512:
6
- metadata.gz: e8428aff55f6b2bc1bd5b9657942d5f11eb84deb9d98b8e7441db6ff2a6161169e80cfa088b8c0a05e0d54c84643b04b5bc623fa7db1bd728c3f2be17f760d11
7
- data.tar.gz: 44ba932445440f9a79e1d5169a62ba41221529d6e5ca3ca69f538776f86fd80727371ad8a7df57045d583f45097ce9024b69d761bb1be756e5a805bda02533be
6
+ metadata.gz: e121c375c8ee83debb69988c0e60c1014e7b7d136c88a4756bbf81cdf97791824eba9b5c75d91aa2c4b06443b555110e0f8d84462fd856f9a573fef8b4fb3736
7
+ data.tar.gz: 5e871c97c14af47395c634b9f58fce03e1cdd61eb7a0f47f981e63fd2e208043261edcb55ad2fa2c21c7b75446abe59edc8afac7040f5cfb396ff701e80995d5
@@ -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
 
@@ -43,7 +43,7 @@ module RuboCop
43
43
  #
44
44
  # The cop will complain if you use FactoryBot factories defined in other
45
45
  # engines in your engine's specs. You can disable this check by adding
46
- # the engine name to `AllowCrossEngineFactoryBotFromEngines` in
46
+ # the engine name to `FactoryBotOutboundAccessAllowedEngines` in
47
47
  # .rubocop.yml.
48
48
  #
49
49
  # # Isolation guarantee
@@ -168,10 +168,6 @@ module RuboCop
168
168
  (send _ {:belongs_to :has_one :has_many} sym $hash)
169
169
  PATTERN
170
170
 
171
- class << self
172
- attr_accessor :factory_engines_cache
173
- end
174
-
175
171
  def on_const(node)
176
172
  return if in_module_or_class_declaration?(node)
177
173
  # There might be value objects that are named
@@ -214,7 +210,7 @@ module RuboCop
214
210
  def check_for_cross_engine_factory_bot_usage(node, factory_node)
215
211
  factory = factory_node.children[0]
216
212
  accessed_engine, model_class_name = factory_engines[factory]
217
- return if accessed_engine.nil?
213
+ return if accessed_engine.nil? || !protected_engines.include?(accessed_engine)
218
214
 
219
215
  model_class_node = parse_ast(model_class_name)
220
216
  return if valid_engine_access?(model_class_node, accessed_engine)
@@ -430,34 +426,34 @@ module RuboCop
430
426
  strongly_protected_engines.include?(engine)
431
427
  end
432
428
 
433
- def allow_cross_engine_factory_bot_from_engines
434
- @allow_cross_engine_factory_bot_from_engines ||=
435
- camelize_all(cop_config['AllowCrossEngineFactoryBotFromEngines'] || [])
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
436
  end
437
437
 
438
438
  def check_for_cross_engine_factory_bot?
439
- spec_file? && !allow_cross_engine_factory_bot_from_engines.include?(current_engine)
439
+ spec_file? &&
440
+ factory_bot_enabled? &&
441
+ !factory_bot_outbound_access_allowed_engines.include?(current_engine)
440
442
  end
441
443
 
442
444
  # Maps factories to the engine where they are defined.
443
445
  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|
446
+ @factory_engines ||= find_factories.each_with_object({}) do |factory_file, h|
447
+ path, factories = factory_file
447
448
  engine_name = engine_name_from_path(path)
448
- ast = parse_ast(File.read(path))
449
- find_factories(ast).each do |factory, model_class_name|
449
+ factories.each do |factory, model_class_name|
450
450
  h[factory] = [engine_name, model_class_name]
451
451
  end
452
452
  end
453
453
  end
454
454
 
455
- def spec_factory_paths
456
- @spec_factory_paths ||= Dir["#{engines_path}*/spec/factories/**/*.rb"]
457
- end
458
-
459
455
  def spec_factories_modified_time_checksum
460
- mtimes = spec_factory_paths.sort.map { |f| File.mtime(f) }
456
+ mtimes = factory_files.sort.map { |f| File.mtime(f) }
461
457
  Digest::SHA1.hexdigest(mtimes.join)
462
458
  end
463
459
  end
@@ -47,7 +47,7 @@ module RuboCop
47
47
  #
48
48
  # This cop will also complain if you try to use global FactoryBot
49
49
  # factories in your engine's specs. To disable this behavior for your
50
- # engine, add it to the `AllowGlobalFactoryBotFromEngines` list in
50
+ # engine, add it to the `FactoryBotGlobalAccessAllowedEngines` list in
51
51
  # .rubocop.yml.
52
52
  #
53
53
  class GlobalModelAccessFromEngine < Cop
@@ -61,10 +61,6 @@ module RuboCop
61
61
  (send _ {:belongs_to :has_one :has_many} sym $hash)
62
62
  PATTERN
63
63
 
64
- class << self
65
- attr_accessor :global_factories_cache
66
- end
67
-
68
64
  def on_const(node)
69
65
  return unless in_enforced_engine_file?
70
66
  return unless global_model_const?(node)
@@ -127,25 +123,14 @@ module RuboCop
127
123
  end
128
124
 
129
125
  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
126
+ @global_factories ||=
127
+ find_factories.reject { |path| path.start_with?(engines_path) }.values.reduce(:merge)
139
128
  end
140
129
 
141
130
  def model_dir_paths
142
131
  Dir[File.join(global_models_path, '**/*.rb')]
143
132
  end
144
133
 
145
- def spec_factory_paths
146
- @spec_factory_paths ||= Dir['spec/factories/**/*.rb']
147
- end
148
-
149
134
  def calculate_global_models
150
135
  all_model_paths = model_dir_paths.reject do |path|
151
136
  path.include?('/concerns/')
@@ -182,7 +167,7 @@ module RuboCop
182
167
  end
183
168
 
184
169
  def check_for_global_factory_bot?
185
- spec_file? && allow_global_factory_bot_from_engines.none? do |engine|
170
+ spec_file? && factory_bot_enabled? && factory_bot_global_access_allowed_engines.none? do |engine|
186
171
  processed_source.path.include?(File.join(engines_path, engine, ''))
187
172
  end
188
173
  end
@@ -223,8 +208,12 @@ module RuboCop
223
208
  end
224
209
  end
225
210
 
226
- def allow_global_factory_bot_from_engines
227
- cop_config['AllowGlobalFactoryBotFromEngines'] || []
211
+ def factory_bot_global_access_allowed_engines
212
+ cop_config['FactoryBotGlobalAccessAllowedEngines'] || []
213
+ end
214
+
215
+ def factory_bot_enabled?
216
+ cop_config['FactoryBotEnabled']
228
217
  end
229
218
 
230
219
  def allowed_global_models
@@ -5,9 +5,12 @@ require 'active_support/inflector'
5
5
  module RuboCop
6
6
  module Cop
7
7
  # Helpers for detecting FactoryBot usage.
8
+ # rubocop:disable Metrics/ModuleLength
8
9
  module FactoryBotUsage
9
10
  extend NodePattern::Macros
10
11
 
12
+ Factory = Struct.new('Factory', :name, :aliases, :parent, :model_class_name)
13
+
11
14
  FACTORY_BOT_METHODS = %i[
12
15
  attributes_for
13
16
  attributes_for_list
@@ -25,36 +28,111 @@ module RuboCop
25
28
  (send _ {#{FACTORY_BOT_METHODS.map(&:inspect).join(' ')}} $sym)
26
29
  PATTERN
27
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
+
28
46
  def spec_file?
29
47
  processed_source&.path&.match?(/_spec\.rb$/) || false
30
48
  end
31
49
 
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)
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)
37
106
 
38
107
  factory_node = extract_factory_node(node)
39
108
  if factory_node
40
- factory_name, aliases, model_class_name = parse_factory_node(factory_node, model_class_name)
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)
41
112
  if factory_node?(node)
42
- ([factory_name] + aliases).each do |name|
43
- factories << [name, model_class_name]
44
- end
113
+ register_factory(path, factory.name, factory.aliases, parent, model_class_name)
114
+ return
45
115
  end
46
116
  end
47
117
 
48
- factories + node.children.flat_map { |child| find_factories(child, model_class_name) }
118
+ node.children.each { |child| traverse_node(child, path, parent, model_class_name) }
49
119
  end
50
120
 
51
- private
52
-
53
121
  def extract_factory_node(node)
54
122
  return node.children[0] if factory_block?(node)
55
123
  return node if factory_node?(node)
56
124
  end
57
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
+
58
136
  def factory_block?(node)
59
137
  return false if node&.type != :block
60
138
 
@@ -65,17 +143,15 @@ module RuboCop
65
143
  node&.type == :send && node.children[1] == :factory
66
144
  end
67
145
 
68
- def parse_factory_node(node, model_class_name_from_parent_factory = nil)
146
+ def parse_factory_node(node)
69
147
  factory_name_node, factory_config_node = node.children[2..3]
70
148
 
71
- factory_name = factory_name_node.children[0]
149
+ name = factory_name_node.children[0]
72
150
  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)
151
+ parent = extract_parent(factory_config_node)
152
+ model_class_name = extract_model_class_name(factory_config_node)
77
153
 
78
- [factory_name, aliases, model_class_name]
154
+ Factory.new(name, aliases, parent, model_class_name)
79
155
  end
80
156
 
81
157
  def extract_aliases(factory_config_hash_node)
@@ -85,6 +161,11 @@ module RuboCop
85
161
  aliases_array.children.map(&:value)
86
162
  end
87
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
+
88
169
  def extract_model_class_name(factory_config_hash_node)
89
170
  model_class_name_node = extract_hash_value(factory_config_hash_node, :class)
90
171
 
@@ -107,6 +188,21 @@ module RuboCop
107
188
 
108
189
  nil
109
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
110
205
  end
206
+ # rubocop:enable Metrics/ModuleLength
111
207
  end
112
208
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Flexport
5
- VERSION = '0.10.0'
5
+ VERSION = '0.10.1'
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.10.0
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Flexport Engineering
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-28 00:00:00.000000000 Z
11
+ date: 2020-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport