package_protections 1.4.0 → 2.1.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: 7b99c93d81087956b947c89fbede35844a2c973f20e2bee3c8f474383ff99a88
4
- data.tar.gz: 64ad87cf10597e5fceeefa315dd3e665f7a684ac36db30c206012c15c2a0f3f2
3
+ metadata.gz: 7471704e495c4ffe4e849be40a9fad64eed08e3980e9d630300eeae5c8e0d006
4
+ data.tar.gz: dbb34a83f3b4f7ed1a3327d36ac2dd5191d17bba0206ebfa06344d056c35b43a
5
5
  SHA512:
6
- metadata.gz: 9a5a70b90e8ecf6a00e8b22db867e6cec3a83ba34a8fa6f18b45449c96f3eb9fd248c80f9a047d9462e2976d233c8f6b29957837feef224a94d8ce93e008a55d
7
- data.tar.gz: 500e49a77a3ee17aa2c4cb05edf2806e37010b5168ebb9cf1a13e1addbb5db5eb11c0caaa82f4a10451dbf96d304c7e010400b19ba7240b920b29a4c93780a90
6
+ metadata.gz: 77012017f0440763480a9f02ab289254870b9d937ff72cfa8f665331b04195cf01e1724562796d1a74414d38c855fa95710879d6efe105367792a3cf7b1bf60b
7
+ data.tar.gz: 48cb93ff9ec6b2b4785587c795fb6571ff55ade4de471c93b38fa3244113b4d4b0ecd6f431d335754ad4d5c6e7d23e3ec6701c71d0fca1328848dcf5a1f27836
@@ -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,13 +22,21 @@ 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]) }
27
37
  def default_protections
38
+ require 'rubocop/cop/package_protections'
39
+
28
40
  [
29
41
  Private::OutgoingDependencyProtection.new,
30
42
  Private::IncomingPrivacyProtection.new,
@@ -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],
@@ -122,7 +116,7 @@ module PackageProtections
122
116
  def self.bust_cache!
123
117
  @protected_packages_indexed_by_name = nil
124
118
  @private_cop_config = nil
125
- config.bust_cache!
119
+ PackageProtections.config.bust_cache!
126
120
  end
127
121
 
128
122
  sig { params(identifier: Identifier).returns(T::Hash[T.untyped, T.untyped]) }
@@ -1,20 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # typed: strict
4
+
4
5
  require 'sorbet-runtime'
6
+
5
7
  require 'open3'
6
8
  require 'set'
7
9
  require 'parse_packwerk'
8
- require 'rubocop'
9
- require 'rubocop-sorbet'
10
10
 
11
- #
11
+ require 'zeitwerk'
12
+ loader = T.unsafe(Zeitwerk::Loader).for_gem
13
+ loader.ignore("#{__dir__}/rubocop")
14
+ loader.setup
15
+
12
16
  # Welcome to PackageProtections!
13
17
  # See https://github.com/rubyatscale/package_protections#readme for more info
14
18
  #
15
19
  # This file is a reference for the available API to `package_protections`, but all implementation details are private
16
20
  # (which is why we delegate to `Private` for the actual implementation).
17
- #
18
21
  module PackageProtections
19
22
  extend T::Sig
20
23
 
@@ -27,30 +30,24 @@ module PackageProtections
27
30
  # This is currently the only handled exception that `PackageProtections` will throw.
28
31
  class IncorrectPublicApiUsageError < StandardError; end
29
32
 
30
- require 'package_protections/offense'
31
- require 'package_protections/violation_behavior'
32
- require 'package_protections/protected_package'
33
- require 'package_protections/per_file_violation'
34
- require 'package_protections/protection_interface'
35
- require 'package_protections/rubocop_protection_interface'
36
- require 'package_protections/private'
37
-
38
- # Implementation of rubocop-based protections
39
- require 'rubocop/cop/package_protections/namespaced_under_package_name'
40
- require 'rubocop/cop/package_protections/typed_public_api'
41
-
42
33
  class << self
43
34
  extend T::Sig
44
35
 
45
36
  sig { params(blk: T.proc.params(arg0: Private::Configuration).void).void }
46
37
  def configure(&blk)
47
- yield(Private.config)
38
+ yield(PackageProtections.config)
48
39
  end
49
40
  end
50
41
 
51
42
  sig { returns(T::Array[ProtectionInterface]) }
52
43
  def self.all
53
- Private.config.protections
44
+ config.protections
45
+ end
46
+
47
+ sig { returns(Private::Configuration) }
48
+ def self.config
49
+ @config = T.let(@config, T.nilable(Private::Configuration))
50
+ @config ||= Private::Configuration.new
54
51
  end
55
52
 
56
53
  #
@@ -122,3 +119,7 @@ module PackageProtections
122
119
  RubocopProtectionInterface.bust_rubocop_todo_yml_cache
123
120
  end
124
121
  end
