rubocop-flexport 0.1.0 → 0.2.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: a599585e57397ed31a361c5267c82ebe7ac4d7e0f673712e31369f135ec0fded
4
- data.tar.gz: c9d92a894a7940d3cea6bec075b5e8934d0a79003e59fe58da9925caa74ab46c
3
+ metadata.gz: 21c9588d65a1cc1b11a85277a94b02caf3dd37bb067b8a2ae09b53da39052bb0
4
+ data.tar.gz: 2ca05dbe05457065dcfd40e703e6a9c29340fd44a20862df21364ebc1c31e76f
5
5
  SHA512:
6
- metadata.gz: c596496c5e30b66d43f587a115d948ccea67549e116f4661337a101132a0776909be6329f2af13a5cf2ea115321787fbd589f00f6e20713cd012a48275d7db5e
7
- data.tar.gz: 88aadf6198c2e703e2bf016d81059ec6e2309889ccaba9be6d10b9a458a8072a72baef8df08d0b0bbac66e2059a4a05b8af365439e76b30c8b2c498ad8c4815a
6
+ metadata.gz: 375ce90040f7a4185de7bc785867b46574dbc04a24ac0f17d8d3213075b9f689c60aec9e127830aea48005f76ca3f8a8023948a792284f45460fc80ce704ff87
7
+ data.tar.gz: 560c5d8655494e752fae1db186c5c7ee1d514fa938fb977c07d09a1c87b9edcffc17d9bec31aa6b42a25436df480f63e70caee00bc3b52f3cdd786cb18962aaf
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Rubocop::Flexport
1
+ # RuboCop::Flexport
2
2
 
3
3
  This repo is for cops developed at Flexport that don't make sense to upstream
4
4
  into any of the existing RuboCop repos. When possible, we prefer upstreaming.
data/config/default.yml CHANGED
@@ -1,3 +1,15 @@
1
+ Flexport/GlobalModelAccessFromEngine:
2
+ Description: 'Do not directly access global models from within Rails Engines.'
3
+ Enabled: false
4
+ VersionAdded: '0.2.0'
5
+ AutoCorrect: false
6
+ EnginesPath: engines/
7
+ GlobalModelsPath: app/models/
8
+ DisabledEngines: []
9
+ AllowedGlobalModels: []
10
+ Include:
11
+ - '**/*.rb'
12
+
1
13
  Flexport/NewGlobalModel:
2
14
  Description: 'Disallows addition of new global models to `app/models`. Prefer Rails Engines or namespaces.'
