rubocop-flexport 0.10.0 → 0.10.1

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