rubocop-flexport 0.4.0 → 0.9.0

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: f0ad53cef118c00bda51659b577dd8a099fb46127d71d90df345a0eb2199aa06
4
- data.tar.gz: e94a72b0ed1f9e9ca886b25ca2fa68538edf9b08874398f9fd4e7fe5e2c50f27
3
+ metadata.gz: 611e7d0841b96380210c5b9c0e69e1faa39714f485c7534a3c99f55a9567a854
4
+ data.tar.gz: 0300627c5f7459c722941ce9f7bf6bf594cbb8c6eb6a1e26791ceeeff8651f86
5
5
  SHA512:
6
- metadata.gz: 73d6743b8da7bd0f7a325950dc312ab96c5a8eb2fd643b5fd1df8e419900996d14cddb3745ddbe204712c00d0e1dd2956ba477b95d08473ac9f977a84faf485d
7
- data.tar.gz: d7c790e2a061b3ff32f7a447569d465903e28eb8c955a73841d60c2ae240d8212fef34d4ce496a3a098ca09aa2d1fbd777cb817c76173eef1b49fa1759ecaef9
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
 
@@ -4,6 +4,7 @@ Flexport/EngineApiBoundary:
4
4
  VersionAdded: '0.3.0'
5
5
  EnginesPath: 'engines/'
6
6
  UnprotectedEngines: []
7
+ StronglyProtectedEngines: []
7
8
  EngineSpecificOverrides: []
8
9
 
9
10
  Flexport/GlobalModelAccessFromEngine:
@@ -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 a `_whitelist.rb` file in `api/`. Modules listed in
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
- MSG = 'Direct access of %<engine>s engine. ' \
114
- 'Only access engine via %<engine>s::Api.'
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
- # Similarly, you might have value objects that are named
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
- engine = extract_engine(node)
139
- return unless engine
140
- return if valid_engine_access?(node, engine)
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: format(MSG, engine: engine))
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
- engine = extract_model_engine(class_name_node)
151
- next if engine.nil?
152
- next if valid_engine_access?(node, engine)
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: format(MSG, engine: engine))
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 extract_engine(node)
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 in_module_or_class_declaration?(node)
196
- depth = 0
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 sending_method_to_namespace_itself?(node)
206
- node.parent.send_type?
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 valid_engine_access?(node, engine)
267
+ def valid_engine_api_access?(node, accessed_engine)
210
268
  (
211
- in_engine_file?(engine) ||
212
- in_legacy_dependent_file?(engine) ||
269
+ in_legacy_dependent_file?(accessed_engine) ||
213
270
  through_api?(node) ||
214
- whitelisted?(node, engine) ||
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?(engine)
254
- current_engine == engine
309
+ def in_engine_file?(accessed_engine)
310
+ current_engine == accessed_engine
255
311
  end
256
312
 
257
- def in_legacy_dependent_file?(engine)
258
- legacy_dependents = read_api_file(engine, :legacy_dependents)
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 whitelisted?(node, engine)
274
- whitelist = read_api_file(engine, :whitelist)
275
- return false if whitelist.empty?
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 whitelist.include?(full_const_name)
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['AllowedModels']
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
- model_name = node.parent.source
312
- model_names_allowed_by_override = overrides_by_engine[current_engine]
313
- return false unless model_names_allowed_by_override
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
- model_names_allowed_by_override.include?(model_name)
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
- file_path.include?(File.join(engines_path, e))
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
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'mixin/engine_api'
4
+ require_relative 'mixin/engine_node_context'
4
5
 
5
6
  require_relative 'flexport/engine_api_boundary'
6
7
  require_relative 'flexport/global_model_access_from_engine'
@@ -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: :whitelist_array
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 :whitelist_array, <<-PATTERN
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Flexport
5
- VERSION = '0.4.0'
5
+ VERSION = '0.9.0'
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.4.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: 2019-12-09 00:00:00.000000000 Z
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