rubocop-flexport 0.4.0 → 0.9.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/README.md +1 -1
- data/config/default.yml +1 -0
- data/lib/rubocop/cop/flexport/engine_api_boundary.rb +129 -49
- data/lib/rubocop/cop/flexport/global_model_access_from_engine.rb +6 -1
- data/lib/rubocop/cop/flexport_cops.rb +1 -0
- data/lib/rubocop/cop/mixin/engine_api.rb +6 -2
- data/lib/rubocop/cop/mixin/engine_node_context.rb +26 -0
- data/lib/rubocop/flexport/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 611e7d0841b96380210c5b9c0e69e1faa39714f485c7534a3c99f55a9567a854
|
4
|
+
data.tar.gz: 0300627c5f7459c722941ce9f7bf6bf594cbb8c6eb6a1e26791ceeeff8651f86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b23d6b05fe8ad73bd029a38b2fa9c5e957ac50433510e0b7f4b37ee94b1ac3148b8f477a4de297c194fc7f136d40168182f1c0a1442218e27aa9070b60c20e5c
|
7
|
+
data.tar.gz: 0b855624ddcfae29e8d211a2e05e54d401a0d06cbfd5e4a118ad6a3bccef1200e409d22f35380cbbf44ec3fc180ed99b34111978042437674675d30e864c651c
|
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
|
|
data/config/default.yml
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# rubocop:disable Metrics/ClassLength
|
3
4
|
module RuboCop
|
4
5
|
module Cop
|
5
6
|
module Flexport
|
@@ -16,7 +17,7 @@ module RuboCop
|
|
16
17
|
# will be accessible outside your engine. For example, adding
|
17
18
|
# `api/foo_service.rb` will allow code outside your engine to
|
18
19
|
# invoke eg `MyEngine::Api::FooService.bar(baz)`.
|
19
|
-
# - Create
|
20
|
+
# - Create an `_allowlist.rb` or `_whitelist.rb` file in `api/`. Modules listed in
|
20
21
|
# this file are accessible to code outside the engine. The file
|
21
22
|
# must have this name and a particular format (see below).
|
22
23
|
#
|
@@ -80,6 +81,36 @@ module RuboCop
|
|
80
81
|
# end
|
81
82
|
# ```
|
82
83
|
#
|
84
|
+
# # "StronglyProtectedEngines" parameter
|
85
|
+
#
|
86
|
+
# The Engine API is not actually a network API surface. Method invocations
|
87
|
+
# may happen synchronously and assume they are part of the same
|
88
|
+
# transaction. So if your engine is using modules whitelisted by
|
89
|
+
# other engines, then you cannot extract your engine code into a
|
90
|
+
# separate network-isolated service (even though within a big Rails
|
91
|
+
# monolith using engines the cross-engine method call might have been
|
92
|
+
# acceptable).
|
93
|
+
#
|
94
|
+
# The "StronglyProtectedEngines" parameter helps in the case you want to
|
95
|
+
# extract your engine completely. If your engine is listed as a strongly
|
96
|
+
# protected engine, then the following additional restricts apply:
|
97
|
+
#
|
98
|
+
# (1) Any use of your engine's code by code outside your engine is
|
99
|
+
# considered a violation, regardless of *your* _legacy_dependents.rb,
|
100
|
+
# _whitelist.rb, or engine API module. (no inbound access)
|
101
|
+
# (2) Any use of other engines' code within your engine is considered
|
102
|
+
# a violation, regardless of *their* _legacy_dependents.rb,
|
103
|
+
# _whitelist.rb, or engine API module. (no outbound access)
|
104
|
+
#
|
105
|
+
# (Note: "EngineSpecificOverrides" parameter still has effect.)
|
106
|
+
#
|
107
|
+
# # "EngineSpecificOverrides" parameter
|
108
|
+
#
|
109
|
+
# This parameter allows defining bi-lateral private "APIs" between
|
110
|
+
# engines. See example in global_model_access_from_engine_spec.rb.
|
111
|
+
# This may be useful if you plan to extract several engines into the
|
112
|
+
# same network-isolated service.
|
113
|
+
#
|
83
114
|
# @example
|
84
115
|
#
|
85
116
|
# # bad
|
@@ -109,25 +140,29 @@ module RuboCop
|
|
109
140
|
#
|
110
141
|
class EngineApiBoundary < Cop
|
111
142
|
include EngineApi
|
143
|
+
include EngineNodeContext
|
144
|
+
|
145
|
+
MSG = 'Direct access of %<accessed_engine>s engine. ' \
|
146
|
+
'Only access engine via %<accessed_engine>s::Api.'
|
147
|
+
|
148
|
+
STRONGLY_PROTECTED_MSG = 'All direct access of ' \
|
149
|
+
'%<accessed_engine>s engine disallowed because ' \
|
150
|
+
'it is in StronglyProtectedEngines list.'
|
112
151
|
|
113
|
-
|
114
|
-
'
|
152
|
+
STRONGLY_PROTECTED_CURRENT_MSG = 'Direct ' \
|
153
|
+
'access of %<accessed_engine>s is disallowed in this file ' \
|
154
|
+
'because it\'s in the %<current_engine>s engine, which ' \
|
155
|
+
'is in the StronglyProtectedEngines list.'
|
156
|
+
|
157
|
+
MAIN_APP_NAME = 'MainApp::EngineApi'
|
115
158
|
|
116
159
|
def_node_matcher :rails_association_hash_args, <<-PATTERN
|
117
160
|
(send _ {:belongs_to :has_one :has_many} sym $hash)
|
118
161
|
PATTERN
|
119
162
|
|
120
163
|
def on_const(node)
|
121
|
-
# Sometimes modules/class are declared with the same name as an
|
122
|
-
# engine. For example, you might have:
|
123
|
-
#
|
124
|
-
# /engines/foo
|
125
|
-
# /app/graph/types/foo
|
126
|
-
#
|
127
|
-
# We ignore instead of yielding false positive for the module
|
128
|
-
# declaration in the latter.
|
129
164
|
return if in_module_or_class_declaration?(node)
|
130
|
-
#
|
165
|
+
# There might be value objects that are named
|
131
166
|
# the same as engines like:
|
132
167
|
#
|
133
168
|
# Warehouse.new
|
@@ -135,11 +170,11 @@ module RuboCop
|
|
135
170
|
# We don't want to warn on these cases either.
|
136
171
|
return if sending_method_to_namespace_itself?(node)
|
137
172
|
|
138
|
-
|
139
|
-
return unless
|
140
|
-
return if valid_engine_access?(node,
|
173
|
+
accessed_engine = extract_accessed_engine(node)
|
174
|
+
return unless accessed_engine
|
175
|
+
return if valid_engine_access?(node, accessed_engine)
|
141
176
|
|
142
|
-
add_offense(node, message:
|
177
|
+
add_offense(node, message: message(accessed_engine))
|
143
178
|
end
|
144
179
|
|
145
180
|
def on_send(node)
|
@@ -147,11 +182,11 @@ module RuboCop
|
|
147
182
|
class_name_node = extract_class_name_node(assocation_hash_args)
|
148
183
|
next if class_name_node.nil?
|
149
184
|
|
150
|
-
|
151
|
-
next if
|
152
|
-
next if valid_engine_access?(node,
|
185
|
+
accessed_engine = extract_model_engine(class_name_node)
|
186
|
+
next if accessed_engine.nil?
|
187
|
+
next if valid_engine_access?(node, accessed_engine)
|
153
188
|
|
154
|
-
add_offense(class_name_node, message:
|
189
|
+
add_offense(class_name_node, message: message(accessed_engine))
|
155
190
|
end
|
156
191
|
end
|
157
192
|
|
@@ -161,12 +196,35 @@ module RuboCop
|
|
161
196
|
|
162
197
|
private
|
163
198
|
|
164
|
-
def
|
199
|
+
def message(accessed_engine)
|
200
|
+
if strongly_protected_engine?(accessed_engine)
|
201
|
+
format(STRONGLY_PROTECTED_MSG, accessed_engine: accessed_engine)
|
202
|
+
elsif strongly_protected_engine?(current_engine)
|
203
|
+
format(
|
204
|
+
STRONGLY_PROTECTED_CURRENT_MSG,
|
205
|
+
accessed_engine: accessed_engine,
|
206
|
+
current_engine: current_engine
|
207
|
+
)
|
208
|
+
else
|
209
|
+
format(MSG, accessed_engine: accessed_engine)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def extract_accessed_engine(node)
|
214
|
+
return MAIN_APP_NAME if disallowed_main_app_access?(node)
|
165
215
|
return nil unless protected_engines.include?(node.const_name)
|
166
216
|
|
167
217
|
node.const_name
|
168
218
|
end
|
169
219
|
|
220
|
+
def disallowed_main_app_access?(node)
|
221
|
+
strongly_protected_engine?(current_engine) && main_app_access?(node)
|
222
|
+
end
|
223
|
+
|
224
|
+
def main_app_access?(node)
|
225
|
+
node.const_name.start_with?(MAIN_APP_NAME)
|
226
|
+
end
|
227
|
+
|
170
228
|
def engines_path
|
171
229
|
path = cop_config['EnginesPath']
|
172
230
|
path += '/' unless path.end_with?('/')
|
@@ -192,27 +250,25 @@ module RuboCop
|
|
192
250
|
names.map { |n| ActiveSupport::Inflector.camelize(n) }
|
193
251
|
end
|
194
252
|
|
195
|
-
def
|
196
|
-
|
197
|
-
max_depth = 10
|
198
|
-
while node.const_type? && depth < max_depth
|
199
|
-
node = node.parent
|
200
|
-
depth += 1
|
201
|
-
end
|
202
|
-
node.module_type? || node.class_type?
|
253
|
+
def sending_method_to_namespace_itself?(node)
|
254
|
+
node.parent&.send_type?
|
203
255
|
end
|
204
256
|
|
205
|
-
def
|
206
|
-
|
257
|
+
def valid_engine_access?(node, accessed_engine)
|
258
|
+
return true if in_engine_file?(accessed_engine)
|
259
|
+
return true if engine_specific_override?(node)
|
260
|
+
|
261
|
+
return false if strongly_protected_engine?(current_engine)
|
262
|
+
return false if strongly_protected_engine?(accessed_engine)
|
263
|
+
|
264
|
+
valid_engine_api_access?(node, accessed_engine)
|
207
265
|
end
|
208
266
|
|
209
|
-
def
|
267
|
+
def valid_engine_api_access?(node, accessed_engine)
|
210
268
|
(
|
211
|
-
|
212
|
-
in_legacy_dependent_file?(engine) ||
|
269
|
+
in_legacy_dependent_file?(accessed_engine) ||
|
213
270
|
through_api?(node) ||
|
214
|
-
|
215
|
-
engine_specific_override?(node)
|
271
|
+
allowlisted?(node, accessed_engine)
|
216
272
|
)
|
217
273
|
end
|
218
274
|
|
@@ -250,12 +306,12 @@ module RuboCop
|
|
250
306
|
end
|
251
307
|
end
|
252
308
|
|
253
|
-
def in_engine_file?(
|
254
|
-
current_engine ==
|
309
|
+
def in_engine_file?(accessed_engine)
|
310
|
+
current_engine == accessed_engine
|
255
311
|
end
|
256
312
|
|
257
|
-
def in_legacy_dependent_file?(
|
258
|
-
legacy_dependents = read_api_file(
|
313
|
+
def in_legacy_dependent_file?(accessed_engine)
|
314
|
+
legacy_dependents = read_api_file(accessed_engine, :legacy_dependents)
|
259
315
|
# The file names are strings so we need to remove the escaped quotes
|
260
316
|
# on either side from the source code.
|
261
317
|
legacy_dependents = legacy_dependents.map do |source|
|
@@ -270,15 +326,16 @@ module RuboCop
|
|
270
326
|
node.parent&.const_type? && node.parent.children.last == :Api
|
271
327
|
end
|
272
328
|
|
273
|
-
def
|
274
|
-
|
275
|
-
|
329
|
+
def allowlisted?(node, engine)
|
330
|
+
allowlist = read_api_file(engine, :allowlist)
|
331
|
+
allowlist = read_api_file(engine, :whitelist) if allowlist.empty?
|
332
|
+
return false if allowlist.empty?
|
276
333
|
|
277
334
|
depth = 0
|
278
335
|
max_depth = 5
|
279
336
|
while node.const_type? && depth < max_depth
|
280
337
|
full_const_name = remove_leading_colons(node.source)
|
281
|
-
return true if
|
338
|
+
return true if allowlist.include?(full_const_name)
|
282
339
|
|
283
340
|
node = node.parent
|
284
341
|
depth += 1
|
@@ -302,19 +359,42 @@ module RuboCop
|
|
302
359
|
|
303
360
|
raw_overrides.each do |raw_override|
|
304
361
|
engine = ActiveSupport::Inflector.camelize(raw_override['Engine'])
|
305
|
-
overrides_by_engine[engine] = raw_override['
|
362
|
+
overrides_by_engine[engine] = raw_override['AllowedModules']
|
306
363
|
end
|
307
364
|
overrides_by_engine
|
308
365
|
end
|
309
366
|
|
310
367
|
def engine_specific_override?(node)
|
311
|
-
|
312
|
-
|
313
|
-
|
368
|
+
return false unless overrides_for_current_engine
|
369
|
+
|
370
|
+
depth = 0
|
371
|
+
max_depth = 5
|
372
|
+
while node&.const_type? && depth < max_depth
|
373
|
+
module_name = node.source
|
374
|
+
return true if overrides_for_current_engine.include?(module_name)
|
375
|
+
|
376
|
+
node = node.parent
|
377
|
+
depth += 1
|
378
|
+
end
|
379
|
+
false
|
380
|
+
end
|
381
|
+
|
382
|
+
def overrides_for_current_engine
|
383
|
+
overrides_by_engine[current_engine]
|
384
|
+
end
|
385
|
+
|
386
|
+
def strongly_protected_engines
|
387
|
+
@strongly_protected_engines ||= begin
|
388
|
+
strongly_protected = cop_config['StronglyProtectedEngines'] || []
|
389
|
+
camelize_all(strongly_protected)
|
390
|
+
end
|
391
|
+
end
|
314
392
|
|
315
|
-
|
393
|
+
def strongly_protected_engine?(engine)
|
394
|
+
strongly_protected_engines.include?(engine)
|
316
395
|
end
|
317
396
|
end
|
318
397
|
end
|
319
398
|
end
|
320
399
|
end
|
400
|
+
# rubocop:enable Metrics/ClassLength
|
@@ -46,6 +46,8 @@ module RuboCop
|
|
46
46
|
# end
|
47
47
|
#
|
48
48
|
class GlobalModelAccessFromEngine < Cop
|
49
|
+
include EngineNodeContext
|
50
|
+
|
49
51
|
MSG = 'Direct access of global model `%<model>s` ' \
|
50
52
|
'from within Rails Engine.'
|
51
53
|
|
@@ -58,6 +60,7 @@ module RuboCop
|
|
58
60
|
return unless global_model_const?(node)
|
59
61
|
# The cop allows access to e.g. MyGlobalModel::MY_CONST.
|
60
62
|
return if child_of_const?(node)
|
63
|
+
return if in_module_or_class_declaration?(node)
|
61
64
|
|
62
65
|
add_offense(node, message: message(node.source))
|
63
66
|
end
|
@@ -124,7 +127,9 @@ module RuboCop
|
|
124
127
|
|
125
128
|
def in_disabled_engine?(file_path)
|
126
129
|
disabled_engines.any? do |e|
|
127
|
-
|
130
|
+
# Add trailing / to engine path to avoid incorrectly
|
131
|
+
# matching engines with similar names
|
132
|
+
file_path.include?(File.join(engines_path, e, ''))
|
128
133
|
end
|
129
134
|
end
|
130
135
|
|
@@ -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: :
|
19
|
+
array_matcher: :allowlist_array
|
16
20
|
},
|
17
21
|
legacy_dependents: {
|
18
22
|
file_basename: '_legacy_dependents.rb',
|
@@ -108,7 +112,7 @@ module RuboCop
|
|
108
112
|
end
|
109
113
|
end
|
110
114
|
|
111
|
-
def_node_matcher :
|
115
|
+
def_node_matcher :allowlist_array, <<-PATTERN
|
112
116
|
(casgn nil? {:PUBLIC_MODULES :PUBLIC_SERVICES :PUBLIC_CONSTANTS :PUBLIC_TYPES} {$array (send $array ...)})
|
113
117
|
PATTERN
|
114
118
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
# Helpers for determining the context of a node for engine violations.
|
6
|
+
module EngineNodeContext
|
7
|
+
# Sometimes modules/class are declared with the same name as an
|
8
|
+
# engine or global model. For example, you might have both:
|
9
|
+
#
|
10
|
+
# /engines/foo
|
11
|
+
# /app/graph/types/foo
|
12
|
+
#
|
13
|
+
# We ignore instead of yielding false positive for the module
|
14
|
+
# declaration in the latter.
|
15
|
+
def in_module_or_class_declaration?(node)
|
16
|
+
depth = 0
|
17
|
+
max_depth = 10
|
18
|
+
while node.const_type? && node.parent && depth < max_depth
|
19
|
+
node = node.parent
|
20
|
+
depth += 1
|
21
|
+
end
|
22
|
+
node.module_type? || node.class_type?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
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.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Flexport Engineering
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-06-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -56,6 +56,7 @@ files:
|
|
56
56
|
- lib/rubocop/cop/flexport/new_global_model.rb
|
57
57
|
- lib/rubocop/cop/flexport_cops.rb
|
58
58
|
- lib/rubocop/cop/mixin/engine_api.rb
|
59
|
+
- lib/rubocop/cop/mixin/engine_node_context.rb
|
59
60
|
- lib/rubocop/flexport.rb
|
60
61
|
- lib/rubocop/flexport/inject.rb
|
61
62
|
- lib/rubocop/flexport/version.rb
|