package_protections 0.64.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 +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
|