package_protections 1.2.0 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41803352508cd5d4ede0fe0caf1121516474fe3e95a030e4db7e0f05badfa9a2
4
- data.tar.gz: bc594511c3b3da35dcd9efcfba0f989928e074e0da429a8f0f538e0537eff5be
3
+ metadata.gz: 2e7bd9febdd0619b714c112033b8bf61958e04368e8fb0c0e5f3498de54e988c
4
+ data.tar.gz: a0f7c5f79f3eb1279d084045c115345424dd2acfa22954eed321d2cf220bd40b
5
5
  SHA512:
6
- metadata.gz: 352aa3ea22bf0d7cf765023c35ed88c6026fda2b52036aa73fbcf35e4aca1df52df24486ce028a238dcf50db89f2864f4080e77d6da52d9f7be7fa984cd70dc7
7
- data.tar.gz: 6bb8ab188d27a3656cd6979d00fcaf725165176b17a42704be114ef0bbfbe7150facf21ae44ebc3c1633ce5b21876fd9b8088179c6e702f8addd49f890d7e1e3
6
+ metadata.gz: b8ff41de5e829270999baa91dc8df0fb3e810c6e8be39358cdbc6fd188a9d79ddf138f19b935a13360106c2a5e6c8300edffdf5fedf71e39c52826d2a6050a9e
7
+ data.tar.gz: f255780ea5ed693e66edcd028b4b8247419c612afddd1790f53078ecec3ca274e456c6ed16e62a0d83158c853f2be25282250c5b40b93b069da504469d25acf8
@@ -8,9 +8,13 @@ module PackageProtections
8
8
  sig { params(protections: T::Array[ProtectionInterface]).void }
9
9
  attr_writer :protections
10
10
 
11
+ sig { params(globally_permitted_namespaces: T::Array[String]).void }
12
+ attr_writer :globally_permitted_namespaces
13
+
11
14
  sig { void }
12
15
  def initialize
13
16
  @protections = T.let(default_protections, T::Array[ProtectionInterface])
17
+ @globally_permitted_namespaces = T.let([], T::Array[String])
14
18
  end
15
19
 
16
20
  sig { returns(T::Array[ProtectionInterface]) }
@@ -18,9 +22,15 @@ module PackageProtections
18
22
  @protections
19
23
  end
20
24
 
25
+ sig { returns(T::Array[String]) }
26
+ def globally_permitted_namespaces
27
+ @globally_permitted_namespaces
28
+ end
29
+
21
30
  sig { void }
22
31
  def bust_cache!
23
32
  @protections = default_protections
33
+ @globally_permitted_namespaces = []
24
34
  end
25
35
 
26
36
  sig { returns(T::Array[ProtectionInterface]) }
@@ -17,12 +17,6 @@ module PackageProtections
17
17
  module Private
18
18
  extend T::Sig
19
19
 
20
- sig { returns(Private::Configuration) }
21
- def self.config
22
- @config = T.let(@config, T.nilable(Configuration))
23
- @config ||= Private::Configuration.new
24
- end
25
-
26
20
  sig do
