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 +4 -4
- data/README.md +1 -1
- data/config/default.yml +12 -0
- data/lib/rubocop/cop/flexport/global_model_access_from_engine.rb +164 -0
- data/lib/rubocop/cop/flexport/new_global_model.rb +6 -0
- data/lib/rubocop/cop/flexport_cops.rb +1 -0
- data/lib/rubocop/flexport/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 21c9588d65a1cc1b11a85277a94b02caf3dd37bb067b8a2ae09b53da39052bb0
|
4
|
+
data.tar.gz: 2ca05dbe05457065dcfd40e703e6a9c29340fd44a20862df21364ebc1c31e76f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 375ce90040f7a4185de7bc785867b46574dbc04a24ac0f17d8d3213075b9f689c60aec9e127830aea48005f76ca3f8a8023948a792284f45460fc80ce704ff87
|
7
|
+
data.tar.gz: 560c5d8655494e752fae1db186c5c7ee1d514fa938fb977c07d09a1c87b9edcffc17d9bec31aa6b42a25436df480f63e70caee00bc3b52f3cdd786cb18962aaf
|
data/README.md
CHANGED
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.
|
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.
|
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
|
+
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
|