package_protections 1.0.0 → 1.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/lib/package_protections/private/configuration.rb +2 -2
- data/lib/package_protections/private.rb +0 -2
- data/lib/package_protections/rspec/application_fixture_helper.rb +54 -0
- data/lib/package_protections/rspec/matchers.rb +99 -0
- data/lib/package_protections/rspec/support.rb +17 -0
- data/lib/package_protections/rubocop_protection_interface.rb +94 -20
- data/lib/rubocop/cop/package_protections/namespaced_under_package_name.rb +76 -2
- data/lib/rubocop/cop/package_protections/typed_public_api.rb +56 -1
- metadata +5 -4
- 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: 41803352508cd5d4ede0fe0caf1121516474fe3e95a030e4db7e0f05badfa9a2
|
4
|
+
data.tar.gz: bc594511c3b3da35dcd9efcfba0f989928e074e0da429a8f0f538e0537eff5be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 352aa3ea22bf0d7cf765023c35ed88c6026fda2b52036aa73fbcf35e4aca1df52df24486ce028a238dcf50db89f2864f4080e77d6da52d9f7be7fa984cd70dc7
|
7
|
+
data.tar.gz: 6bb8ab188d27a3656cd6979d00fcaf725165176b17a42704be114ef0bbfbe7150facf21ae44ebc3c1633ce5b21876fd9b8088179c6e702f8addd49f890d7e1e3
|
data/README.md
CHANGED
@@ -141,7 +141,7 @@ 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:
|
@@ -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
|
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module ApplicationFixtureHelper
|
7
|
+
def write_file(path, content = '')
|
8
|
+
pathname = Pathname.new(path)
|
9
|
+
FileUtils.mkdir_p(pathname.dirname)
|
10
|
+
pathname.write(content)
|
11
|
+
path
|
12
|
+
end
|
13
|
+
|
14
|
+
def write_package_yml(
|
15
|
+
pack_name,
|
16
|
+
dependencies: [],
|
17
|
+
enforce_dependencies: true,
|
18
|
+
enforce_privacy: true,
|
19
|
+
protections: {},
|
20
|
+
global_namespaces: [],
|
21
|
+
visible_to: []
|
22
|
+
)
|
23
|
+
defaults = {
|
24
|
+
'prevent_this_package_from_violating_its_stated_dependencies' => 'fail_on_new',
|
25
|
+
'prevent_other_packages_from_using_this_packages_internals' => 'fail_on_new',
|
26
|
+
'prevent_this_package_from_exposing_an_untyped_api' => 'fail_on_new',
|
27
|
+
'prevent_this_package_from_creating_other_namespaces' => 'fail_on_new',
|
28
|
+
'prevent_other_packages_from_using_this_package_without_explicit_visibility' => 'fail_never'
|
29
|
+
}
|
30
|
+
protections_with_defaults = defaults.merge(protections)
|
31
|
+
metadata = { 'protections' => protections_with_defaults }
|
32
|
+
if visible_to.any?
|
33
|
+
metadata.merge!('visible_to' => visible_to)
|
34
|
+
end
|
35
|
+
|
36
|
+
if global_namespaces.any?
|
37
|
+
metadata.merge!('global_namespaces' => global_namespaces)
|
38
|
+
end
|
39
|
+
|
40
|
+
package = ParsePackwerk::Package.new(
|
41
|
+
name: pack_name,
|
42
|
+
dependencies: dependencies,
|
43
|
+
enforce_dependencies: enforce_dependencies,
|
44
|
+
enforce_privacy: enforce_privacy,
|
45
|
+
metadata: metadata
|
46
|
+
)
|
47
|
+
|
48
|
+
ParsePackwerk.write_package_yml!(package)
|
49
|
+
end
|
50
|
+
|
51
|
+
def delete_app_file(path)
|
52
|
+
File.delete(path)
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
def offense(
|
2
|
+
package_name, message, file, violation_type
|
3
|
+
)
|
4
|
+
|
5
|
+
package = ParsePackwerk.all.find { |p| p.name == package_name }
|
6
|
+
PackageProtections::Offense.new(
|
7
|
+
package: package,
|
8
|
+
message: message,
|
9
|
+
file: file,
|
10
|
+
violation_type: violation_type
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
def serialize_offenses_diff(actual_offenses, expected_offense)
|
15
|
+
color_by_match = ->(actual, expected) { actual == expected ? Rainbow(actual).green : "#{Rainbow(actual).red} (expected: #{expected})" }
|
16
|
+
|
17
|
+
actual_offenses.map do |offense|
|
18
|
+
# We color each field red or green depending on if the attributes match our expected
|
19
|
+
<<~SERIALIZED_OFFENSE
|
20
|
+
File: #{color_by_match.call(offense.file, expected_offense.file)}
|
21
|
+
Message: #{color_by_match.call(offense.message, expected_offense.message)}
|
22
|
+
Violation Type: #{color_by_match.call(offense.violation_type, expected_offense.violation_type)}
|
23
|
+
Package: #{color_by_match.call(offense.package.name, expected_offense.package.name)}
|
24
|
+
SERIALIZED_OFFENSE
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def serialize_offenses(actual_offenses)
|
29
|
+
actual_offenses.map do |offense|
|
30
|
+
<<~SERIALIZED_OFFENSE
|
31
|
+
File: #{offense.file}
|
32
|
+
Message: #{offense.message}
|
33
|
+
Violation Type: #{offense.violation_type}
|
34
|
+
Package: #{offense.package.name}
|
35
|
+
SERIALIZED_OFFENSE
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
RSpec::Matchers.define(:include_offense) do |expected_offense|
|
40
|
+
match do |actual_offenses|
|
41
|
+
@actual_offenses = actual_offenses
|
42
|
+
@expected_offense = expected_offense
|
43
|
+
if ENV['DEBUG']
|
44
|
+
PackageProtections.print_offenses(actual_offenses)
|
45
|
+
end
|
46
|
+
@matching_offense = actual_offenses.find do |actual_offense|
|
47
|
+
actual_offense.file == expected_offense.file &&
|
48
|
+
actual_offense.message == expected_offense.message &&
|
49
|
+
actual_offense.violation_type == expected_offense.violation_type &&
|
50
|
+
actual_offense.package.name == expected_offense.package.name
|
51
|
+
end
|
52
|
+
!@matching_offense.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
description do
|
56
|
+
"to have an offense with type `#{expected_offense.type}` tied to package `#{expected_offense.package_name}` with message `#{expected_offense.message}` and instances `#{expected_offense.submessages.join(', ')}`"
|
57
|
+
end
|
58
|
+
|
59
|
+
failure_message do
|
60
|
+
<<~MSG
|
61
|
+
Could not find offense! Here are the found offenses:
|
62
|
+
#{serialize_offenses_diff(@actual_offenses, expected_offense).join("\n\n")}
|
63
|
+
MSG
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
RSpec::Matchers.define(:contain_exactly) do |number_of_offenses|
|
68
|
+
match do |actual_offenses|
|
69
|
+
@actual_offenses = actual_offenses || []
|
70
|
+
@offenses = []
|
71
|
+
@actual_offenses.each do |offense|
|
72
|
+
@offenses << offense
|
73
|
+
end
|
74
|
+
@offenses.size == number_of_offenses
|
75
|
+
end
|
76
|
+
|
77
|
+
chain :offense, :number_of_offenses
|
78
|
+
chain :offenses, :number_of_offenses
|
79
|
+
|
80
|
+
description do
|
81
|
+
'to contain offenses'
|
82
|
+
end
|
83
|
+
|
84
|
+
failure_message_when_negated do
|
85
|
+
"Found the following offenses:\n#{@offenses.map { |r| "#{r.package_name}: #{r.message}" }}"
|
86
|
+
end
|
87
|
+
|
88
|
+
failure_message do
|
89
|
+
if @offenses.empty?
|
90
|
+
"Found #{@offenses.size} instead."
|
91
|
+
else
|
92
|
+
<<~MSG
|
93
|
+
Found #{@offenses.size} instead.
|
94
|
+
|
95
|
+
#{serialize_offenses(@offenses).join("\n")}
|
96
|
+
MSG
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Require this file to load code that supports testing using RSpec.
|
4
|
+
|
5
|
+
require_relative 'application_fixture_helper'
|
6
|
+
require_relative 'matchers'
|
7
|
+
|
8
|
+
def get_resulting_rubocop
|
9
|
+
write_file('config/default.yml', <<~YML.strip)
|
10
|
+
<%= PackageProtections.rubocop_yml %>
|
11
|
+
YML
|
12
|
+
YAML.safe_load(ERB.new(File.read('config/default.yml')).result(binding))
|
13
|
+
end
|
14
|
+
|
15
|
+
RSpec.configure do |config|
|
16
|
+
config.include ApplicationFixtureHelper
|
17
|
+
end
|
@@ -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]) }
|
@@ -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)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
|
3
3
|
module RuboCop
|
4
4
|
module Cop
|
@@ -16,6 +16,61 @@ module RuboCop
|
|
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
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
|
19
74
|
end
|
20
75
|
end
|
21
76
|
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: 1.
|
4
|
+
version: 1.2.0
|
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-07-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -181,13 +181,14 @@ 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
|
189
|
+
- lib/package_protections/rspec/application_fixture_helper.rb
|
190
|
+
- lib/package_protections/rspec/matchers.rb
|
191
|
+
- lib/package_protections/rspec/support.rb
|
191
192
|
- lib/package_protections/rubocop_protection_interface.rb
|
192
193
|
- lib/package_protections/violation_behavior.rb
|
193
194
|
- lib/rubocop/cop/package_protections/namespaced_under_package_name.rb
|
@@ -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 = 'PackageProtections/TypedPublicApi'
|
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
|