package_protections 0.64.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +149 -0
- data/config/default.yml +8 -0
- data/lib/package_protections/offense.rb +17 -0
- data/lib/package_protections/per_file_violation.rb +39 -0
- data/lib/package_protections/private/colorized_string.rb +97 -0
- data/lib/package_protections/private/incoming_privacy_protection.rb +118 -0
- data/lib/package_protections/private/metadata_modifiers.rb +27 -0
- data/lib/package_protections/private/multiple_namespaces_protection.rb +128 -0
- data/lib/package_protections/private/outgoing_dependency_protection.rb +117 -0
- data/lib/package_protections/private/output.rb +24 -0
- data/lib/package_protections/private/typed_api_protection.rb +106 -0
- data/lib/package_protections/private/visibility_protection.rb +186 -0
- data/lib/package_protections/private.rb +155 -0
- data/lib/package_protections/protected_package.rb +101 -0
- data/lib/package_protections/protection_interface.rb +69 -0
- data/lib/package_protections/rubocop_protection_interface.rb +84 -0
- data/lib/package_protections/violation_behavior.rb +69 -0
- data/lib/package_protections.rb +135 -0
- data/lib/rubocop/cop/package_protections/namespaced_under_package_name.rb +108 -0
- metadata +220 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f1b60522d3be3ff25aaab73a4c0e6d4d0585657663352b9c353d6df3307287c4
|
4
|
+
data.tar.gz: 8b57faaea71bfb8b64f842b7e2ec40cdc313bc2c674243a698c9dc7bcbc21b56
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e35ac2ae85c9459f3e480ba9939bf7e5db0902049c3643620e3ba2478a443c87dfe1ac1c9029254d7f616ac0d4821df3d07625e332a431349ff4860d5f46e64e
|
7
|
+
data.tar.gz: c90972ea3fbf86c36ea1492948fa56cd42e2b38731f6e9410c69296286d43310e7104413ccb20206392e1546e3b82c23ea25ba16defe09960718b7dadb63189f
|
data/README.md
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
# PackageProtections
|
2
|
+
|
3
|
+
This gem helps us use Packwerk and Rubocop to create well-packaged code.
|
4
|
+
The intent of this gem is two fold:
|
5
|
+
1) Provide a coherent modularization interface, where each `package.yml` is the main place you go to configure modularization checks.
|
6
|
+
2) Create hard-checks for packwerk and rubocop. Packwerk and rubocop support gradual adoption, but they don't support the ability to block adding to the TODO list once a package has fully adhered to a rule.
|
7
|
+
|
8
|
+
This gem ships with the following checks
|
9
|
+
1) Your package is not introducing dependencies that are not intended (via `packwerk` `enforce_dependencies`)
|
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` `Sorbet/StrictSigil` cop)
|
12
|
+
4) Your package only creates a single namespace (via the `rubocop` `PackageProtections/NamespacedUnderPackageName` cop)
|
13
|
+
4) Your package is only visible to a select number of packages (via the `packwerk` `enforce_privacy` cop)
|
14
|
+
|
15
|
+
## Initial Configuration
|
16
|
+
Package protections first requires that your application is using [`packwerk`](https://github.com/Shopify/packwerk), [`rubocop`](https://github.com/rubocop/rubocop), and [`rubocop-sorbet`](https://github.com/Shopify/rubocop-sorbet). Follow the regular setup instructions for those tools before proceeding.
|
17
|
+
|
18
|
+
Some of our package protections are implemented by rubocop, with their interface in `package.yml` files.
|
19
|
+
For initial configuration in a new application, you need to tell RuboCop to load the package protections extension:
|
20
|
+
```yml
|
21
|
+
# `.rubocop.yml`
|
22
|
+
inherit_gem:
|
23
|
+
package_protections:
|
24
|
+
- config/default.yml
|
25
|
+
|
26
|
+
require:
|
27
|
+
- package_protections
|
28
|
+
```
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
Today, `PackageProtections` has several built-in protections that you can configure to protect your package.
|
32
|
+
|
33
|
+
*By default, all protections are set to fail on new violations. Users need to specifically "opt out" if they do not want a protection.
|
34
|
+
We want this because we want default behavior to be our vision for well-protected packages, and deviations from the ideal vision should require explicit user action.*
|
35
|
+
Most protections set their default to `fail_on_new` instead of `fail_on_any` because we want to make it easy for users to split up packages into other ones and improve boundaries incrementally. We recommend packages for totally greenfield features use the `fail_on_any` behavior.
|
36
|
+
|
37
|
+
Lastly, note that unless a protection's default behavior is `fail_never`, the protection must explicitly be set.
|
38
|
+
|
39
|
+
To change the behavior for these protections, add the correct YAML key under `metadata.protections`. See `Example Usage` below for an example.
|
40
|
+
|
41
|
+
### `prevent_this_package_from_violating_its_stated_dependencies`
|
42
|
+
*This is only available if your package has `enforce_dependencies` set to `true`!*
|
43
|
+
This protection ensures that your package does not use API from packages that are not listed under `dependencies` in `package.yml`. This helps make sure you manage your dependencies.
|
44
|
+
|
45
|
+
### `prevent_other_packages_from_using_this_packages_internals`
|
46
|
+
*This is only available if your package has `enforce_privacy` set to `true`!*
|
47
|
+
This protection ensures that OTHER packages do not use the private API of your package. This helps ensure that clients are using your code the way you intend.
|
48
|
+
|
49
|
+
### `prevent_this_package_from_exposing_an_untyped_api`
|
50
|
+
This protection ensures that all files within `app/public` are typed at level `strict`, which means that every file must have a type signature. See https://sorbet.org/docs/static#file-level-granularity-strictness-levels for more information on typed strictness levels. Make sure to generate a TODO list if you want to use the `fail_on_new` violation behavior. See more information on generating a TODO list in the `fail_on_new` subsection under violation behaviors.
|
51
|
+
|
52
|
+
### `prevent_this_package_from_creating_other_namespaces`
|
53
|
+
*This is only available if your package is in `./packs`, `./gems`, `./components`, or `./packages`.*
|
54
|
+
This helps ensure that your package is only creating one namespace (based on folder hierarchy). This helps organize the public API of your pack into one place.
|
55
|
+
This protection only looks at files in `packs/your_pack/app` (it ignores spec files).
|
56
|
+
This protection is implemented via Rubocop -- expect to see results for this when running `rubocop` however you normally do. To add to the TODO list, add to `.rubocop_todo.yml`
|
57
|
+
Lastly – this protection can be configured by setting `global_namespaces` within the `package.yml`, e.g.:
|
58
|
+
```
|
59
|
+
enforce_privacy: true
|
60
|
+
enforce_dependencies: true
|
61
|
+
metadata:
|
62
|
+
protections:
|
63
|
+
# ... nothing changes here
|
64
|
+
global_namespaces:
|
65
|
+
- MyNamespace
|
66
|
+
- MyOtherNamespace
|
67
|
+
- MyThirdNamespace
|
68
|
+
# ... etc.
|
69
|
+
```
|
70
|
+
|
71
|
+
It's encouraged to limit the number of global namespaces your package exposes, and to make sure your global namespaces are as specific to your domain as possible.
|
72
|
+
|
73
|
+
### `prevent_other_packages_from_using_this_package_without_explicit_visibility`
|
74
|
+
*This is only available if your package has `enforce_privacy` set to `true`!*
|
75
|
+
This protection exists to help packages have control over who their clients are. When turning on this protection, only clients who are listed in your `visible_to` metadata will be allowed to consume your package. Here is an example in `packs/apples/package.yml`:
|
76
|
+
```yml
|
77
|
+
enforce_privacy: true
|
78
|
+
enforce_dependencies: true
|
79
|
+
metadata:
|
80
|
+
protections:
|
81
|
+
prevent_other_packages_from_using_this_package_without_explicit_visibility: fail_on_new
|
82
|
+
# ... other protections are the same
|
83
|
+
visible_to:
|
84
|
+
- packs/other_pack
|
85
|
+
- packs/another_pack
|
86
|
+
```
|
87
|
+
In this package, only `packs/other_pack` and `packs/another_pack` can use `packs/apples`. With both the `fail_on_new` and `fail_on_any` setting, only those packs can state a dependency on `packs/apples` in their `package.yml`. If any other packs state a dependency on `packs/apples`, the build will fail, even with violations. With the `fail_on_new` setting, a pack can create a dependency or privacy violation on `packs/apples` even if it's not listed. With `fail_on_any`, no violations are allowed.
|
88
|
+
If `visible_to` is not set and the protection is turned on, then the package cannot be consumed by any package (a top-level package might be a good candidate for this).
|
89
|
+
|
90
|
+
Note that this protection's default behavior is `fail_never`, so it can remain unset in the `package.yml`.
|
91
|
+
|
92
|
+
## Violation Behaviors
|
93
|
+
#### `fail_on_any`
|
94
|
+
If this behavior is selected, the build will fail if there is *any* issue, new or old.
|
95
|
+
#### `fail_on_new`
|
96
|
+
#### For protections from packwerk
|
97
|
+
If this behavior is selected, everything that is already in `deprecated_references.yml` is considered allowed. Think of it like `.rubocop_todo.yml`. If your PR introduces a new violation that is not captured in `deprecated_references.yml`, the build will rerun `bin/packwerk check` and fail if a new violation shows up. If for whatever reason you'd like to allow for the new violation, you can simply run `bin/packwerk update-deprecations` locally and commit the changes to `deprecated_references.yml` files.
|
98
|
+
#### For protections from rubocop
|
99
|
+
Similar to above, but instead of `deprecated_references.yml`, violations are stored in your `.rubocop_todo.yml` file. You can add to that file to bypass protections at this level.
|
100
|
+
|
101
|
+
#### `fail_never`
|
102
|
+
If this behavior is selected, the protection will not be active.
|
103
|
+
|
104
|
+
## Example Usage
|
105
|
+
This is an example package that is focused on having a typed API that respects other teams' stated boundaries.
|
106
|
+
|
107
|
+
```yml
|
108
|
+
enforce_dependencies: true
|
109
|
+
enforce_privacy: true
|
110
|
+
metadata:
|
111
|
+
protections:
|
112
|
+
prevent_this_package_from_violating_its_stated_dependencies: fail_never
|
113
|
+
prevent_other_packages_from_using_this_packages_internals: fail_never
|
114
|
+
prevent_this_package_from_exposing_an_untyped_api: fail_on_any
|
115
|
+
prevent_this_package_from_creating_other_namespaces: fail_never
|
116
|
+
```
|
117
|
+
|
118
|
+
## PackageProtections.set_defaults!
|
119
|
+
Calling `PackageProtections.set_defaults!(...)` will make sure that all available protections are set in the protections metadata key without changing any protection behaviors that are already set.
|
120
|
+
|
121
|
+
### Example Usage
|
122
|
+
```ruby
|
123
|
+
# get your packages
|
124
|
+
packages = ParsePackwerk.all
|
125
|
+
# then set defaults
|
126
|
+
PackageProtections.set_defaults!(packages)
|
127
|
+
# or just set defaults for one package
|
128
|
+
PackageProtections.set_defaults!(packages.select{|p| p.package_name == 'packs/my_package'})
|
129
|
+
```
|
130
|
+
|
131
|
+
## Incorporating into your CI Pipeline
|
132
|
+
Your CI pipeline can execute the public API ta and fail if there are any offenses.
|
133
|
+
|
134
|
+
## Discussions, Issues, Questions, and More
|
135
|
+
To keep things organized, here are some recommended homes:
|
136
|
+
### Issues:
|
137
|
+
https://github.com/bigrails/package_protections/issues
|
138
|
+
|
139
|
+
### Questions:
|
140
|
+
https://github.com/bigrails/package_protections/discussions/categories/q-a
|
141
|
+
|
142
|
+
### General discussions:
|
143
|
+
https://github.com/bigrails/package_protections/discussions/categories/general
|
144
|
+
|
145
|
+
### Ideas, new features, requests for change:
|
146
|
+
https://github.com/bigrails/package_protections/discussions/categories/ideas
|
147
|
+
|
148
|
+
### Showcasing your work:
|
149
|
+
https://github.com/bigrails/package_protections/discussions/categories/show-and-tell
|
data/config/default.yml
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# Relevant documentation:
|
2
|
+
# - Inheriting config from a gem:
|
3
|
+
# - https://docs.rubocop.org/rubocop/configuration.html#inheriting-configuration-from-a-dependency-gem
|
4
|
+
# - ERB in a .rubocop.yml file
|
5
|
+
# - https://docs.rubocop.org/rubocop/configuration.html#pre-processing
|
6
|
+
# - Client usage
|
7
|
+
# - README.md in this repo
|
8
|
+
<%= PackageProtections.rubocop_yml %>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module PackageProtections
|
4
|
+
class Offense < T::Struct
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
const :file, String
|
8
|
+
const :message, String
|
9
|
+
const :violation_type, Identifier
|
10
|
+
const :package, ParsePackwerk::Package
|
11
|
+
|
12
|
+
sig { returns(String) }
|
13
|
+
def package_name
|
14
|
+
package.name
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
module PackageProtections
|
5
|
+
# Perhaps this should be in ParsePackwerk. For now, this is here to help us break down violations per file.
|
6
|
+
# This is analogous to `Packwerk::ReferenceOffense`
|
7
|
+
class PerFileViolation < T::Struct
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
const :class_name, String
|
11
|
+
const :filepath, String
|
12
|
+
const :type, String
|
13
|
+
const :constant_source_package, String
|
14
|
+
const :reference_source_package, ParsePackwerk::Package
|
15
|
+
|
16
|
+
sig { params(violation: ParsePackwerk::Violation, reference_source_package: ParsePackwerk::Package).returns(T::Array[PerFileViolation]) }
|
17
|
+
def self.from(violation, reference_source_package)
|
18
|
+
violation.files.map do |file|
|
19
|
+
PerFileViolation.new(
|
20
|
+
type: violation.type,
|
21
|
+
class_name: violation.class_name,
|
22
|
+
filepath: file,
|
23
|
+
constant_source_package: violation.to_package_name,
|
24
|
+
reference_source_package: reference_source_package
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { returns(T::Boolean) }
|
30
|
+
def dependency?
|
31
|
+
type == 'dependency'
|
32
|
+
end
|
33
|
+
|
34
|
+
sig { returns(T::Boolean) }
|
35
|
+
def privacy?
|
36
|
+
type == 'privacy'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module PackageProtections
|
5
|
+
module Private
|
6
|
+
class ColorizedString
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
class Color < T::Enum
|
10
|
+
enums do
|
11
|
+
Black = new
|
12
|
+
Red = new
|
13
|
+
Green = new
|
14
|
+
Yellow = new
|
15
|
+
Blue = new
|
16
|
+
Pink = new
|
17
|
+
LightBlue = new
|
18
|
+
White = new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { params(original_string: String, color: Color).void }
|
23
|
+
def initialize(original_string, color = Color::White)
|
24
|
+
@original_string = original_string
|
25
|
+
@color = color
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { returns(String) }
|
29
|
+
def colorized_to_s
|
30
|
+
"\e[#{color_code}m#{@original_string}\e[0m"
|
31
|
+
end
|
32
|
+
|
33
|
+
sig { returns(String) }
|
34
|
+
def to_s
|
35
|
+
@original_string
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { returns(ColorizedString) }
|
39
|
+
def red
|
40
|
+
colorize(Color::Red)
|
41
|
+
end
|
42
|
+
|
43
|
+
sig { returns(ColorizedString) }
|
44
|
+
def green
|
45
|
+
colorize(Color::Green)
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { returns(ColorizedString) }
|
49
|
+
def yellow
|
50
|
+
colorize(Color::Yellow)
|
51
|
+
end
|
52
|
+
|
53
|
+
sig { returns(ColorizedString) }
|
54
|
+
def blue
|
55
|
+
colorize(Color::Blue)
|
56
|
+
end
|
57
|
+
|
58
|
+
sig { returns(ColorizedString) }
|
59
|
+
def pink
|
60
|
+
colorize(Color::Pink)
|
61
|
+
end
|
62
|
+
|
63
|
+
sig { returns(ColorizedString) }
|
64
|
+
def light_blue
|
65
|
+
colorize(Color::LightBlue)
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { returns(ColorizedString) }
|
69
|
+
def white
|
70
|
+
colorize(Color::White)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
sig { params(color: Color).returns(ColorizedString) }
|
76
|
+
def colorize(color)
|
77
|
+
self.class.new(@original_string, color)
|
78
|
+
end
|
79
|
+
|
80
|
+
sig { returns(Integer) }
|
81
|
+
def color_code
|
82
|
+
case @color
|
83
|
+
when Color::Black then 30
|
84
|
+
when Color::Red then 31
|
85
|
+
when Color::Green then 32
|
86
|
+
when Color::Yellow then 33
|
87
|
+
when Color::Blue then 34
|
88
|
+
when Color::Pink then 35
|
89
|
+
when Color::LightBlue then 36
|
90
|
+
when Color::White then 37
|
91
|
+
else
|
92
|
+
T.absurd(@color)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module PackageProtections
|
5
|
+
module Private
|
6
|
+
class IncomingPrivacyProtection
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
include ProtectionInterface
|
10
|
+
|
11
|
+
IDENTIFIER = 'prevent_other_packages_from_using_this_packages_internals'
|
12
|
+
|
13
|
+
sig { override.returns(String) }
|
14
|
+
def identifier
|
15
|
+
IDENTIFIER
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { override.params(behavior: ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
|
19
|
+
def unmet_preconditions_for_behavior(behavior, package)
|
20
|
+
if behavior.enabled? && !package.enforces_privacy?
|
21
|
+
"Package #{package.name} must have `enforce_privacy: true` to use this protection"
|
22
|
+
elsif !behavior.enabled? && package.enforces_privacy?
|
23
|
+
"Package #{package.name} must have `enforce_privacy: false` to turn this protection off"
|
24
|
+
else
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { override.returns(String) }
|
30
|
+
def humanized_protection_name
|
31
|
+
'Privacy Violations'
|
32
|
+
end
|
33
|
+
|
34
|
+
sig { override.returns(String) }
|
35
|
+
def humanized_protection_description
|
36
|
+
<<~MESSAGE
|
37
|
+
To resolve these violations, check the `public/` folder in each pack for public constants and APIs.
|
38
|
+
If you need help or can't find what you need to meet your use case, reach out to the owning team.
|
39
|
+
See https://go/packwerk_cheatsheet_privacy for more info.
|
40
|
+
MESSAGE
|
41
|
+
end
|
42
|
+
|
43
|
+
sig do
|
44
|
+
override.params(
|
45
|
+
new_violations: T::Array[PerFileViolation]
|
46
|
+
).returns(T::Array[Offense])
|
47
|
+
end
|
48
|
+
def get_offenses_for_new_violations(new_violations)
|
49
|
+
new_violations.select(&:privacy?).flat_map do |per_file_violation|
|
50
|
+
protected_package = Private.get_package_with_name(per_file_violation.constant_source_package)
|
51
|
+
violation_behavior = protected_package.violation_behavior_for(identifier)
|
52
|
+
|
53
|
+
case violation_behavior
|
54
|
+
when ViolationBehavior::FailNever
|
55
|
+
next []
|
56
|
+
when ViolationBehavior::FailOnNew
|
57
|
+
message = message_for_fail_on_new(per_file_violation)
|
58
|
+
when ViolationBehavior::FailOnAny
|
59
|
+
message = message_for_fail_on_any(per_file_violation)
|
60
|
+
else
|
61
|
+
T.absurd(violation_behavior)
|
62
|
+
end
|
63
|
+
|
64
|
+
Offense.new(
|
65
|
+
file: per_file_violation.filepath,
|
66
|
+
message: message,
|
67
|
+
violation_type: identifier,
|
68
|
+
package: protected_package.original_package
|
69
|
+
)
|
70
|
+
end
|
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
|
+
all_listed_violations = protected_packages.flat_map do |protected_package|
|
80
|
+
protected_package.violations.select(&:privacy?).flat_map do |violation|
|
81
|
+
PerFileViolation.from(violation, protected_package.original_package)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
all_listed_violations.flat_map do |per_file_violation|
|
86
|
+
constant_source_package = Private.get_package_with_name(per_file_violation.constant_source_package)
|
87
|
+
violation_behavior = constant_source_package.violation_behavior_for(identifier)
|
88
|
+
|
89
|
+
case violation_behavior
|
90
|
+
when ViolationBehavior::FailNever, ViolationBehavior::FailOnNew
|
91
|
+
[]
|
92
|
+
when ViolationBehavior::FailOnAny
|
93
|
+
Offense.new(
|
94
|
+
file: per_file_violation.filepath,
|
95
|
+
message: message_for_fail_on_any(per_file_violation),
|
96
|
+
violation_type: identifier,
|
97
|
+
package: constant_source_package.original_package
|
98
|
+
)
|
99
|
+
else
|
100
|
+
T.absurd(violation_behavior)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
sig { params(per_file_violation: PerFileViolation).returns(String) }
|
108
|
+
def message_for_fail_on_any(per_file_violation)
|
109
|
+
"#{message_for_fail_on_new(per_file_violation)} (`#{per_file_violation.constant_source_package}` set to `fail_on_any`)"
|
110
|
+
end
|
111
|
+
|
112
|
+
sig { params(per_file_violation: PerFileViolation).returns(String) }
|
113
|
+
def message_for_fail_on_new(per_file_violation)
|
114
|
+
"`#{per_file_violation.filepath}` references private `#{per_file_violation.class_name}` from `#{per_file_violation.constant_source_package}`"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
module PackageProtections
|
5
|
+
module Private
|
6
|
+
class MetadataModifiers
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { params(package: ParsePackwerk::Package, protection_identifier: Identifier, violation_behavior: ViolationBehavior).returns(ParsePackwerk::Package) }
|
10
|
+
def self.package_with_modified_protection(package, protection_identifier, violation_behavior)
|
11
|
+
# We dup this to prevent mutations to the original underlying hash
|
12
|
+
new_metadata = package.metadata.dup
|
13
|
+
protections = new_metadata['protections'].dup || {}
|
14
|
+
protections[protection_identifier] = violation_behavior.serialize
|
15
|
+
new_metadata['protections'] = protections
|
16
|
+
|
17
|
+
ParsePackwerk::Package.new(
|
18
|
+
name: package.name,
|
19
|
+
enforce_dependencies: package.enforce_dependencies,
|
20
|
+
enforce_privacy: package.enforce_privacy,
|
21
|
+
dependencies: package.dependencies,
|
22
|
+
metadata: new_metadata
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,128 @@
|
|
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
|