122
+
123
+ if defined?(Rubocop)
124
+ require 'rubocop/cop/package_protections'
125
+ end
@@ -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,92 +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
- package_name = ParsePackwerk.package_from_path(relative_filename)&.name
24
- return if package_name.nil?
25
-
26
- return if relative_filepath.extname != '.rb'
27
-
28
- # Zeitwerk establishes a standard convention by which namespaces are defined.
29
- # The package protections namespace checker is coupled to a specific assumption about how auto-loading works.
30
- #
31
- # Namely, we expect the following autoload paths: `packs/**/app/**/`
32
- # Examples:
33
- # 1) `packs/package_1/app/public/package_1/my_constant.rb` produces constant `Package1::MyConstant`
34
- # 2) `packs/package_1/app/services/package_1/my_service.rb` produces constant `Package1::MyService`
35
- # 3) `packs/package_1/app/services/package_1.rb` produces constant `Package1`
36
- # 4) `packs/package_1/app/public/package_1.rb` produces constant `Package1`
37
- #
38
- # Described another way, we expect any part of the directory labeled NAMESPACE to establish a portion of the fully qualified runtime constant:
39
- # `packs/**/app/**/NAMESPACE1/NAMESPACE2/[etc]`
40
- #
41
- # Therefore, for our implementation, we substitute out the non-namespace producing portions of the filename to count the number of namespaces.
42
- # Note this will *not work* properly in applications that have different assumptions about autoloading.
43
- package_last_name = T.must(package_name.split('/').last)
44
- path_without_package_base = relative_filename.gsub(%r{#{package_name}/app/}, '')
45
- if path_without_package_base.include?('concerns')
46
- autoload_folder_name = path_without_package_base.split('/').first(2).join('/')
47
- else
48
- autoload_folder_name = path_without_package_base.split('/').first
49
- end
28
+ # This cop only works for files ruby files in `app`
29
+ return if !relative_filename.include?('app/') || relative_filepath.extname != '.rb'
50
30
 
51
- remaining_file_path = path_without_package_base.gsub(%r{\A#{autoload_folder_name}/}, '')
52
- actual_namespace = get_actual_namespace(remaining_file_path, relative_filepath, package_name)
53
- allowed_namespaces = get_allowed_namespaces(package_name)
54
- if allowed_namespaces.include?(actual_namespace)
55
- # No problem!
56
- elsif allowed_namespaces.count == 1
57
- single_allowed_namespace = allowed_namespaces.first
58
- if relative_filepath.to_s.include?('app/')
59
- app_or_lib = 'app'
60
- elsif relative_filepath.to_s.include?('lib/')
61
- app_or_lib = 'lib'
62
- 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?
63
34
 
64
- 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?
65
37
 
66
- 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
+ ])
67
42
 
68
- add_offense(
69
- source_range(processed_source.buffer, 1, 0),
70
- message: format(
71
- '`%<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.',
72
- package_name: package_name,
73
- expected_namespace: package_last_name.camelize,
74
- expected_path: relative_desired_path
75
- )
76
- )
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!
77
48
  else
78
- add_offense(
79
- source_range(processed_source.buffer, 1, 0),
80
- message: format(
81
- '`%<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.',
82
- package_name: package_name,
83
- 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
+ )
84
63
  )
85
- )
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
77
+ end
78
+ end
79
+
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
86
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
+ ]
87
104
  end
88
105
 
89
- IDENTIFIER = 'prevent_this_package_from_creating_other_namespaces'.freeze
106
+ IDENTIFIER = T.let('prevent_this_package_from_creating_other_namespaces'.freeze, String)
90
107
 
91
108
  sig { override.returns(String) }
92
109
  def identifier
@@ -123,15 +140,6 @@ module RuboCop
123
140
  ]
124
141
  end
125
142
 
126
- sig do
127
- params(package: ::PackageProtections::ProtectedPackage).returns(T::Hash[T.untyped, T.untyped])
128
- end
129
- def custom_cop_config(package)
130
- {
131
- 'GlobalNamespaces' => package.metadata['global_namespaces']
132
- }
133
- end
134
-
135
143
  sig do
136
144
  override.params(file: String).returns(String)
137
145
  end
@@ -161,22 +169,29 @@ module RuboCop
161
169
  MESSAGE
162
170
  end
163
171
 
164
- private
165
-
166
- def get_allowed_namespaces(package_name)
167
- cop_config = ::PackageProtections.private_cop_config('prevent_this_package_from_creating_other_namespaces')
168
- allowed_global_namespaces = cop_config[package_name]['GlobalNamespaces'] || [package_name.split('/').last.camelize]
169
- 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
170
178
  end
171
179
 
172
- def get_actual_namespace(remaining_file_path, relative_filepath, package_name)
173
- # If the remaining file path is a ruby file (not a directory), then it establishes a global namespace
174
- # Otherwise, per Zeitwerk's conventions as listed above, its a directory that establishes another global namespace
175
- remaining_file_path.split('/').first.gsub('.rb', '').camelize
176
- 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
177
192
 
178
- def root_pathname
179
- Pathname.pwd
193
+ namespaces_to_packs
194
+ end
180
195
  end
181
196
  end
182
197
  end
@@ -0,0 +1,13 @@
1
+ require 'rubocop'
2
+ require 'rubocop-sorbet'
3
+
4
+ module RuboCop
5
+ module Cop
6
+ module PackageProtections
7
+ autoload :NamespacedUnderPackageName, 'rubocop/cop/package_protections/namespaced_under_package_name'
8
+ autoload :TypedPublicApi, 'rubocop/cop/package_protections/typed_public_api'
9
+ end
10
+ end
11
+ end
12
+
13
+ # Implementation of rubocop-based protections
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.4.0
4
+ version: 2.1.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-08-13 00:00:00.000000000 Z
11
+ date: 2022-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: zeitwerk
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rake
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -191,7 +205,9 @@ files:
191
205
  - lib/package_protections/rspec/support.rb
192
206
  - lib/package_protections/rubocop_protection_interface.rb
193
207
  - lib/package_protections/violation_behavior.rb
208
+ - lib/rubocop/cop/package_protections.rb
194
209
  - lib/rubocop/cop/package_protections/namespaced_under_package_name.rb
210
+ - lib/rubocop/cop/package_protections/namespaced_under_package_name/desired_zeitwerk_api.rb
195
211
  - lib/rubocop/cop/package_protections/typed_public_api.rb
196
212
  homepage: https://github.com/rubyatscale/package_protections
197
213
  licenses: