package_protections 2.2.1 → 2.3.1
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/lib/package_protections/per_file_violation.rb +1 -0
- data/lib/package_protections/private/metadata_modifiers.rb +1 -0
- data/lib/package_protections/private/output.rb +1 -0
- data/lib/package_protections/private.rb +1 -1
- data/lib/package_protections/protected_package.rb +1 -0
- data/lib/package_protections/protection_interface.rb +1 -0
- data/lib/package_protections/rspec/application_fixture_helper.rb +0 -5
- data/lib/package_protections/rubocop_protection_interface.rb +6 -0
- data/lib/package_protections/violation_behavior.rb +1 -0
- data/lib/package_protections.rb +3 -1
- data/lib/rubocop/cop/package_protections/namespaced_under_package_name.rb +18 -106
- data/lib/rubocop/cop/package_protections/typed_public_api.rb +1 -1
- metadata +17 -4
- data/lib/rubocop/cop/package_protections/namespaced_under_package_name/desired_zeitwerk_api.rb +0 -110
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 918d945d5ceb1f2df5f544a50c74ebc8977d36819c4d5204866e90e8518115fc
|
4
|
+
data.tar.gz: e80077db49323a725e67d41c861e78da371f7e5de986c9240e9de1a4308f6686
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81569a675bd20e6993b018d71f29cffb45d90621b9e6f036a746d89c8674b1d43e9a6d7bed6df7e4689ce3e008dc0a89b8506da9f959daa0611cf43d9adb2dc2
|
7
|
+
data.tar.gz: 3c290042abe91b8bbd285dae9091db68729fb99dd8f0b861c99cbac131f9705caf581d6c117e582e34b73ec38ae3b9f5ac70b885cab41974cd89f6295f03662c
|
@@ -128,7 +128,7 @@ module PackageProtections
|
|
128
128
|
@private_cop_config ||= begin
|
129
129
|
protected_packages = all_protected_packages
|
130
130
|
protection = T.cast(PackageProtections.with_identifier(identifier), PackageProtections::RubocopProtectionInterface)
|
131
|
-
protected_packages.
|
131
|
+
protected_packages.to_h { |p| [p.name, protection.custom_cop_config(p)] }
|
132
132
|
end
|
133
133
|
end
|
134
134
|
|
@@ -17,7 +17,6 @@ module ApplicationFixtureHelper
|
|
17
17
|
enforce_dependencies: true,
|
18
18
|
enforce_privacy: true,
|
19
19
|
protections: {},
|
20
|
-
global_namespaces: [],
|
21
20
|
visible_to: []
|
22
21
|
)
|
23
22
|
defaults = {
|
@@ -33,10 +32,6 @@ module ApplicationFixtureHelper
|
|
33
32
|
metadata.merge!('visible_to' => visible_to)
|
34
33
|
end
|
35
34
|
|
36
|
-
if global_namespaces.any?
|
37
|
-
metadata.merge!('global_namespaces' => global_namespaces)
|
38
|
-
end
|
39
|
-
|
40
35
|
package = ParsePackwerk::Package.new(
|
41
36
|
name: pack_name,
|
42
37
|
dependencies: dependencies,
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# typed: strict
|
4
|
+
|
4
5
|
module PackageProtections
|
5
6
|
module RubocopProtectionInterface
|
6
7
|
class CopConfig < T::Struct
|
@@ -9,6 +10,7 @@ module PackageProtections
|
|
9
10
|
const :enabled, T::Boolean, default: true
|
10
11
|
const :include_paths, T::Array[String], default: []
|
11
12
|
const :exclude_paths, T::Array[String], default: []
|
13
|
+
const :metadata, T.untyped, default: {}
|
12
14
|
|
13
15
|
sig { returns(String) }
|
14
16
|
def to_rubocop_yml_compatible_format
|
@@ -22,6 +24,10 @@ module PackageProtections
|
|
22
24
|
cop_config['Exclude'] = exclude_paths
|
23
25
|
end
|
24
26
|
|
27
|
+
if metadata.any?
|
28
|
+
cop_config.merge!(metadata)
|
29
|
+
end
|
30
|
+
|
25
31
|
{ name => cop_config }.to_yaml.gsub("---\n", '')
|
26
32
|
end
|
27
33
|
end
|
data/lib/package_protections.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# typed: strict
|
4
|
+
|
4
5
|
require 'sorbet-runtime'
|
5
6
|
require 'open3'
|
6
7
|
require 'set'
|
7
8
|
require 'parse_packwerk'
|
8
9
|
require 'rubocop'
|
9
10
|
require 'rubocop-sorbet'
|
11
|
+
require 'rubocop-modularization'
|
10
12
|
|
11
13
|
#
|
12
14
|
# Welcome to PackageProtections!
|
@@ -66,7 +68,7 @@ module PackageProtections
|
|
66
68
|
sig { params(identifier: Identifier).returns(ProtectionInterface) }
|
67
69
|
def self.with_identifier(identifier)
|
68
70
|
@map ||= T.let(@map, T.nilable(T::Hash[Identifier, ProtectionInterface]))
|
69
|
-
@map ||= all.
|
71
|
+
@map ||= all.to_h { |protection| [protection.identifier, protection] }
|
70
72
|
@map.fetch(identifier)
|
71
73
|
end
|
72
74
|
|
@@ -2,81 +2,14 @@
|
|
2
2
|
|
3
3
|
# For String#camelize
|
4
4
|
require 'active_support/core_ext/string/inflections'
|
5
|
-
require 'rubocop/cop/package_protections/namespaced_under_package_name/desired_zeitwerk_api'
|
6
5
|
|
7
6
|
module RuboCop
|
8
7
|
module Cop
|
9
8
|
module PackageProtections
|
10
|
-
|
11
|
-
# TODO:
|
12
|
-
# This class is in serious need of being split up into helpful abstractions.
|
13
|
-
# A really helpful abstraction would be one that takes a file path and can spit out information about
|
14
|
-
# namespacing, such as the exposed namespace, the file path for a different namespace, and more.
|
15
|
-
#
|
16
|
-
class NamespacedUnderPackageName < Base
|
9
|
+
class NamespacedUnderPackageName < Modularization::NamespacedUnderPackageName
|
17
10
|
extend T::Sig
|
18
|
-
|
19
|
-
include RangeHelp
|
20
11
|
include ::PackageProtections::RubocopProtectionInterface
|
21
12
|
|
22
|
-
sig { void }
|
23
|
-
def on_new_investigation
|
24
|
-
absolute_filepath = Pathname.new(processed_source.file_path)
|
25
|
-
relative_filepath = absolute_filepath.relative_path_from(Pathname.pwd)
|
26
|
-
relative_filename = relative_filepath.to_s
|
27
|
-
|
28
|
-
# This cop only works for files ruby files in `app`
|
29
|
-
return if !relative_filename.include?('app/') || relative_filepath.extname != '.rb'
|
30
|
-
|
31
|
-
relative_filename = relative_filepath.to_s
|
32
|
-
package_for_path = ParsePackwerk.package_from_path(relative_filename)
|
33
|
-
return if package_for_path.nil?
|
34
|
-
|
35
|
-
namespace_context = self.class.desired_zeitwerk_api.for_file(relative_filename, package_for_path)
|
36
|
-
return if namespace_context.nil?
|
37
|
-
|
38
|
-
allowed_global_namespaces = Set.new([
|
39
|
-
namespace_context.expected_namespace,
|
40
|
-
*::PackageProtections.config.globally_permitted_namespaces
|
41
|
-
])
|
42
|
-
|
43
|
-
package_name = package_for_path.name
|
44
|
-
actual_namespace = namespace_context.current_namespace
|
45
|
-
|
46
|
-
if allowed_global_namespaces.include?(actual_namespace)
|
47
|
-
# No problem!
|
48
|
-
else
|
49
|
-
package_enforces_namespaces = !::PackageProtections::ProtectedPackage.from(package_for_path).violation_behavior_for(NamespacedUnderPackageName::IDENTIFIER).fail_never?
|
50
|
-
expected_namespace = namespace_context.expected_namespace
|
51
|
-
relative_desired_path = namespace_context.expected_filepath
|
52
|
-
pack_owning_this_namespace = self.class.namespaces_to_packs[actual_namespace]
|
53
|
-
|
54
|
-
if package_enforces_namespaces
|
55
|
-
add_offense(
|
56
|
-
source_range(processed_source.buffer, 1, 0),
|
57
|
-
message: format(
|
58
|
-
'`%<package_name>s` prevents modules/classes that are not submodules of the package namespace. Should be namespaced under `%<expected_namespace>s` with path `%<expected_path>s`. See https://go/packwerk_cheatsheet_namespaces for more info.',
|
59
|
-
package_name: package_name,
|
60
|
-
expected_namespace: expected_namespace,
|
61
|
-
expected_path: relative_desired_path
|
62
|
-
)
|
63
|
-
)
|
64
|
-
elsif pack_owning_this_namespace
|
65
|
-
add_offense(
|
66
|
-
source_range(processed_source.buffer, 1, 0),
|
67
|
-
message: format(
|
68
|
-
'`%<pack_owning_this_namespace>s` prevents other packs from sitting in the `%<actual_namespace>s` namespace. This should be namespaced under `%<expected_namespace>s` with path `%<expected_path>s`. See https://go/packwerk_cheatsheet_namespaces for more info.',
|
69
|
-
package_name: package_name,
|
70
|
-
pack_owning_this_namespace: pack_owning_this_namespace,
|
71
|
-
expected_namespace: expected_namespace,
|
72
|
-
actual_namespace: actual_namespace,
|
73
|
-
expected_path: relative_desired_path
|
74
|
-
)
|
75
|
-
)
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
13
|
# We override `cop_configs` for this protection.
|
81
14
|
# The default behavior disables cops when a package has turned off a protection.
|
82
15
|
# However: namespace violations can occur even when one package has TURNED OFF their namespace protection
|
@@ -87,22 +20,34 @@ module RuboCop
|
|
87
20
|
.returns(T::Array[::PackageProtections::RubocopProtectionInterface::CopConfig])
|
88
21
|
end
|
89
22
|
def cop_configs(packages)
|
90
|
-
|
23
|
+
include_packs = T.let([], T::Array[String])
|
91
24
|
packages.each do |p|
|
92
|
-
|
93
|
-
|
25
|
+
enabled_for_pack = !p.violation_behavior_for(NamespacedUnderPackageName::IDENTIFIER).fail_never?
|
26
|
+
if enabled_for_pack
|
27
|
+
include_packs << p.name
|
94
28
|
end
|
95
29
|
end
|
96
30
|
|
97
31
|
[
|
98
32
|
::PackageProtections::RubocopProtectionInterface::CopConfig.new(
|
99
33
|
name: cop_name,
|
100
|
-
enabled:
|
101
|
-
|
34
|
+
enabled: include_packs.any?,
|
35
|
+
metadata: {
|
36
|
+
'IncludePacks' => include_packs,
|
37
|
+
'GloballyPermittedNamespaces' => ::PackageProtections.config.globally_permitted_namespaces
|
38
|
+
}
|
102
39
|
)
|
103
40
|
]
|
104
41
|
end
|
105
42
|
|
43
|
+
sig { override.returns(T::Array[String]) }
|
44
|
+
def included_globs_for_pack
|
45
|
+
[
|
46
|
+
'app/**/*',
|
47
|
+
'lib/**/*'
|
48
|
+
]
|
49
|
+
end
|
50
|
+
|
106
51
|
IDENTIFIER = T.let('prevent_this_package_from_creating_other_namespaces'.freeze, String)
|
107
52
|
|
108
53
|
sig { override.returns(String) }
|
@@ -132,14 +77,6 @@ module RuboCop
|
|
132
77
|
end
|
133
78
|
end
|
134
79
|
|
135
|
-
sig { override.returns(T::Array[String]) }
|
136
|
-
def included_globs_for_pack
|
137
|
-
[
|
138
|
-
'app/**/*',
|
139
|
-
'lib/**/*'
|
140
|
-
]
|
141
|
-
end
|
142
|
-
|
143
80
|
sig do
|
144
81
|
override.params(file: String).returns(String)
|
145
82
|
end
|
@@ -168,31 +105,6 @@ module RuboCop
|
|
168
105
|
See https://go/packwerk_cheatsheet_namespaces for more info.
|
169
106
|
MESSAGE
|
170
107
|
end
|
171
|
-
|
172
|
-
sig { returns(DesiredZeitwerkApi) }
|
173
|
-
def self.desired_zeitwerk_api
|
174
|
-
# This is cached at the class level so we will cache more expensive operations
|
175
|
-
# across rubocop requests.
|
176
|
-
@desired_zeitwerk_api ||= T.let(nil, T.nilable(DesiredZeitwerkApi))
|
177
|
-
@desired_zeitwerk_api ||= DesiredZeitwerkApi.new
|
178
|
-
end
|
179
|
-
|
180
|
-
sig { returns(T::Hash[String, String]) }
|
181
|
-
def self.namespaces_to_packs
|
182
|
-
@namespaces_to_packs = T.let(nil, T.nilable(T::Hash[String, String]))
|
183
|
-
@namespaces_to_packs ||= begin
|
184
|
-
all_packs_enforcing_namespaces = ParsePackwerk.all.reject do |p|
|
185
|
-
::PackageProtections::ProtectedPackage.from(p).violation_behavior_for(NamespacedUnderPackageName::IDENTIFIER).fail_never?
|
186
|
-
end
|
187
|
-
|
188
|
-
namespaces_to_packs = {}
|
189
|
-
all_packs_enforcing_namespaces.each do |package|
|
190
|
-
namespaces_to_packs[desired_zeitwerk_api.get_pack_based_namespace(package)] = package.name
|
191
|
-
end
|
192
|
-
|
193
|
-
namespaces_to_packs
|
194
|
-
end
|
195
|
-
end
|
196
108
|
end
|
197
109
|
end
|
198
110
|
end
|
@@ -15,7 +15,7 @@ module RuboCop
|
|
15
15
|
#
|
16
16
|
# We can apply this same pattern if we want to use other cops in the context of package protections and prevent clashing.
|
17
17
|
#
|
18
|
-
class TypedPublicApi <
|
18
|
+
class TypedPublicApi < Modularization::TypedPublicApi
|
19
19
|
extend T::Sig
|
20
20
|
|
21
21
|
include ::PackageProtections::ProtectionInterface
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: package_protections
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gusto Engineers
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop-modularization
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: rubocop-sorbet
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -192,7 +206,6 @@ files:
|
|
192
206
|
- lib/package_protections/rubocop_protection_interface.rb
|
193
207
|
- lib/package_protections/violation_behavior.rb
|
194
208
|
- lib/rubocop/cop/package_protections/namespaced_under_package_name.rb
|
195
|
-
- lib/rubocop/cop/package_protections/namespaced_under_package_name/desired_zeitwerk_api.rb
|
196
209
|
- lib/rubocop/cop/package_protections/typed_public_api.rb
|
197
210
|
homepage: https://github.com/rubyatscale/package_protections
|
198
211
|
licenses:
|
@@ -210,7 +223,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
210
223
|
requirements:
|
211
224
|
- - ">="
|
212
225
|
- !ruby/object:Gem::Version
|
213
|
-
version: 2.
|
226
|
+
version: 2.6.0
|
214
227
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
215
228
|
requirements:
|
216
229
|
- - ">="
|
data/lib/rubocop/cop/package_protections/namespaced_under_package_name/desired_zeitwerk_api.rb
DELETED
@@ -1,110 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
|
3
|
-
module RuboCop
|
4
|
-
module Cop
|
5
|
-
module PackageProtections
|
6
|
-
class NamespacedUnderPackageName < Base
|
7
|
-
#
|
8
|
-
# This is a private class that represents API that we would prefer to be available somehow in Zeitwerk.
|
9
|
-
#
|
10
|
-
class DesiredZeitwerkApi
|
11
|
-
extend T::Sig
|
12
|
-
|
13
|
-
class NamespaceContext < T::Struct
|
14
|
-
const :current_namespace, String
|
15
|
-
const :expected_namespace, String
|
16
|
-
const :expected_filepath, String
|
17
|
-
end
|
18
|
-
|
19
|
-
#
|
20
|
-
# For now, this API includes `package_for_path`
|
21
|
-
# If this were truly zeitwerk API, it wouldn't include any mention of packs and it would likely not need the package at all
|
22
|
-
# Since it could get the actual namespace without knowing anything about packs.
|
23
|
-
# However, we would need to pass to it the desired namespace based on the pack name for it to be able to suggest
|
24
|
-
# a desired filepath.
|
25
|
-
# Likely this means that our own cop should determine the desired namespace and pass that in
|
26
|
-
# and this can determine actual namespace and how to get to expected.
|
27
|
-
#
|
28
|
-
sig { params(relative_filename: String, package_for_path: ParsePackwerk::Package).returns(T.nilable(NamespaceContext)) }
|
29
|
-
def for_file(relative_filename, package_for_path)
|
30
|
-
package_name = package_for_path.name
|
31
|
-
|
32
|
-
# Zeitwerk establishes a standard convention by which namespaces are defined.
|
33
|
-
# The package protections namespace checker is coupled to a specific assumption about how auto-loading works.
|
34
|
-
#
|
35
|
-
# Namely, we expect the following autoload paths: `packs/**/app/**/`
|
36
|
-
# Examples:
|
37
|
-
# 1) `packs/package_1/app/public/package_1/my_constant.rb` produces constant `Package1::MyConstant`
|
38
|
-
# 2) `packs/package_1/app/services/package_1/my_service.rb` produces constant `Package1::MyService`
|
39
|
-
# 3) `packs/package_1/app/services/package_1.rb` produces constant `Package1`
|
40
|
-
# 4) `packs/package_1/app/public/package_1.rb` produces constant `Package1`
|
41
|
-
#
|
42
|
-
# Described another way, we expect any part of the directory labeled NAMESPACE to establish a portion of the fully qualified runtime constant:
|
43
|
-
# `packs/**/app/**/NAMESPACE1/NAMESPACE2/[etc]`
|
44
|
-
#
|
45
|
-
# Therefore, for our implementation, we substitute out the non-namespace producing portions of the filename to count the number of namespaces.
|
46
|
-
# Note this will *not work* properly in applications that have different assumptions about autoloading.
|
47
|
-
|
48
|
-
path_without_package_base = relative_filename.gsub(%r{#{package_name}/app/}, '')
|
49
|
-
if path_without_package_base.include?('concerns')
|
50
|
-
autoload_folder_name = path_without_package_base.split('/').first(2).join('/')
|
51
|
-
else
|
52
|
-
autoload_folder_name = path_without_package_base.split('/').first
|
53
|
-
end
|
54
|
-
|
55
|
-
remaining_file_path = path_without_package_base.gsub(%r{\A#{autoload_folder_name}/}, '')
|
56
|
-
actual_namespace = get_actual_namespace(remaining_file_path, package_name)
|
57
|
-
|
58
|
-
if relative_filename.include?('app/')
|
59
|
-
app_or_lib = 'app'
|
60
|
-
elsif relative_filename.include?('lib/')
|
61
|
-
app_or_lib = 'lib'
|
62
|
-
end
|
63
|
-
|
64
|
-
absolute_desired_path = root_pathname.join(
|
65
|
-
package_name,
|
66
|
-
T.must(app_or_lib),
|
67
|
-
T.must(autoload_folder_name),
|
68
|
-
get_package_last_name(package_for_path),
|
69
|
-
remaining_file_path
|
70
|
-
)
|
71
|
-
|
72
|
-
relative_desired_path = absolute_desired_path.relative_path_from(root_pathname)
|
73
|
-
|
74
|
-
NamespaceContext.new(
|
75
|
-
current_namespace: actual_namespace,
|
76
|
-
expected_namespace: get_pack_based_namespace(package_for_path),
|
77
|
-
expected_filepath: relative_desired_path.to_s
|
78
|
-
)
|
79
|
-
end
|
80
|
-
|
81
|
-
sig { params(pack: ParsePackwerk::Package).returns(String) }
|
82
|
-
def get_pack_based_namespace(pack)
|
83
|
-
get_package_last_name(pack).camelize
|
84
|
-
end
|
85
|
-
|
86
|
-
private
|
87
|
-
|
88
|
-
sig { returns(Pathname) }
|
89
|
-
def root_pathname
|
90
|
-
Pathname.pwd
|
91
|
-
end
|
92
|
-
|
93
|
-
sig { params(pack: ParsePackwerk::Package).returns(String) }
|
94
|
-
def get_package_last_name(pack)
|
95
|
-
T.must(pack.name.split('/').last)
|
96
|
-
end
|
97
|
-
|
98
|
-
sig { params(remaining_file_path: String, package_name: String).returns(String) }
|
99
|
-
def get_actual_namespace(remaining_file_path, package_name)
|
100
|
-
# If the remaining file path is a ruby file (not a directory), then it establishes a global namespace
|
101
|
-
# Otherwise, per Zeitwerk's conventions as listed above, its a directory that establishes another global namespace
|
102
|
-
T.must(remaining_file_path.split('/').first).gsub('.rb', '').camelize
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
private_constant :DesiredZeitwerkApi
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|