27
21
  params(
28
22
  packages: T::Array[ParsePackwerk::Package],
@@ -88,27 +82,6 @@ module PackageProtections
88
82
  end
89
83
  end
90
84
 
91
- sig do
92
- params(
93
- package_names: T::Array[String],
94
- all_packages: T::Array[ParsePackwerk::Package]
95
- ).returns(T::Array[ParsePackwerk::Package])
96
- end
97
- def self.packages_for_names(package_names, all_packages)
98
- all_packages_indexed_by_name = {}
99
- all_packages.each { |package| all_packages_indexed_by_name[package.name] = package }
100
-
101
- package_names.map do |package_name|
102
- clean_pack_name = package_name.gsub(%r{/$}, '')
103
- package = all_packages_indexed_by_name[clean_pack_name]
104
- if package.nil?
105
- raise "Sorry, we couldn't find a package with name #{package_name}. Here are all of the package names we know about: #{all_packages.map(&:name).sort.inspect}"
106
- end
107
-
108
- package
109
- end
110
- end
111
-
112
85
  sig { params(root_pathname: Pathname).returns(String) }
113
86
  def self.rubocop_yml(root_pathname:)
114
87
  protected_packages = Dir.chdir(root_pathname) { all_protected_packages }
@@ -143,7 +116,7 @@ module PackageProtections
143
116
  def self.bust_cache!
144
117
  @protected_packages_indexed_by_name = nil
145
118
  @private_cop_config = nil
146
- config.bust_cache!
119
+ PackageProtections.config.bust_cache!
147
120
  end
148
121
 
149
122
  sig { params(identifier: Identifier).returns(T::Hash[T.untyped, T.untyped]) }
@@ -7,11 +7,15 @@ require_relative 'matchers'
7
7
 
8
8
  def get_resulting_rubocop
9
9
  write_file('config/default.yml', <<~YML.strip)
10
- <%= PackageProtections.rubocop_yml %>
10
+ <%= PackageProtections.rubocop_yml(root_pathname: Pathname.pwd) %>
11
11
  YML
12
12
  YAML.safe_load(ERB.new(File.read('config/default.yml')).result(binding))
13
13
  end
14
14
 
15
15
  RSpec.configure do |config|
16
16
  config.include ApplicationFixtureHelper
17
+
18
+ config.before do
19
+ PackageProtections.bust_cache!
20
+ end
17
21
  end
@@ -44,13 +44,19 @@ module PackageProtections
44
44
 
45
45
  sig { params(blk: T.proc.params(arg0: Private::Configuration).void).void }
46
46
  def configure(&blk)
47
- yield(Private.config)
47
+ yield(PackageProtections.config)
48
48
  end
49
49
  end
50
50
 
51
51
  sig { returns(T::Array[ProtectionInterface]) }
52
52
  def self.all
53
- Private.config.protections
53
+ config.protections
54
+ end
55
+
56
+ sig { returns(Private::Configuration) }
57
+ def self.config
58
+ @config = T.let(@config, T.nilable(Private::Configuration))
59
+ @config ||= Private::Configuration.new
54
60
  end
55
61
 
56
62
  #
@@ -116,16 +122,6 @@ module PackageProtections
116
122
  Private.private_cop_config(identifier)
117
123
  end
118
124
 
119
- sig do
120
- params(
121
- package_names: T::Array[String],
122
- all_packages: T::Array[ParsePackwerk::Package]
123
- ).returns(T::Array[ParsePackwerk::Package])
124
- end
125
- def self.packages_for_names(package_names, all_packages)
126
- Private.packages_for_names(package_names, all_packages)
127
- end
128
-
129
125
  sig { void }
130
126
  def self.bust_cache!
131
127
  Private.bust_cache!
@@ -0,0 +1,110 @@
1
+ # typed: strict
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module PackageProtections
6
+ class NamespacedUnderPackageName < Base
7
+ #
8
+ # This is a private class that represents API that we would prefer to be available somehow in Zeitwerk.
9
+ #
10
+ class DesiredZeitwerkApi
11
+ extend T::Sig
12
+
13
+ class NamespaceContext < T::Struct
14
+ const :current_namespace, String
15
+ const :expected_namespace, String
16
+ const :expected_filepath, String
17
+ end
18
+
19
+ #
20
+ # For now, this API includes `package_for_path`
21
+ # If this were truly zeitwerk API, it wouldn't include any mention of packs and it would likely not need the package at all
22
+ # Since it could get the actual namespace without knowing anything about packs.
23
+ # However, we would need to pass to it the desired namespace based on the pack name for it to be able to suggest
24
+ # a desired filepath.
25
+ # Likely this means that our own cop should determine the desired namespace and pass that in
26
+ # and this can determine actual namespace and how to get to expected.
27
+ #
28
+ sig { params(relative_filename: String, package_for_path: ParsePackwerk::Package).returns(T.nilable(NamespaceContext)) }
29
+ def for_file(relative_filename, package_for_path)
30
+ package_name = package_for_path.name
31
+
32
+ # Zeitwerk establishes a standard convention by which namespaces are defined.
33
+ # The package protections namespace checker is coupled to a specific assumption about how auto-loading works.
34
+ #
35
+ # Namely, we expect the following autoload paths: `packs/**/app/**/`
36
+ # Examples:
37
+ # 1) `packs/package_1/app/public/package_1/my_constant.rb` produces constant `Package1::MyConstant`
38
+ # 2) `packs/package_1/app/services/package_1/my_service.rb` produces constant `Package1::MyService`
39
+ # 3) `packs/package_1/app/services/package_1.rb` produces constant `Package1`
40
+ # 4) `packs/package_1/app/public/package_1.rb` produces constant `Package1`
41
+ #
42
+ # Described another way, we expect any part of the directory labeled NAMESPACE to establish a portion of the fully qualified runtime constant:
43
+ # `packs/**/app/**/NAMESPACE1/NAMESPACE2/[etc]`
44
+ #
45
+ # Therefore, for our implementation, we substitute out the non-namespace producing portions of the filename to count the number of namespaces.
46
+ # Note this will *not work* properly in applications that have different assumptions about autoloading.
47
+
48
+ path_without_package_base = relative_filename.gsub(%r{#{package_name}/app/}, '')
49
+ if path_without_package_base.include?('concerns')
50
+ autoload_folder_name = path_without_package_base.split('/').first(2).join('/')
51
+ else
52
+ autoload_folder_name = path_without_package_base.split('/').first
53
+ end
54
+
55
+ remaining_file_path = path_without_package_base.gsub(%r{\A#{autoload_folder_name}/}, '')
56
+ actual_namespace = get_actual_namespace(remaining_file_path, package_name)
57
+
58
+ if relative_filename.include?('app/')
59
+ app_or_lib = 'app'
60
+ elsif relative_filename.include?('lib/')
61
+ app_or_lib = 'lib'
62
+ end
63
+
64
+ absolute_desired_path = root_pathname.join(
65
+ package_name,
66
+ T.must(app_or_lib),
67
+ T.must(autoload_folder_name),
68
+ get_package_last_name(package_for_path),
69
+ remaining_file_path
70
+ )
71
+
72
+ relative_desired_path = absolute_desired_path.relative_path_from(root_pathname)
73
+
74
+ NamespaceContext.new(
75
+ current_namespace: actual_namespace,
76
+ expected_namespace: get_pack_based_namespace(package_for_path),
77
+ expected_filepath: relative_desired_path.to_s
78
+ )
79
+ end
80
+
81
+ sig { params(pack: ParsePackwerk::Package).returns(String) }
82
+ def get_pack_based_namespace(pack)
83
+ get_package_last_name(pack).camelize
84
+ end
85
+
86
+ private
87
+
88
+ sig { returns(Pathname) }
89
+ def root_pathname
90
+ Pathname.pwd
91
+ end
92
+
93
+ sig { params(pack: ParsePackwerk::Package).returns(String) }
94
+ def get_package_last_name(pack)
95
+ T.must(pack.name.split('/').last)
96
+ end
97
+
98
+ sig { params(remaining_file_path: String, package_name: String).returns(String) }
99
+ def get_actual_namespace(remaining_file_path, package_name)
100
+ # If the remaining file path is a ruby file (not a directory), then it establishes a global namespace
101
+ # Otherwise, per Zeitwerk's conventions as listed above, its a directory that establishes another global namespace
102
+ T.must(remaining_file_path.split('/').first).gsub('.rb', '').camelize
103
+ end
104
+ end
105
+
106
+ private_constant :DesiredZeitwerkApi
107
+ end
108
+ end
109
+ end
110
+ end
@@ -1,94 +1,109 @@
1
- # typed: true
1
+ # typed: strict
2
2
 
3
3
  # For String#camelize
4
4
  require 'active_support/core_ext/string/inflections'
5
+ require 'rubocop/cop/package_protections/namespaced_under_package_name/desired_zeitwerk_api'
5
6
 
6
7
  module RuboCop
7
8
  module Cop
8
9
  module PackageProtections
10
+ #
11
+ # TODO:
12
+ # This class is in serious need of being split up into helpful abstractions.
13
+ # A really helpful abstraction would be one that takes a file path and can spit out information about
14
+ # namespacing, such as the exposed namespace, the file path for a different namespace, and more.
15
+ #
9
16
  class NamespacedUnderPackageName < Base
10
17
  extend T::Sig
11
18
 
12
19
  include RangeHelp
13
20
  include ::PackageProtections::RubocopProtectionInterface
14
21
 
22
+ sig { void }
15
23
  def on_new_investigation
16
24
  absolute_filepath = Pathname.new(processed_source.file_path)
17
25
  relative_filepath = absolute_filepath.relative_path_from(Pathname.pwd)
18
26
  relative_filename = relative_filepath.to_s
19
27
 
20
- # This cop only works for files in `app`
21
- return if !relative_filename.include?('app/')
22
-
23
- match = relative_filename.match(%r{((#{::PackageProtections::EXPECTED_PACK_DIRECTORIES.join("|")})/.*?)/})
24
- package_name = match && match[1]
25
-
26
- return if package_name.nil?
27
-
28
- return if relative_filepath.extname != '.rb'
29
-
30
- # Zeitwerk establishes a standard convention by which namespaces are defined.
31
- # The package protections namespace checker is coupled to a specific assumption about how auto-loading works.
32
- #
33
- # Namely, we expect the following autoload paths: `packs/**/app/**/`
34
- # Examples:
35
- # 1) `packs/package_1/app/public/package_1/my_constant.rb` produces constant `Package1::MyConstant`
36
- # 2) `packs/package_1/app/services/package_1/my_service.rb` produces constant `Package1::MyService`
37
- # 3) `packs/package_1/app/services/package_1.rb` produces constant `Package1`
38
- # 4) `packs/package_1/app/public/package_1.rb` produces constant `Package1`
39
- #
40
- # Described another way, we expect any part of the directory labeled NAMESPACE to establish a portion of the fully qualified runtime constant:
41
- # `packs/**/app/**/NAMESPACE1/NAMESPACE2/[etc]`
42
- #
43
- # Therefore, for our implementation, we substitute out the non-namespace producing portions of the filename to count the number of namespaces.
44
- # Note this will *not work* properly in applications that have different assumptions about autoloading.
45
- package_last_name = T.must(package_name.split('/').last)
46
- path_without_package_base = relative_filename.gsub(%r{#{package_name}/app/}, '')
47
- if path_without_package_base.include?('concerns')
48
- autoload_folder_name = path_without_package_base.split('/').first(2).join('/')
49
- else
50
- autoload_folder_name = path_without_package_base.split('/').first
51
- end
28
+ # This cop only works for files ruby files in `app`
29
+ return if !relative_filename.include?('app/') || relative_filepath.extname != '.rb'
52
30
 
53
- remaining_file_path = path_without_package_base.gsub(%r{\A#{autoload_folder_name}/}, '')
54
- actual_namespace = get_actual_namespace(remaining_file_path, relative_filepath, package_name)
55
- allowed_namespaces = get_allowed_namespaces(package_name)
56
- if allowed_namespaces.include?(actual_namespace)
57
- # No problem!
58
- elsif allowed_namespaces.count == 1
59
- single_allowed_namespace = allowed_namespaces.first
60
- if relative_filepath.to_s.include?('app/')
61
- app_or_lib = 'app'
62
- elsif relative_filepath.to_s.include?('lib/')
63
- app_or_lib = 'lib'
64
- end
31
+ relative_filename = relative_filepath.to_s
32
+ package_for_path = ParsePackwerk.package_from_path(relative_filename)
33
+ return if package_for_path.nil?
65
34
 
66
- absolute_desired_path = root_pathname.join(package_name, app_or_lib, T.must(autoload_folder_name), single_allowed_namespace.underscore, remaining_file_path)
35
+ namespace_context = self.class.desired_zeitwerk_api.for_file(relative_filename, package_for_path)
36
+ return if namespace_context.nil?
67
37
 
68
- relative_desired_path = absolute_desired_path.relative_path_from(root_pathname)
38
+ allowed_global_namespaces = Set.new([
39
+ namespace_context.expected_namespace,
40
+ *::PackageProtections.config.globally_permitted_namespaces
41
+ ])
69
42
 
70
- add_offense(
71
- source_range(processed_source.buffer, 1, 0),
72
- message: format(
73
- '`%<package_name>s` prevents modules/classes that are not submodules of the package namespace. Should be namespaced under `%<expected_namespace>s` with path `%<expected_path>s`. See https://go/packwerk_cheatsheet_namespaces for more info.',
74
- package_name: package_name,
75
- expected_namespace: package_last_name.camelize,
76
- expected_path: relative_desired_path
77
- )
78
- )
43
+ package_name = package_for_path.name
44
+ actual_namespace = namespace_context.current_namespace
45
+
46
+ if allowed_global_namespaces.include?(actual_namespace)
47
+ # No problem!
79
48
  else
80
- add_offense(
81
- source_range(processed_source.buffer, 1, 0),
82
- message: format(
83
- '`%<package_name>s` prevents modules/classes that are not submodules of one of the allowed namespaces in `%<package_yml>s`. See https://go/packwerk_cheatsheet_namespaces for more info.',
84
- package_name: package_name,
85
- package_yml: "#{package_name}/package.yml"
49
+ package_enforces_namespaces = !::PackageProtections::ProtectedPackage.from(package_for_path).violation_behavior_for(NamespacedUnderPackageName::IDENTIFIER).fail_never?
50
+ expected_namespace = namespace_context.expected_namespace
51
+ relative_desired_path = namespace_context.expected_filepath
52
+ pack_owning_this_namespace = self.class.namespaces_to_packs[actual_namespace]
53
+
54
+ if package_enforces_namespaces
55
+ add_offense(
56
+ source_range(processed_source.buffer, 1, 0),
57
+ message: format(
58
+ '`%<package_name>s` prevents modules/classes that are not submodules of the package namespace. Should be namespaced under `%<expected_namespace>s` with path `%<expected_path>s`. See https://go/packwerk_cheatsheet_namespaces for more info.',
59
+ package_name: package_name,
60
+ expected_namespace: expected_namespace,
61
+ expected_path: relative_desired_path
62
+ )
86
63
  )
87
- )
64
+ elsif pack_owning_this_namespace
65
+ add_offense(
66
+ source_range(processed_source.buffer, 1, 0),
67
+ message: format(
68
+ '`%<pack_owning_this_namespace>s` prevents other packs from sitting in the `%<actual_namespace>s` namespace. This should be namespaced under `%<expected_namespace>s` with path `%<expected_path>s`. See https://go/packwerk_cheatsheet_namespaces for more info.',
69
+ package_name: package_name,
70
+ pack_owning_this_namespace: pack_owning_this_namespace,
71
+ expected_namespace: expected_namespace,
72
+ actual_namespace: actual_namespace,
73
+ expected_path: relative_desired_path
74
+ )
75
+ )
76
+ end
88
77
  end
89
78
  end
90
79
 
91
- IDENTIFIER = 'prevent_this_package_from_creating_other_namespaces'.freeze
80
+ # We override `cop_configs` for this protection.
81
+ # The default behavior disables cops when a package has turned off a protection.
82
+ # However: namespace violations can occur even when one package has TURNED OFF their namespace protection
83
+ # but another package has it turned on. Therefore, all packages must always be opted in no matter what.
84
+ #
85
+ sig do
86
+ params(packages: T::Array[::PackageProtections::ProtectedPackage])
87
+ .returns(T::Array[::PackageProtections::RubocopProtectionInterface::CopConfig])
88
+ end
89
+ def cop_configs(packages)
90
+ include_paths = T.let([], T::Array[String])
91
+ packages.each do |p|
92
+ included_globs_for_pack.each do |glob|
93
+ include_paths << p.original_package.directory.join(glob).to_s
94
+ end
95
+ end
96
+
97
+ [
98
+ ::PackageProtections::RubocopProtectionInterface::CopConfig.new(
99
+ name: cop_name,
100
+ enabled: include_paths.any?,
101
+ include_paths: include_paths
102
+ )
103
+ ]
104
+ end
105
+
106
+ IDENTIFIER = T.let('prevent_this_package_from_creating_other_namespaces'.freeze, String)
92
107
 
93
108
  sig { override.returns(String) }
94
109
  def identifier
@@ -105,7 +120,11 @@ module RuboCop
105
120
 
106
121
  # The reason for this is precondition is the `MultipleNamespacesProtection` assumes this to work properly.
107
122
  # To remove this precondition, we need to modify `MultipleNamespacesProtection` to be more generalized!
108
- if ::PackageProtections::EXPECTED_PACK_DIRECTORIES.include?(Pathname.new(package.name).dirname.to_s) || package.name == ParsePackwerk::ROOT_PACKAGE_NAME
123
+ is_root_package = package.name == ParsePackwerk::ROOT_PACKAGE_NAME
124
+ in_allowed_directory = ::PackageProtections::EXPECTED_PACK_DIRECTORIES.any? do |expected_package_directory|
125
+ package.directory.to_s.start_with?(expected_package_directory)
126
+ end
127
+ if in_allowed_directory || is_root_package
109
128
  nil
110
129
  else
111
130
  "Package #{package.name} must be located in one of #{::PackageProtections::EXPECTED_PACK_DIRECTORIES.join(', ')} (or be the root) to use this protection"
@@ -121,15 +140,6 @@ module RuboCop
121
140
  ]
122
141
  end
123
142
 
124
- sig do
125
- params(package: ::PackageProtections::ProtectedPackage).returns(T::Hash[T.untyped, T.untyped])
126
- end
127
- def custom_cop_config(package)
128
- {
129
- 'GlobalNamespaces' => package.metadata['global_namespaces']
130
- }
131
- end
132
-
133
143
  sig do
134
144
  override.params(file: String).returns(String)
135
145
  end
@@ -159,22 +169,29 @@ module RuboCop
159
169
  MESSAGE
160
170
  end
161
171
 
162
- private
163
-
164
- def get_allowed_namespaces(package_name)
165
- cop_config = ::PackageProtections.private_cop_config('prevent_this_package_from_creating_other_namespaces')
166
- allowed_global_namespaces = cop_config[package_name]['GlobalNamespaces'] || [package_name.split('/').last.camelize]
167
- Set.new(allowed_global_namespaces)
172
+ sig { returns(DesiredZeitwerkApi) }
173
+ def self.desired_zeitwerk_api
174
+ # This is cached at the class level so we will cache more expensive operations
175
+ # across rubocop requests.
176
+ @desired_zeitwerk_api ||= T.let(nil, T.nilable(DesiredZeitwerkApi))
177
+ @desired_zeitwerk_api ||= DesiredZeitwerkApi.new
168
178
  end
169
179
 
170
- def get_actual_namespace(remaining_file_path, relative_filepath, package_name)
171
- # If the remaining file path is a ruby file (not a directory), then it establishes a global namespace
172
- # Otherwise, per Zeitwerk's conventions as listed above, its a directory that establishes another global namespace
173
- remaining_file_path.split('/').first.gsub('.rb', '').camelize
174
- end
180
+ sig { returns(T::Hash[String, String]) }
181
+ def self.namespaces_to_packs
182
+ @namespaces_to_packs = T.let(nil, T.nilable(T::Hash[String, String]))
183
+ @namespaces_to_packs ||= begin
184
+ all_packs_enforcing_namespaces = ParsePackwerk.all.reject do |p|
185
+ ::PackageProtections::ProtectedPackage.from(p).violation_behavior_for(NamespacedUnderPackageName::IDENTIFIER).fail_never?
186
+ end
187
+
188
+ namespaces_to_packs = {}
189
+ all_packs_enforcing_namespaces.each do |package|
190
+ namespaces_to_packs[desired_zeitwerk_api.get_pack_based_namespace(package)] = package.name
191
+ end
175
192
 
176
- def root_pathname
177
- Pathname.pwd
193
+ namespaces_to_packs
194
+ end
178
195
  end
179
196
  end
180
197
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: package_protections
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gusto Engineers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-01 00:00:00.000000000 Z
11
+ date: 2022-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -192,6 +192,7 @@ files:
192
192
  - lib/package_protections/rubocop_protection_interface.rb
193
193
  - lib/package_protections/violation_behavior.rb
194
194
  - lib/rubocop/cop/package_protections/namespaced_under_package_name.rb
195
+ - lib/rubocop/cop/package_protections/namespaced_under_package_name/desired_zeitwerk_api.rb
195
196
  - lib/rubocop/cop/package_protections/typed_public_api.rb
196
197
  homepage: https://github.com/rubyatscale/package_protections
197
198
  licenses: