rubocop-flexport 0.9.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 +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: []
|