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