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 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
@@ -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