package_protections 0.64.0

Sign up to get free protection for your applications and to get access to all the features.
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