rubocop-flexport 0.9.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/rubocop/cop/flexport/engine_api_boundary.rb +84 -17
- data/lib/rubocop/cop/flexport/global_model_access_from_engine.rb +67 -5
- data/lib/rubocop/cop/flexport_cops.rb +1 -0
- data/lib/rubocop/cop/mixin/engine_api.rb +2 -3
- data/lib/rubocop/cop/mixin/factory_bot_usage.rb +112 -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: 1111bda95a88ccde039176a94717252bb9828339ecc9ffef53134068ce9c119a
|
4
|
+
data.tar.gz: c93f725711efde4dd37de393c365c744545c4193e7cc8e433e58f23ca3915768
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8428aff55f6b2bc1bd5b9657942d5f11eb84deb9d98b8e7441db6ff2a6161169e80cfa088b8c0a05e0d54c84643b04b5bc623fa7db1bd728c3f2be17f760d11
|
7
|
+
data.tar.gz: 44ba932445440f9a79e1d5169a62ba41221529d6e5ca3ca69f538776f86fd80727371ad8a7df57045d583f45097ce9024b69d761bb1be756e5a805bda02533be
|
@@ -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
|
@@ -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,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.'
|
@@ -160,6 +168,10 @@ module RuboCop
|
|
160
168
|
(send _ {:belongs_to :has_one :has_many} sym $hash)
|
161
169
|
PATTERN
|
162
170
|
|
171
|
+
class << self
|
172
|
+
attr_accessor :factory_engines_cache
|
173
|
+
end
|
174
|
+
|
163
175
|
def on_const(node)
|
164
176
|
return if in_module_or_class_declaration?(node)
|
165
177
|
# There might be value objects that are named
|
@@ -179,19 +191,42 @@ module RuboCop
|
|
179
191
|
|
180
192
|
def on_send(node)
|
181
193
|
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)
|
194
|
+
check_for_cross_engine_rails_association(node, assocation_hash_args)
|
195
|
+
end
|
196
|
+
return unless check_for_cross_engine_factory_bot?
|
188
197
|
|
189
|
-
|
198
|
+
factory_bot_usage(node) do |factory_node|
|
199
|
+
check_for_cross_engine_factory_bot_usage(node, factory_node)
|
190
200
|
end
|
191
201
|
end
|
192
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
|
+
|
193
225
|
def external_dependency_checksum
|
194
|
-
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
|
195
230
|
end
|
196
231
|
|
197
232
|
private
|
@@ -296,14 +331,15 @@ module RuboCop
|
|
296
331
|
end
|
297
332
|
|
298
333
|
def current_engine
|
299
|
-
@current_engine ||=
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
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
|
307
343
|
end
|
308
344
|
|
309
345
|
def in_engine_file?(accessed_engine)
|
@@ -333,7 +369,7 @@ module RuboCop
|
|
333
369
|
|
334
370
|
depth = 0
|
335
371
|
max_depth = 5
|
336
|
-
while node
|
372
|
+
while node&.const_type? && depth < max_depth
|
337
373
|
full_const_name = remove_leading_colons(node.source)
|
338
374
|
return true if allowlist.include?(full_const_name)
|
339
375
|
|
@@ -393,6 +429,37 @@ module RuboCop
|
|
393
429
|
def strongly_protected_engine?(engine)
|
394
430
|
strongly_protected_engines.include?(engine)
|
395
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
|
396
463
|
end
|
397
464
|
end
|
398
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
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
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
|
-
|
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/')
|
@@ -133,6 +181,12 @@ module RuboCop
|
|
133
181
|
end
|
134
182
|
end
|
135
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, ''))
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
136
190
|
def global_model_const?(const_node)
|
137
191
|
# Remove leading `::`, if any.
|
138
192
|
class_name = const_node.source.sub(/^:*/, '')
|
@@ -144,6 +198,10 @@ module RuboCop
|
|
144
198
|
global_model_names.include?(class_name)
|
145
199
|
end
|
146
200
|
|
201
|
+
def global_factory?(factory_name)
|
202
|
+
global_factories.include?(factory_name)
|
203
|
+
end
|
204
|
+
|
147
205
|
def child_of_const?(node)
|
148
206
|
node.parent.const_type?
|
149
207
|
end
|
@@ -165,6 +223,10 @@ module RuboCop
|
|
165
223
|
end
|
166
224
|
end
|
167
225
|
|
226
|
+
def allow_global_factory_bot_from_engines
|
227
|
+
cop_config['AllowGlobalFactoryBotFromEngines'] || []
|
228
|
+
end
|
229
|
+
|
168
230
|
def allowed_global_models
|
169
231
|
cop_config['AllowedGlobalModels'] || []
|
170
232
|
end
|
@@ -62,8 +62,7 @@ module RuboCop
|
|
62
62
|
File.join(engines_path, "#{raw_name}/app/api/#{raw_name}/api/")
|
63
63
|
end
|
64
64
|
|
65
|
-
def parse_ast(
|
66
|
-
source_code = File.read(file_path)
|
65
|
+
def parse_ast(source_code)
|
67
66
|
source = RuboCop::ProcessedSource.new(source_code, RUBY_VERSION.to_f)
|
68
67
|
source.ast
|
69
68
|
end
|
@@ -103,7 +102,7 @@ module RuboCop
|
|
103
102
|
# s(:const, nil, :Trucking), :LoadTypes)), :freeze)))
|
104
103
|
#
|
105
104
|
# We want the :begin in the 2nd case, the :module in the 1st case.
|
106
|
-
module_node = parse_ast(path)
|
105
|
+
module_node = parse_ast(File.read(path))
|
107
106
|
module_block_node = module_node&.children&.[](1)
|
108
107
|
if module_block_node&.begin_type?
|
109
108
|
module_block_node
|
@@ -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
|
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.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-
|
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
|
-
|
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: []
|