3
15
  Enabled: true
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/inflector'
4
+ require 'digest/sha1'
5
+
6
+ module RuboCop
7
+ module Cop
8
+ module Flexport
9
+ # This cop checks for engines reaching directly into app/ models.
10
+ #
11
+ # With an ActiveRecord object, engine code can perform arbitrary
12
+ # reads and arbitrary writes to models located in the main `app/`
13
+ # directory. This cop helps isolate Rails Engine code to ensure
14
+ # that modular boundaries are respected.
15
+ #
16
+ # Checks for both access via `MyGlobalModel.foo` and associations.
17
+ #
18
+ # @example
19
+ #
20
+ # # bad
21
+ #
22
+ # class MyEngine::MyService
23
+ # m = SomeGlobalModel.find(123)
24
+ # m.any_random_attribute = "whatever i want"
25
+ # m.save
26
+ # end
27
+ #
28
+ # # good
29
+ #
30
+ # class MyEngine::MyService
31
+ # ApiServiceForGlobalModels.perform_a_supported_operation("foo")
32
+ # end
33
+ #
34
+ # @example
35
+ #
36
+ # # bad
37
+ #
38
+ # class MyEngine::MyModel < ApplicationModel
39
+ # has_one :some_global_model, class_name: "SomeGlobalModel"
40
+ # end
41
+ #
42
+ # # good
43
+ #
44
+ # class MyEngine::MyModel < ApplicationModel
45
+ # # No direct association to global models.
46
+ # end
47
+ #
48
+ class GlobalModelAccessFromEngine < Cop
49
+ MSG = 'Direct access of global model from within Rails Engine.'
50
+
51
+ def_node_matcher :rails_association_hash_args, <<-PATTERN
52
+ (send _ {:belongs_to :has_one :has_many} sym $hash)
53
+ PATTERN
54
+
55
+ def on_const(node)
56
+ return unless in_enforced_engine_file?
57
+ return unless global_model_const?(node)
58
+ # The cop allows access to e.g. MyGlobalModel::MY_CONST.
59
+ return if child_of_const?(node)
60
+
61
+ add_offense(node)
62
+ end
63
+
64
+ def on_send(node)
65
+ return unless in_enforced_engine_file?
66
+
67
+ rails_association_hash_args(node) do |assocation_hash_args|
68
+ class_name_node = extract_class_name_node(assocation_hash_args)
69
+ class_name = class_name_node&.value
70
+ next unless global_model?(class_name)
71
+
72
+ add_offense(class_name_node)
73
+ end
74
+ end
75
+
76
+ # Because this cop's behavior depends on the state of external files,
77
+ # we override this method to bust the RuboCop cache when those files
78
+ # change.
79
+ def external_dependency_checksum
80
+ Digest::SHA1.hexdigest(model_dir_paths.join)
81
+ end
82
+
83
+ private
84
+
85
+ def global_model_names
86
+ @global_model_names ||= calculate_global_models
87
+ end
88
+
89
+ def model_dir_paths
90
+ Dir[File.join(global_models_path, '**/*.rb')]
91
+ end
92
+
93
+ def calculate_global_models
94
+ all_model_paths = model_dir_paths.reject do |path|
95
+ path.include?('/concerns/')
96
+ end
97
+ all_models = all_model_paths.map do |path|
98
+ # Translates `app/models/foo/bar_baz.rb` to `Foo::BarBaz`.
99
+ file_name = path.gsub(global_models_path, '').gsub('.rb', '')
100
+ ActiveSupport::Inflector.classify(file_name)
101
+ end
102
+ all_models - allowed_global_models
103
+ end
104
+
105
+ def extract_class_name_node(assocation_hash_args)
106
+ assocation_hash_args.each_pair do |key, value|
107
+ return value if key.value == :class_name && value.str_type?
108
+ end
109
+ nil
110
+ end
111
+
112
+ def in_enforced_engine_file?
113
+ file_path = processed_source.path
114
+ return false unless file_path.include?(engines_path)
115
+ return false if in_disabled_engine?(file_path)
116
+
117
+ true
118
+ end
119
+
120
+ def in_disabled_engine?(file_path)
121
+ disabled_engines.any? do |e|
122
+ file_path.include?(File.join(engines_path, e))
123
+ end
124
+ end
125
+
126
+ def global_model_const?(const_node)
127
+ # Remove leading `::`, if any.
128
+ class_name = const_node.source.sub(/^:*/, '')
129
+ global_model?(class_name)
130
+ end
131
+
132
+ # class_name is e.g. "FooGlobalModelNamespace::BarModel"
133
+ def global_model?(class_name)
134
+ global_model_names.include?(class_name)
135
+ end
136
+
137
+ def child_of_const?(node)
138
+ node.parent.const_type?
139
+ end
140
+
141
+ def global_models_path
142
+ path = cop_config['GlobalModelsPath']
143
+ path += '/' unless path.end_with?('/')
144
+ path
145
+ end
146
+
147
+ def engines_path
148
+ cop_config['EnginesPath']
149
+ end
150
+
151
+ def disabled_engines
152
+ raw = cop_config['DisabledEngines'] || []
153
+ raw.map do |e|
154
+ ActiveSupport::Inflector.underscore(e)
155
+ end
156
+ end
157
+
158
+ def allowed_global_models
159
+ cop_config['AllowedGlobalModels'] || []
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -12,6 +12,12 @@ module RuboCop
12
12
  # Use RuboCop's standard `Exclude` file list parameter to exclude
13
13
  # existing global model files from counting as violations for this cop.
14
14
  #
15
+ # If you have a monorepo with multiple Rails services, you may wish to
16
+ # set GlobalModelsPath to something more specific than `app/models` so
17
+ # that it only matches the main monolith service and allows global
18
+ # models in other, smaller services. To do this, just set GlobalModelsPath
19
+ # to e.g. `flexport/app/models` in your `.rubocop.yml`.
20
+ #
15
21
  # @example AllowNamespacedGlobalModels: true (default)
16
22
  # # When `AllowNamespacedGlobalModels` is true, the cop only forbids
17
23
  # # additions at the top-level directory.
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'flexport/global_model_access_from_engine'
3
4
  require_relative 'flexport/new_global_model'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Flexport
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-flexport
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.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-11-19 00:00:00.000000000 Z
11
+ date: 2019-12-05 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rubocop
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -37,6 +51,7 @@ files:
37
51
  - bin/setup
38
52
  - config/default.yml
39
53
  - lib/rubocop-flexport.rb
54
+ - lib/rubocop/cop/flexport/global_model_access_from_engine.rb
40
55
  - lib/rubocop/cop/flexport/new_global_model.rb
41
56
  - lib/rubocop/cop/flexport_cops.rb
42
57
  - lib/rubocop/flexport.rb