package_protections 0.67.0 → 1.1.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/README.md +7 -7
- data/lib/package_protections/private/configuration.rb +2 -2
- data/lib/package_protections/private.rb +0 -2
- data/lib/package_protections/protected_package.rb +3 -3
- data/lib/package_protections/rubocop_protection_interface.rb +94 -20
- data/lib/package_protections/violation_behavior.rb +1 -1
- data/lib/package_protections.rb +2 -1
- data/lib/rubocop/cop/package_protections/namespaced_under_package_name.rb +76 -2
- data/lib/rubocop/cop/package_protections/typed_public_api.rb +77 -0
- metadata +7 -8
- data/lib/package_protections/private/multiple_namespaces_protection.rb +0 -128
- data/lib/package_protections/private/typed_api_protection.rb +0 -106
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 27281e8109b1edc0fff691f606a70260be38e130810e088d48e2d73d31424fbb
|
4
|
+
data.tar.gz: cd99fe90189671daa334be828aaad40b0928742098cfb1430164f5080d3060ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b1d22a6dee650ffbe8dd4027d5815b6c104f0825d9d257a40aee08dd44ad4c458f72b502e3ed28caa848f0aa6c468502edb4e4f6c5acd7943b808a8ca43096a3
|
7
|
+
data.tar.gz: 91ac08c0a0697dad9fbacd7e7d62ab2a22ef058ad074b2d0371b4faf67f9b264aa4b7ed9a597fb09280b44fead58b641aeda165001568ccb7db015dff03d0bc4
|
data/README.md
CHANGED
@@ -8,7 +8,7 @@ The intent of this gem is two fold:
|
|
8
8
|
This gem ships with the following checks
|
9
9
|
1) Your package is not introducing dependencies that are not intended (via `packwerk` `enforce_dependencies`)
|
10
10
|
2) Other packages are not using the private API of your package (via `packwerk` `enforce_privacy`)
|
11
|
-
3) Your package has a typed public API (via the `rubocop` `
|
11
|
+
3) Your package has a typed public API (via the `rubocop` `PackageProtections/TypedPublicApi` cop)
|
12
12
|
4) Your package only creates a single namespace (via the `rubocop` `PackageProtections/NamespacedUnderPackageName` cop)
|
13
13
|
4) Your package is only visible to a select number of packages (via the `packwerk` `enforce_privacy` cop)
|
14
14
|
|
@@ -141,21 +141,21 @@ end
|
|
141
141
|
In this example, `MyCustomProtection` needs to implement the `PackageProtections::ProtectionInterface` (for protections powered by `packwerk` that look at new and existing violations) OR `PackageProtections::RubocopProtectionInterface` (for protections powered by `rubocop` that look at the AST). It's recommended to take a look at the existing protections as examples. If you're having any trouble with this, please file an issue and we'll be glad to help.
|
142
142
|
|
143
143
|
## Incorporating into your CI Pipeline
|
144
|
-
Your CI pipeline can execute the public API
|
144
|
+
Your CI pipeline can execute the public API and fail if there are any offenses.
|
145
145
|
|
146
146
|
## Discussions, Issues, Questions, and More
|
147
147
|
To keep things organized, here are some recommended homes:
|
148
148
|
### Issues:
|
149
|
-
https://github.com/
|
149
|
+
https://github.com/rubyatscale/package_protections/issues
|
150
150
|
|
151
151
|
### Questions:
|
152
|
-
https://github.com/
|
152
|
+
https://github.com/rubyatscale/package_protections/discussions/categories/q-a
|
153
153
|
|
154
154
|
### General discussions:
|
155
|
-
https://github.com/
|
155
|
+
https://github.com/rubyatscale/package_protections/discussions/categories/general
|
156
156
|
|
157
157
|
### Ideas, new features, requests for change:
|
158
|
-
https://github.com/
|
158
|
+
https://github.com/rubyatscale/package_protections/discussions/categories/ideas
|
159
159
|
|
160
160
|
### Showcasing your work:
|
161
|
-
https://github.com/
|
161
|
+
https://github.com/rubyatscale/package_protections/discussions/categories/show-and-tell
|
@@ -28,8 +28,8 @@ module PackageProtections
|
|
28
28
|
[
|
29
29
|
Private::OutgoingDependencyProtection.new,
|
30
30
|
Private::IncomingPrivacyProtection.new,
|
31
|
-
|
32
|
-
|
31
|
+
RuboCop::Cop::PackageProtections::TypedPublicApi.new,
|
32
|
+
RuboCop::Cop::PackageProtections::NamespacedUnderPackageName.new,
|
33
33
|
Private::VisibilityProtection.new
|
34
34
|
]
|
35
35
|
end
|
@@ -3,11 +3,9 @@
|
|
3
3
|
|
4
4
|
require 'package_protections/private/colorized_string'
|
5
5
|
require 'package_protections/private/output'
|
6
|
-
require 'package_protections/private/typed_api_protection'
|
7
6
|
require 'package_protections/private/incoming_privacy_protection'
|
8
7
|
require 'package_protections/private/outgoing_dependency_protection'
|
9
8
|
require 'package_protections/private/metadata_modifiers'
|
10
|
-
require 'package_protections/private/multiple_namespaces_protection'
|
11
9
|
require 'package_protections/private/visibility_protection'
|
12
10
|
require 'package_protections/private/configuration'
|
13
11
|
|
@@ -17,14 +17,14 @@ module PackageProtections
|
|
17
17
|
invalid_identifiers = metadata.keys - valid_identifiers
|
18
18
|
|
19
19
|
if invalid_identifiers.any?
|
20
|
-
raise IncorrectPublicApiUsageError.new("Invalid configuration for package `#{original_package.name}`. The metadata keys #{invalid_identifiers.inspect} are not valid behaviors under the `protection` metadata namespace. Valid keys are #{valid_identifiers.inspect}. See https://github.com/
|
20
|
+
raise IncorrectPublicApiUsageError.new("Invalid configuration for package `#{original_package.name}`. The metadata keys #{invalid_identifiers.inspect} are not valid behaviors under the `protection` metadata namespace. Valid keys are #{valid_identifiers.inspect}. See https://github.com/rubyatscale/package_protections#readme for more info") # rubocop:disable Style/RaiseArgs
|
21
21
|
end
|
22
22
|
|
23
23
|
protections = {}
|
24
24
|
metadata.each_key do |protection_key|
|
25
25
|
protection = PackageProtections.with_identifier(protection_key)
|
26
26
|
if !protection
|
27
|
-
raise IncorrectPublicApiUsageError.new("Invalid configuration for package `#{original_package.name}`. The metadata key #{protection_key} is not a valid behaviors under the `protection` metadata namespace. Valid keys are #{valid_identifiers.inspect}. See https://github.com/
|
27
|
+
raise IncorrectPublicApiUsageError.new("Invalid configuration for package `#{original_package.name}`. The metadata key #{protection_key} is not a valid behaviors under the `protection` metadata namespace. Valid keys are #{valid_identifiers.inspect}. See https://github.com/rubyatscale/package_protections#readme for more info") # rubocop:disable Style/RaiseArgs
|
28
28
|
end
|
29
29
|
|
30
30
|
protections[protection.identifier] = get_violation_behavior(protection, metadata, original_package)
|
@@ -57,7 +57,7 @@ module PackageProtections
|
|
57
57
|
behavior = ViolationBehavior.from_raw_value(metadata[protection.identifier])
|
58
58
|
unmet_preconditions = protection.unmet_preconditions_for_behavior(behavior, package)
|
59
59
|
if !unmet_preconditions.nil?
|
60
|
-
raise IncorrectPublicApiUsageError.new("#{protection.identifier} protection does not have the valid preconditions. #{unmet_preconditions}. See https://github.com/
|
60
|
+
raise IncorrectPublicApiUsageError.new("#{protection.identifier} protection does not have the valid preconditions. #{unmet_preconditions}. See https://github.com/rubyatscale/package_protections#readme for more info") # rubocop:disable Style/RaiseArgs
|
61
61
|
end
|
62
62
|
|
63
63
|
behavior
|
@@ -3,26 +3,6 @@
|
|
3
3
|
# typed: strict
|
4
4
|
module PackageProtections
|
5
5
|
module RubocopProtectionInterface
|
6
|
-
include ProtectionInterface
|
7
|
-
extend T::Sig
|
8
|
-
extend T::Helpers
|
9
|
-
|
10
|
-
abstract!
|
11
|
-
|
12
|
-
sig do
|
13
|
-
abstract
|
14
|
-
.params(packages: T::Array[ProtectedPackage])
|
15
|
-
.returns(T::Array[CopConfig])
|
16
|
-
end
|
17
|
-
def cop_configs(packages); end
|
18
|
-
|
19
|
-
sig do
|
20
|
-
params(package: ProtectedPackage).returns(T::Hash[T.untyped, T.untyped])
|
21
|
-
end
|
22
|
-
def custom_cop_config(package)
|
23
|
-
{}
|
24
|
-
end
|
25
|
-
|
26
6
|
class CopConfig < T::Struct
|
27
7
|
extend T::Sig
|
28
8
|
const :name, String
|
@@ -46,6 +26,40 @@ module PackageProtections
|
|
46
26
|
end
|
47
27
|
end
|
48
28
|
|
29
|
+
include ProtectionInterface
|
30
|
+
extend T::Sig
|
31
|
+
extend T::Helpers
|
32
|
+
|
33
|
+
abstract!
|
34
|
+
|
35
|
+
###########################################################################
|
36
|
+
# Abstract Methods: These are methods that the client needs to implement
|
37
|
+
############################################################################
|
38
|
+
sig { abstract.returns(String) }
|
39
|
+
def cop_name; end
|
40
|
+
|
41
|
+
sig do
|
42
|
+
abstract.params(file: String).returns(String)
|
43
|
+
end
|
44
|
+
def message_for_fail_on_any(file); end
|
45
|
+
|
46
|
+
sig { abstract.returns(T::Array[String]) }
|
47
|
+
def included_globs_for_pack; end
|
48
|
+
|
49
|
+
###########################################################################
|
50
|
+
# Overriddable Methods: These are methods that the client can override,
|
51
|
+
# but a default is provided.
|
52
|
+
############################################################################
|
53
|
+
sig do
|
54
|
+
params(package: ProtectedPackage).returns(T::Hash[T.untyped, T.untyped])
|
55
|
+
end
|
56
|
+
def custom_cop_config(package)
|
57
|
+
{}
|
58
|
+
end
|
59
|
+
|
60
|
+
sig { override.params(behavior: ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
|
61
|
+
def unmet_preconditions_for_behavior(behavior, package); end
|
62
|
+
|
49
63
|
sig do
|
50
64
|
override.params(
|
51
65
|
new_violations: T::Array[PerFileViolation]
|
@@ -73,6 +87,66 @@ module PackageProtections
|
|
73
87
|
end
|
74
88
|
end
|
75
89
|
|
90
|
+
sig do
|
91
|
+
override.params(
|
92
|
+
protected_packages: T::Array[ProtectedPackage]
|
93
|
+
).returns(T::Array[Offense])
|
94
|
+
end
|
95
|
+
def get_offenses_for_existing_violations(protected_packages)
|
96
|
+
exclude_list = exclude_for_rule(cop_name)
|
97
|
+
offenses = []
|
98
|
+
|
99
|
+
protected_packages.each do |package|
|
100
|
+
violation_behavior = package.violation_behavior_for(identifier)
|
101
|
+
|
102
|
+
case violation_behavior
|
103
|
+
when ViolationBehavior::FailNever, ViolationBehavior::FailOnNew
|
104
|
+
next
|
105
|
+
when ViolationBehavior::FailOnAny
|
106
|
+
# Continue
|
107
|
+
else
|
108
|
+
T.absurd(violation_behavior)
|
109
|
+
end
|
110
|
+
|
111
|
+
package.original_package.directory.glob(included_globs_for_pack).each do |relative_path_to_file|
|
112
|
+
next unless exclude_list.include?(relative_path_to_file.to_s)
|
113
|
+
|
114
|
+
file = relative_path_to_file.to_s
|
115
|
+
offenses << Offense.new(
|
116
|
+
file: file,
|
117
|
+
message: message_for_fail_on_any(file),
|
118
|
+
violation_type: identifier,
|
119
|
+
package: package.original_package
|
120
|
+
)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
offenses
|
125
|
+
end
|
126
|
+
|
127
|
+
sig do
|
128
|
+
params(packages: T::Array[ProtectedPackage])
|
129
|
+
.returns(T::Array[CopConfig])
|
130
|
+
end
|
131
|
+
def cop_configs(packages)
|
132
|
+
include_paths = T.let([], T::Array[String])
|
133
|
+
packages.each do |p|
|
134
|
+
next unless p.violation_behavior_for(identifier).enabled?
|
135
|
+
|
136
|
+
included_globs_for_pack.each do |glob|
|
137
|
+
include_paths << p.original_package.directory.join(glob).to_s
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
[
|
142
|
+
CopConfig.new(
|
143
|
+
name: cop_name,
|
144
|
+
enabled: include_paths.any?,
|
145
|
+
include_paths: include_paths
|
146
|
+
)
|
147
|
+
]
|
148
|
+
end
|
149
|
+
|
76
150
|
private
|
77
151
|
|
78
152
|
sig { params(rule: String).returns(T::Set[String]) }
|
@@ -19,7 +19,7 @@ module PackageProtections
|
|
19
19
|
rescue KeyError
|
20
20
|
# Let's not encourage "unknown." That's mostly considered an internal value if nothing is specified.
|
21
21
|
acceptable_values = ViolationBehavior.values.map(&:serialize) - ['unknown']
|
22
|
-
raise IncorrectPublicApiUsageError.new("The metadata value #{value} is not a valid behavior. Double check your spelling! Acceptable values are #{acceptable_values}. See https://github.com/
|
22
|
+
raise IncorrectPublicApiUsageError.new("The metadata value #{value} is not a valid behavior. Double check your spelling! Acceptable values are #{acceptable_values}. See https://github.com/rubyatscale/package_protections#readme for more info") # rubocop:disable Style/RaiseArgs
|
23
23
|
end
|
24
24
|
|
25
25
|
sig { returns(T::Boolean) }
|
data/lib/package_protections.rb
CHANGED
@@ -10,7 +10,7 @@ require 'rubocop-sorbet'
|
|
10
10
|
|
11
11
|
#
|
12
12
|
# Welcome to PackageProtections!
|
13
|
-
# See https://github.com/
|
13
|
+
# See https://github.com/rubyatscale/package_protections#readme for more info
|
14
14
|
#
|
15
15
|
# This file is a reference for the available API to `package_protections`, but all implementation details are private
|
16
16
|
# (which is why we delegate to `Private` for the actual implementation).
|
@@ -37,6 +37,7 @@ module PackageProtections
|
|
37
37
|
|
38
38
|
# Implementation of rubocop-based protections
|
39
39
|
require 'rubocop/cop/package_protections/namespaced_under_package_name'
|
40
|
+
require 'rubocop/cop/package_protections/typed_public_api'
|
40
41
|
|
41
42
|
class << self
|
42
43
|
extend T::Sig
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: true
|
2
2
|
|
3
3
|
# For String#camelize
|
4
4
|
require 'active_support/core_ext/string/inflections'
|
@@ -7,7 +7,10 @@ module RuboCop
|
|
7
7
|
module Cop
|
8
8
|
module PackageProtections
|
9
9
|
class NamespacedUnderPackageName < Base
|
10
|
+
extend T::Sig
|
11
|
+
|
10
12
|
include RangeHelp
|
13
|
+
include ::PackageProtections::RubocopProtectionInterface
|
11
14
|
|
12
15
|
def on_new_investigation
|
13
16
|
absolute_filepath = Pathname.new(processed_source.file_path)
|
@@ -39,7 +42,7 @@ module RuboCop
|
|
39
42
|
#
|
40
43
|
# Therefore, for our implementation, we substitute out the non-namespace producing portions of the filename to count the number of namespaces.
|
41
44
|
# Note this will *not work* properly in applications that have different assumptions about autoloading.
|
42
|
-
package_last_name = package_name.split('/').last
|
45
|
+
package_last_name = T.must(package_name.split('/').last)
|
43
46
|
path_without_package_base = relative_filename.gsub(%r{#{package_name}/app/}, '')
|
44
47
|
if path_without_package_base.include?('concerns')
|
45
48
|
autoload_folder_name = path_without_package_base.split('/').first(2).join('/')
|
@@ -85,6 +88,77 @@ module RuboCop
|
|
85
88
|
end
|
86
89
|
end
|
87
90
|
|
91
|
+
IDENTIFIER = 'prevent_this_package_from_creating_other_namespaces'.freeze
|
92
|
+
|
93
|
+
sig { override.returns(String) }
|
94
|
+
def identifier
|
95
|
+
IDENTIFIER
|
96
|
+
end
|
97
|
+
|
98
|
+
sig { override.params(behavior: ::PackageProtections::ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
|
99
|
+
def unmet_preconditions_for_behavior(behavior, package)
|
100
|
+
if !behavior.enabled? && !package.metadata['global_namespaces'].nil?
|
101
|
+
"Invalid configuration for package `#{package.name}`. `#{identifier}` must be turned on to use `global_namespaces` configuration."
|
102
|
+
else
|
103
|
+
# We don't need to validate if the behavior is currentely fail_never
|
104
|
+
return if behavior.fail_never?
|
105
|
+
|
106
|
+
# The reason for this is precondition is the `MultipleNamespacesProtection` assumes this to work properly.
|
107
|
+
# To remove this precondition, we need to modify `MultipleNamespacesProtection` to be more generalized!
|
108
|
+
if ::PackageProtections::EXPECTED_PACK_DIRECTORIES.include?(Pathname.new(package.name).dirname.to_s) || package.name == ParsePackwerk::ROOT_PACKAGE_NAME
|
109
|
+
nil
|
110
|
+
else
|
111
|
+
"Package #{package.name} must be located in one of #{::PackageProtections::EXPECTED_PACK_DIRECTORIES.join(', ')} (or be the root) to use this protection"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
sig { override.returns(T::Array[String]) }
|
117
|
+
def included_globs_for_pack
|
118
|
+
[
|
119
|
+
'app/**/*',
|
120
|
+
'lib/**/*'
|
121
|
+
]
|
122
|
+
end
|
123
|
+
|
124
|
+
sig do
|
125
|
+
params(package: ::PackageProtections::ProtectedPackage).returns(T::Hash[T.untyped, T.untyped])
|
126
|
+
end
|
127
|
+
def custom_cop_config(package)
|
128
|
+
{
|
129
|
+
'GlobalNamespaces' => package.metadata['global_namespaces']
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
sig do
|
134
|
+
override.params(file: String).returns(String)
|
135
|
+
end
|
136
|
+
def message_for_fail_on_any(file)
|
137
|
+
"`#{file}` should be namespaced under the package namespace"
|
138
|
+
end
|
139
|
+
|
140
|
+
sig { override.returns(String) }
|
141
|
+
def cop_name
|
142
|
+
'PackageProtections/NamespacedUnderPackageName'
|
143
|
+
end
|
144
|
+
|
145
|
+
sig { override.returns(String) }
|
146
|
+
def humanized_protection_name
|
147
|
+
'Multiple Namespaces Violations'
|
148
|
+
end
|
149
|
+
|
150
|
+
sig { override.returns(String) }
|
151
|
+
def humanized_protection_description
|
152
|
+
<<~MESSAGE
|
153
|
+
These files cannot have ANY modules/classes that are not submodules of the package's allowed namespaces.
|
154
|
+
This is failing because these files are in `.rubocop_todo.yml` under `#{cop_name}`.
|
155
|
+
If you want to be able to ignore these files, you'll need to open the file's package's `package.yml` file and
|
156
|
+
change `#{IDENTIFIER}` to `#{::PackageProtections::ViolationBehavior::FailOnNew.serialize}`
|
157
|
+
|
158
|
+
See https://go/packwerk_cheatsheet_namespaces for more info.
|
159
|
+
MESSAGE
|
160
|
+
end
|
161
|
+
|
88
162
|
private
|
89
163
|
|
90
164
|
def get_allowed_namespaces(package_name)
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module PackageProtections
|
6
|
+
#
|
7
|
+
# This inherits from `Sorbet::StrictSigil` and doesn't change any behavior of it.
|
8
|
+
# The only reason we do this is so that configuration for this cop can live under a different cop namespace.
|
9
|
+
# This prevents this cop's configuration from clashing with other configurations for the same cop.
|
10
|
+
# A concrete example of this would be if a user is using this package protection to make sure public APIs are typed,
|
11
|
+
# and separately the application as a whole requiring strict typing in certain parts of the application.
|
12
|
+
#
|
13
|
+
# To prevent problems associated with needing to manage identical configurations for the same cop, we simply call it
|
14
|
+
# something else in the context of this protection.
|
15
|
+
#
|
16
|
+
# We can apply this same pattern if we want to use other cops in the context of package protections and prevent clashing.
|
17
|
+
#
|
18
|
+
class TypedPublicApi < Sorbet::StrictSigil
|
19
|
+
extend T::Sig
|
20
|
+
|
21
|
+
include ::PackageProtections::ProtectionInterface
|
22
|
+
include ::PackageProtections::RubocopProtectionInterface
|
23
|
+
|
24
|
+
IDENTIFIER = T.let('prevent_this_package_from_exposing_an_untyped_api'.freeze, String)
|
25
|
+
|
26
|
+
sig { override.returns(String) }
|
27
|
+
def identifier
|
28
|
+
IDENTIFIER
|
29
|
+
end
|
30
|
+
|
31
|
+
sig { override.params(behavior: ::PackageProtections::ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
|
32
|
+
def unmet_preconditions_for_behavior(behavior, package)
|
33
|
+
# We might decide that we should check that `package.enforces_privacy?` is true here too, since that signifies the app has decided they want
|
34
|
+
# a public api in `app/public`. For now, we say there are no preconditions, because the user can still make `app/public` even if they are not yet
|
35
|
+
# ready to enforce privacy, and they might want to enforce a typed API.
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
sig { override.returns(String) }
|
40
|
+
def cop_name
|
41
|
+
'PackageProtections/TypedPublicApi'
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { override.returns(T::Array[String]) }
|
45
|
+
def included_globs_for_pack
|
46
|
+
[
|
47
|
+
'app/public/**/*'
|
48
|
+
]
|
49
|
+
end
|
50
|
+
|
51
|
+
sig do
|
52
|
+
override.params(file: String).returns(String)
|
53
|
+
end
|
54
|
+
def message_for_fail_on_any(file)
|
55
|
+
"#{file} should be `typed: strict`"
|
56
|
+
end
|
57
|
+
|
58
|
+
sig { override.returns(String) }
|
59
|
+
def humanized_protection_name
|
60
|
+
'Typed API Violations'
|
61
|
+
end
|
62
|
+
|
63
|
+
sig { override.returns(String) }
|
64
|
+
def humanized_protection_description
|
65
|
+
<<~MESSAGE
|
66
|
+
These files cannot have ANY Ruby files in the public API that are not typed strict or higher.
|
67
|
+
This is failing because these files are in `.rubocop_todo.yml` under `#{cop_name}`.
|
68
|
+
If you want to be able to ignore these files, you'll need to open the file's package's `package.yml` file and
|
69
|
+
change `#{IDENTIFIER}` to `#{::PackageProtections::ViolationBehavior::FailOnNew.serialize}`
|
70
|
+
|
71
|
+
See https://go/packwerk_cheatsheet_typed_api for more info.
|
72
|
+
MESSAGE
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
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:
|
4
|
+
version: 1.1.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-06-
|
11
|
+
date: 2022-06-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -181,23 +181,22 @@ files:
|
|
181
181
|
- lib/package_protections/private/configuration.rb
|
182
182
|
- lib/package_protections/private/incoming_privacy_protection.rb
|
183
183
|
- lib/package_protections/private/metadata_modifiers.rb
|
184
|
-
- lib/package_protections/private/multiple_namespaces_protection.rb
|
185
184
|
- lib/package_protections/private/outgoing_dependency_protection.rb
|
186
185
|
- lib/package_protections/private/output.rb
|
187
|
-
- lib/package_protections/private/typed_api_protection.rb
|
188
186
|
- lib/package_protections/private/visibility_protection.rb
|
189
187
|
- lib/package_protections/protected_package.rb
|
190
188
|
- lib/package_protections/protection_interface.rb
|
191
189
|
- lib/package_protections/rubocop_protection_interface.rb
|
192
190
|
- lib/package_protections/violation_behavior.rb
|
193
191
|
- lib/rubocop/cop/package_protections/namespaced_under_package_name.rb
|
194
|
-
|
192
|
+
- lib/rubocop/cop/package_protections/typed_public_api.rb
|
193
|
+
homepage: https://github.com/rubyatscale/package_protections
|
195
194
|
licenses:
|
196
195
|
- MIT
|
197
196
|
metadata:
|
198
|
-
homepage_uri: https://github.com/
|
199
|
-
source_code_uri: https://github.com/
|
200
|
-
changelog_uri: https://github.com/
|
197
|
+
homepage_uri: https://github.com/rubyatscale/package_protections
|
198
|
+
source_code_uri: https://github.com/rubyatscale/parse_packwerk
|
199
|
+
changelog_uri: https://github.com/rubyatscale/parse_packwerk/releases
|
201
200
|
allowed_push_host: https://rubygems.org
|
202
201
|
post_install_message:
|
203
202
|
rdoc_options: []
|
@@ -1,128 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# typed: strict
|
4
|
-
|
5
|
-
module PackageProtections
|
6
|
-
module Private
|
7
|
-
class MultipleNamespacesProtection
|
8
|
-
extend T::Sig
|
9
|
-
|
10
|
-
include ProtectionInterface
|
11
|
-
include RubocopProtectionInterface
|
12
|
-
|
13
|
-
IDENTIFIER = 'prevent_this_package_from_creating_other_namespaces'
|
14
|
-
COP_NAME = 'PackageProtections/NamespacedUnderPackageName'
|
15
|
-
|
16
|
-
sig { override.returns(String) }
|
17
|
-
def identifier
|
18
|
-
IDENTIFIER
|
19
|
-
end
|
20
|
-
|
21
|
-
sig { override.params(behavior: ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
|
22
|
-
def unmet_preconditions_for_behavior(behavior, package)
|
23
|
-
if !behavior.enabled? && !package.metadata['global_namespaces'].nil?
|
24
|
-
"Invalid configuration for package `#{package.name}`. `#{identifier}` must be turned on to use `global_namespaces` configuration."
|
25
|
-
else
|
26
|
-
# We don't need to validate if the behavior is currentely fail_never
|
27
|
-
return if behavior.fail_never?
|
28
|
-
|
29
|
-
# The reason for this is precondition is the `MultipleNamespacesProtection` assumes this to work properly.
|
30
|
-
# To remove this precondition, we need to modify `MultipleNamespacesProtection` to be more generalized!
|
31
|
-
if EXPECTED_PACK_DIRECTORIES.include?(Pathname.new(package.name).dirname.to_s) || package.name == ParsePackwerk::ROOT_PACKAGE_NAME
|
32
|
-
nil
|
33
|
-
else
|
34
|
-
"Package #{package.name} must be located in one of #{EXPECTED_PACK_DIRECTORIES.join(', ')} (or be the root) to use this protection"
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
sig do
|
40
|
-
override
|
41
|
-
.params(packages: T::Array[ProtectedPackage])
|
42
|
-
.returns(T::Array[CopConfig])
|
43
|
-
end
|
44
|
-
def cop_configs(packages)
|
45
|
-
include_paths = T.let([], T::Array[String])
|
46
|
-
packages.each do |p|
|
47
|
-
next if p.name == ParsePackwerk::ROOT_PACKAGE_NAME
|
48
|
-
|
49
|
-
if p.violation_behavior_for(identifier).enabled?
|
50
|
-
include_paths << p.original_package.directory.join('app', '**', '*').to_s
|
51
|
-
include_paths << p.original_package.directory.join('lib', '**', '*').to_s
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
[
|
56
|
-
CopConfig.new(
|
57
|
-
name: COP_NAME,
|
58
|
-
enabled: include_paths.any?,
|
59
|
-
include_paths: include_paths
|
60
|
-
)
|
61
|
-
]
|
62
|
-
end
|
63
|
-
|
64
|
-
sig do
|
65
|
-
params(package: ProtectedPackage).returns(T::Hash[T.untyped, T.untyped])
|
66
|
-
end
|
67
|
-
def custom_cop_config(package)
|
68
|
-
{
|
69
|
-
'GlobalNamespaces' => package.metadata['global_namespaces']
|
70
|
-
}
|
71
|
-
end
|
72
|
-
|
73
|
-
sig do
|
74
|
-
override.params(
|
75
|
-
protected_packages: T::Array[ProtectedPackage]
|
76
|
-
).returns(T::Array[Offense])
|
77
|
-
end
|
78
|
-
def get_offenses_for_existing_violations(protected_packages)
|
79
|
-
exclude_list = exclude_for_rule(COP_NAME)
|
80
|
-
offenses = []
|
81
|
-
|
82
|
-
protected_packages.each do |package|
|
83
|
-
violation_behavior = package.violation_behavior_for(identifier)
|
84
|
-
|
85
|
-
case violation_behavior
|
86
|
-
when ViolationBehavior::FailNever, ViolationBehavior::FailOnNew
|
87
|
-
next
|
88
|
-
when ViolationBehavior::FailOnAny
|
89
|
-
# Continue
|
90
|
-
else
|
91
|
-
T.absurd(violation_behavior)
|
92
|
-
end
|
93
|
-
|
94
|
-
package.original_package.directory.glob('**/**/*.*').each do |relative_path_to_file|
|
95
|
-
next unless exclude_list.include?(relative_path_to_file.to_s)
|
96
|
-
|
97
|
-
file = relative_path_to_file.to_s
|
98
|
-
offenses << Offense.new(
|
99
|
-
file: file,
|
100
|
-
message: "`#{file}` should be namespaced under the package namespace",
|
101
|
-
violation_type: identifier,
|
102
|
-
package: package.original_package
|
103
|
-
)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
offenses
|
108
|
-
end
|
109
|
-
|
110
|
-
sig { override.returns(String) }
|
111
|
-
def humanized_protection_name
|
112
|
-
'Multiple Namespaces Violations'
|
113
|
-
end
|
114
|
-
|
115
|
-
sig { override.returns(String) }
|
116
|
-
def humanized_protection_description
|
117
|
-
<<~MESSAGE
|
118
|
-
These files cannot have ANY modules/classes that are not submodules of the package's allowed namespaces.
|
119
|
-
This is failing because these files are in `.rubocop_todo.yml` under `#{COP_NAME}`.
|
120
|
-
If you want to be able to ignore these files, you'll need to open the file's package's `package.yml` file and
|
121
|
-
change `#{IDENTIFIER}` to `#{ViolationBehavior::FailOnNew.serialize}`
|
122
|
-
|
123
|
-
See https://go/packwerk_cheatsheet_namespaces for more info.
|
124
|
-
MESSAGE
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
@@ -1,106 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
module PackageProtections
|
5
|
-
module Private
|
6
|
-
class TypedApiProtection
|
7
|
-
extend T::Sig
|
8
|
-
|
9
|
-
include ProtectionInterface
|
10
|
-
include RubocopProtectionInterface
|
11
|
-
|
12
|
-
IDENTIFIER = 'prevent_this_package_from_exposing_an_untyped_api'
|
13
|
-
COP_NAME = 'Sorbet/StrictSigil'
|
14
|
-
|
15
|
-
sig { override.returns(String) }
|
16
|
-
def identifier
|
17
|
-
IDENTIFIER
|
18
|
-
end
|
19
|
-
|
20
|
-
sig { override.params(behavior: ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
|
21
|
-
def unmet_preconditions_for_behavior(behavior, package)
|
22
|
-
# We might decide that we should check that `package.enforces_privacy?` is true here too, since that signifies the app has decided they want
|
23
|
-
# a public api in `app/public`. For now, we say there are no preconditions, because the user can still make `app/public` even if they are not yet
|
24
|
-
# ready to enforce privacy, and they might want to enforce a typed API.
|
25
|
-
nil
|
26
|
-
end
|
27
|
-
|
28
|
-
sig do
|
29
|
-
override
|
30
|
-
.params(packages: T::Array[ProtectedPackage])
|
31
|
-
.returns(T::Array[CopConfig])
|
32
|
-
end
|
33
|
-
def cop_configs(packages)
|
34
|
-
include_paths = T.let([], T::Array[String])
|
35
|
-
packages.each do |p|
|
36
|
-
if p.violation_behavior_for(identifier).enabled?
|
37
|
-
directory = p.original_package.directory
|
38
|
-
include_paths << directory.join('app', 'public', '**', '*').to_s
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
[
|
43
|
-
CopConfig.new(
|
44
|
-
name: COP_NAME,
|
45
|
-
enabled: include_paths.any?,
|
46
|
-
include_paths: include_paths
|
47
|
-
)
|
48
|
-
]
|
49
|
-
end
|
50
|
-
|
51
|
-
sig do
|
52
|
-
override.params(
|
53
|
-
protected_packages: T::Array[ProtectedPackage]
|
54
|
-
).returns(T::Array[Offense])
|
55
|
-
end
|
56
|
-
def get_offenses_for_existing_violations(protected_packages)
|
57
|
-
exclude_list = exclude_for_rule(COP_NAME)
|
58
|
-
|
59
|
-
offenses = T.let([], T::Array[Offense])
|
60
|
-
protected_packages.flat_map do |protected_package|
|
61
|
-
violation_behavior = protected_package.violation_behavior_for(identifier)
|
62
|
-
|
63
|
-
case violation_behavior
|
64
|
-
when ViolationBehavior::FailNever, ViolationBehavior::FailOnNew
|
65
|
-
next
|
66
|
-
when ViolationBehavior::FailOnAny
|
67
|
-
# Continue
|
68
|
-
else
|
69
|
-
T.absurd(violation_behavior)
|
70
|
-
end
|
71
|
-
|
72
|
-
protected_package.original_package.directory.glob('app/public/**/**/*.*').each do |relative_path_to_file|
|
73
|
-
next unless exclude_list.include?(relative_path_to_file.to_s)
|
74
|
-
|
75
|
-
file = relative_path_to_file.to_s
|
76
|
-
offenses << Offense.new(
|
77
|
-
file: file,
|
78
|
-
message: "#{file} should be `typed: strict`",
|
79
|
-
violation_type: identifier,
|
80
|
-
package: protected_package.original_package
|
81
|
-
)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
offenses
|
86
|
-
end
|
87
|
-
|
88
|
-
sig { override.returns(String) }
|
89
|
-
def humanized_protection_name
|
90
|
-
'Typed API Violations'
|
91
|
-
end
|
92
|
-
|
93
|
-
sig { override.returns(String) }
|
94
|
-
def humanized_protection_description
|
95
|
-
<<~MESSAGE
|
96
|
-
These files cannot have ANY Ruby files in the public API that are not typed strict or higher.
|
97
|
-
This is failing because these files are in `.rubocop_todo.yml` under `#{COP_NAME}`.
|
98
|
-
If you want to be able to ignore these files, you'll need to open the file's package's `package.yml` file and
|
99
|
-
change `#{IDENTIFIER}` to `#{ViolationBehavior::FailOnNew.serialize}`
|
100
|
-
|
101
|
-
See https://go/packwerk_cheatsheet_typed_api for more info.
|
102
|
-
MESSAGE
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|