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.
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: strict
4
+ module PackageProtections
5
+ module ProtectionInterface
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ abstract!
10
+
11
+ requires_ancestor { Kernel }
12
+
13
+ sig do
14
+ params(
15
+ protected_packages: T::Array[ProtectedPackage],
16
+ new_violations: T::Array[PerFileViolation]
17
+ ).returns(T::Array[Offense])
18
+ end
19
+ def get_offenses(protected_packages, new_violations)
20
+ [
21
+ # First we get all offenses for new violations
22
+ *get_offenses_for_new_violations(new_violations),
23
+ # Then we separately look at TODO lists and add violations if there are no issues
24
+ *get_offenses_for_existing_violations(protected_packages)
25
+ ].sort_by(&:message)
26
+ end
27
+
28
+ sig { abstract.params(behavior: ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
29
+ def unmet_preconditions_for_behavior(behavior, package); end
30
+
31
+ sig { params(behavior: ViolationBehavior, package: ParsePackwerk::Package).returns(T::Boolean) }
32
+ def supports_violation_behavior?(behavior, package)
33
+ unmet_preconditions_for_behavior(behavior, package).nil?
34
+ end
35
+
36
+ sig { returns(ViolationBehavior) }
37
+ def default_behavior
38
+ # The default behavior here is that we simply return the `fail_on_new` protection.
39
+ # In some cases, this protection may not actually be supported. For example, `OutgoingPrivacyProtection` raises if the user has `enforce_privacy: false`
40
+ # Error messages should provide enough clarity to either:
41
+ # A) Know which protection to explicitly set to fail_never
42
+ # B) Know how to change conditions to allow protection to be supported (i.e. set enforce dependencies to be true)
43
+ ViolationBehavior::FailOnNew
44
+ end
45
+
46
+ sig do
47
+ abstract.params(
48
+ new_violations: T::Array[PerFileViolation]
49
+ ).returns(T::Array[Offense])
50
+ end
51
+ def get_offenses_for_new_violations(new_violations); end
52
+
53
+ sig do
54
+ abstract.params(
55
+ protected_packages: T::Array[ProtectedPackage]
56
+ ).returns(T::Array[Offense])
57
+ end
58
+ def get_offenses_for_existing_violations(protected_packages); end
59
+
60
+ sig { abstract.returns(String) }
61
+ def identifier; end
62
+
63
+ sig { abstract.returns(String) }
64
+ def humanized_protection_name; end
65
+
66
+ sig { abstract.returns(String) }
67
+ def humanized_protection_description; end
68
+ end
69
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: strict
4
+ module PackageProtections
5
+ module RubocopProtectionInterface
6
+ include ProtectionInterface
7
+ extend T::Sig
8
+ extend T::Helpers
9
+
10
+ abstract!
11
+
12
+ sig do
13
+ abstract
14
+ .params(packages: T::Array[ProtectedPackage])
15
+ .returns(T::Array[CopConfig])
16
+ end
17
+ def cop_configs(packages); end
18
+
19
+ sig do
20
+ params(package: ProtectedPackage).returns(T::Hash[T.untyped, T.untyped])
21
+ end
22
+ def custom_cop_config(package)
23
+ {}
24
+ end
25
+
26
+ class CopConfig < T::Struct
27
+ extend T::Sig
28
+ const :name, String
29
+ const :enabled, T::Boolean, default: true
30
+ const :include_paths, T::Array[String], default: []
31
+ const :exclude_paths, T::Array[String], default: []
32
+
33
+ sig { returns(String) }
34
+ def to_rubocop_yml_compatible_format
35
+ cop_config = { 'Enabled' => enabled }
36
+
37
+ if include_paths.any?
38
+ cop_config['Include'] = include_paths
39
+ end
40
+
41
+ if exclude_paths.any?
42
+ cop_config['Exclude'] = exclude_paths
43
+ end
44
+
45
+ { name => cop_config }.to_yaml.gsub("---\n", '')
46
+ end
47
+ end
48
+
49
+ sig do
50
+ override.params(
51
+ new_violations: T::Array[PerFileViolation]
52
+ ).returns(T::Array[Offense])
53
+ end
54
+ def get_offenses_for_new_violations(new_violations)
55
+ []
56
+ end
57
+
58
+ sig { void }
59
+ def self.bust_rubocop_todo_yml_cache
60
+ @rubocop_todo_yml = nil
61
+ end
62
+
63
+ sig { returns(T.untyped) }
64
+ def self.rubocop_todo_yml
65
+ @rubocop_todo_yml = T.let(@rubocop_todo_yml, T.untyped)
66
+ @rubocop_todo_yml ||= begin
67
+ todo_file = Pathname.new('.rubocop_todo.yml')
68
+ if todo_file.exist?
69
+ YAML.load_file(todo_file)
70
+ else
71
+ {}
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ sig { params(rule: String).returns(T::Set[String]) }
79
+ def exclude_for_rule(rule)
80
+ rule_config = RubocopProtectionInterface.rubocop_todo_yml[rule] || {}
81
+ Set.new(rule_config['Exclude'] || [])
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: strict
4
+ module PackageProtections
5
+ class IncorrectPublicApiUsageError < StandardError; end
6
+
7
+ class ViolationBehavior < T::Enum
8
+ extend T::Sig
9
+
10
+ enums do
11
+ FailOnAny = new('fail_on_any')
12
+ FailOnNew = new('fail_on_new')
13
+ FailNever = new('fail_never')
14
+ end
15
+
16
+ sig { params(value: T.untyped).returns(ViolationBehavior) }
17
+ def self.from_raw_value(value)
18
+ ViolationBehavior.deserialize(value.to_s)
19
+ rescue KeyError
20
+ # Let's not encourage "unknown." That's mostly considered an internal value if nothing is specified.
21
+ acceptable_values = ViolationBehavior.values.map(&:serialize) - ['unknown']
22
+ raise IncorrectPublicApiUsageError.new("The metadata value #{value} is not a valid behavior. Double check your spelling! Acceptable values are #{acceptable_values}. See https://github.com/bigrails/package_protections#readme for more info") # rubocop:disable Style/RaiseArgs
23
+ end
24
+
25
+ sig { returns(T::Boolean) }
26
+ def fail_on_any?
27
+ case self
28
+ when FailOnAny then true
29
+ when FailOnNew then false
30
+ when FailNever then false
31
+ else
32
+ T.absurd(self)
33
+ end
34
+ end
35
+
36
+ sig { returns(T::Boolean) }
37
+ def enabled?
38
+ case self
39
+ when FailOnAny then true
40
+ when FailOnNew then true
41
+ when FailNever then false
42
+ else
43
+ T.absurd(self)
44
+ end
45
+ end
46
+
47
+ sig { returns(T::Boolean) }
48
+ def fail_on_new?
49
+ case self
50
+ when FailOnAny then false
51
+ when FailOnNew then true
52
+ when FailNever then false
53
+ else
54
+ T.absurd(self)
55
+ end
56
+ end
57
+
58
+ sig { returns(T::Boolean) }
59
+ def fail_never?
60
+ case self
61
+ when FailOnAny then false
62
+ when FailOnNew then false
63
+ when FailNever then true
64
+ else
65
+ T.absurd(self)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: strict
4
+ require 'sorbet-runtime'
5
+ require 'open3'
6
+ require 'set'
7
+ require 'parse_packwerk'
8
+ require 'rubocop'
9
+ require 'rubocop-sorbet'
10
+
11
+ #
12
+ # Welcome to PackageProtections!
13
+ # See https://github.com/bigrails/package_protections#readme for more info
14
+ #
15
+ # This file is a reference for the available API to `package_protections`, but all implementation details are private
16
+ # (which is why we delegate to `Private` for the actual implementation).
17
+ #
18
+ module PackageProtections
19
+ extend T::Sig
20
+
21
+ PROTECTIONS_TODO_YML = 'protections_todo.yml'
22
+ EXPECTED_PACK_DIRECTORIES = T.let(%w[packs packages gems components], T::Array[String])
23
+
24
+ # A protection identifier is just a string that identifies the name of the protection within a `package.yml`
25
+ Identifier = T.type_alias { String }
26
+
27
+ # This is currently the only handled exception that `PackageProtections` will throw.
28
+ class IncorrectPublicApiUsageError < StandardError; end
29
+
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
+
41
+ #
42
+ # These are all of the concrete implementations of the `ProtectionInterface`.
43
+ # This list is added to when a new protection is created, but in the future,
44
+ # this list may be injected by the client.
45
+ #
46
+ sig { returns(T::Array[ProtectionInterface]) }
47
+ def self.all
48
+ [
49
+ Private::OutgoingDependencyProtection.new,
50
+ Private::IncomingPrivacyProtection.new,
51
+ Private::TypedApiProtection.new,
52
+ Private::MultipleNamespacesProtection.new,
53
+ Private::VisibilityProtection.new
54
+ ]
55
+ end
56
+
57
+ #
58
+ # This is a fast way to get a protection given an identifier
59
+ #
60
+ sig { params(identifier: Identifier).returns(ProtectionInterface) }
61
+ def self.with_identifier(identifier)
62
+ @map ||= T.let(@map, T.nilable(T::Hash[Identifier, ProtectionInterface]))
63
+ @map ||= all.map { |protection| [protection.identifier, protection] }.to_h
64
+ @map.fetch(identifier)
65
+ end
66
+
67
+ #
68
+ # This returns an array of a `Offense` which is how we represent the outcome of attempting to protect one or more packages,
69
+ # each of which is configured with different violation behaviors for each protection.
70
+ #
71
+ sig do
72
+ params(
73
+ packages: T::Array[ParsePackwerk::Package],
74
+ new_violations: T::Array[PerFileViolation]
75
+ ).returns(T::Array[Offense])
76
+ end
77
+ def self.get_offenses(packages:, new_violations:)
78
+ Private.get_offenses(
79
+ packages: packages,
80
+ new_violations: new_violations
81
+ ).compact
82
+ end
83
+
84
+ #
85
+ # PackageProtections.set_defaults! sets any unset protections to their default enforcement
86
+ #
87
+ sig do
88
+ params(
89
+ packages: T::Array[ParsePackwerk::Package],
90
+ protection_identifiers: T::Array[String],
91
+ verbose: T::Boolean
92
+ ).void
93
+ end
94
+ def self.set_defaults!(packages, protection_identifiers: PackageProtections.all.map(&:identifier), verbose: true)
95
+ Private.set_defaults!(packages, protection_identifiers: protection_identifiers, verbose: verbose)
96
+ end
97
+
98
+ # Why do we use Bundler.root here?
99
+ # The reason is that this function is evaluated in `.client_rubocop.yml`, so when rubocop evaluates the
100
+ # YML and parses the ERB, the working directory is inside this gem. We need to make sure it's at the root of the
101
+ # application so that we can find all of the packwerk packages.
102
+ # We use Bundler.root to get the root because:
103
+ # A) We expect it to be reliable
104
+ # B) We expect the client to be running bundle exec rubocop, which is typically done at the root, and also means
105
+ # the client already has this dependency.
106
+ sig { params(root_pathname: Pathname).returns(String) }
107
+ def self.rubocop_yml(root_pathname: Bundler.root)
108
+ Private.rubocop_yml(root_pathname: root_pathname)
109
+ end
110
+
111
+ #
112
+ # Do not use this method -- it's meant to be used by Rubocop cops to get directory-specific
113
+ # parameters without needing to have directory-specific .rubocop.yml files.
114
+ #
115
+ sig { params(identifier: Identifier).returns(T::Hash[T.untyped, T.untyped]) }
116
+ def self.private_cop_config(identifier)
117
+ Private.private_cop_config(identifier)
118
+ end
119
+
120
+ sig do
121
+ params(
122
+ package_names: T::Array[String],
123
+ all_packages: T::Array[ParsePackwerk::Package]
124
+ ).returns(T::Array[ParsePackwerk::Package])
125
+ end
126
+ def self.packages_for_names(package_names, all_packages)
127
+ Private.packages_for_names(package_names, all_packages)
128
+ end
129
+
130
+ sig { void }
131
+ def self.bust_cache!
132
+ Private.bust_cache!
133
+ RubocopProtectionInterface.bust_rubocop_todo_yml_cache
134
+ end
135
+ end
@@ -0,0 +1,108 @@
1
+ # typed: ignore
2
+
3
+ # For String#camelize
4
+ require 'active_support/core_ext/string/inflections'
5
+
6
+ module RuboCop
7
+ module Cop
8
+ module PackageProtections
9
+ class NamespacedUnderPackageName < Base
10
+ include RangeHelp
11
+
12
+ def on_new_investigation
13
+ absolute_filepath = Pathname.new(processed_source.file_path)
14
+ relative_filepath = absolute_filepath.relative_path_from(Pathname.pwd)
15
+ relative_filename = relative_filepath.to_s
16
+
17
+ # This cop only works for files in `app`
18
+ return if !relative_filename.include?('app/')
19
+
20
+ match = relative_filename.match(%r{((#{::PackageProtections::EXPECTED_PACK_DIRECTORIES.join("|")})/.*?)/})
21
+ package_name = match && match[1]
22
+
23
+ return if package_name.nil?
24
+
25
+ return if relative_filepath.extname != '.rb'
26
+
27
+ # Zeitwerk establishes a standard convention by which namespaces are defined.
28
+ # The package protections namespace checker is coupled to a specific assumption about how auto-loading works.
29
+ #
30
+ # Namely, we expect the following autoload paths: `packs/**/app/**/`
31
+ # Examples:
32
+ # 1) `packs/package_1/app/public/package_1/my_constant.rb` produces constant `Package1::MyConstant`
33
+ # 2) `packs/package_1/app/services/package_1/my_service.rb` produces constant `Package1::MyService`
34
+ # 3) `packs/package_1/app/services/package_1.rb` produces constant `Package1`
35
+ # 4) `packs/package_1/app/public/package_1.rb` produces constant `Package1`
36
+ #
37
+ # Described another way, we expect any part of the directory labeled NAMESPACE to establish a portion of the fully qualified runtime constant:
38
+ # `packs/**/app/**/NAMESPACE1/NAMESPACE2/[etc]`
39
+ #
40
+ # Therefore, for our implementation, we substitute out the non-namespace producing portions of the filename to count the number of namespaces.
41
+ # Note this will *not work* properly in applications that have different assumptions about autoloading.
42
+ package_last_name = package_name.split('/').last
43
+ path_without_package_base = relative_filename.gsub(%r{#{package_name}/app/}, '')
44
+ if path_without_package_base.include?('concerns')
45
+ autoload_folder_name = path_without_package_base.split('/').first(2).join('/')
46
+ else
47
+ autoload_folder_name = path_without_package_base.split('/').first
48
+ end
49
+
50
+ remaining_file_path = path_without_package_base.gsub(%r{\A#{autoload_folder_name}/}, '')
51
+ actual_namespace = get_actual_namespace(remaining_file_path, relative_filepath, package_name)
52
+ allowed_namespaces = get_allowed_namespaces(package_name)
53
+ if allowed_namespaces.include?(actual_namespace)
54
+ # No problem!
55
+ elsif allowed_namespaces.count == 1
56
+ single_allowed_namespace = allowed_namespaces.first
57
+ if relative_filepath.to_s.include?('app/')
58
+ app_or_lib = 'app'
59
+ elsif relative_filepath.to_s.include?('lib/')
60
+ app_or_lib = 'lib'
61
+ end
62
+
63
+ absolute_desired_path = root_pathname.join(package_name, app_or_lib, T.must(autoload_folder_name), single_allowed_namespace.underscore, remaining_file_path)
64
+
65
+ relative_desired_path = absolute_desired_path.relative_path_from(root_pathname)
66
+
67
+ add_offense(
68
+ source_range(processed_source.buffer, 1, 0),
69
+ message: format(
70
+ '`%<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.',
71
+ package_name: package_name,
72
+ expected_namespace: package_last_name.camelize,
73
+ expected_path: relative_desired_path
74
+ )
75
+ )
76
+ else
77
+ add_offense(
78
+ source_range(processed_source.buffer, 1, 0),
79
+ message: format(
80
+ '`%<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.',
81
+ package_name: package_name,
82
+ package_yml: "#{package_name}/package.yml"
83
+ )
84
+ )
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def get_allowed_namespaces(package_name)
91
+ cop_config = ::PackageProtections.private_cop_config('prevent_this_package_from_creating_other_namespaces')
92
+ allowed_global_namespaces = cop_config[package_name]['GlobalNamespaces'] || [package_name.split('/').last.camelize]
93
+ Set.new(allowed_global_namespaces)
94
+ end
95
+
96
+ def get_actual_namespace(remaining_file_path, relative_filepath, package_name)
97
+ # If the remaining file path is a ruby file (not a directory), then it establishes a global namespace
98
+ # Otherwise, per Zeitwerk's conventions as listed above, its a directory that establishes another global namespace
99
+ remaining_file_path.split('/').first.gsub('.rb', '').camelize
100
+ end
101
+
102
+ def root_pathname
103
+ Pathname.pwd
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
metadata ADDED
@@ -0,0 +1,220 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: package_protections
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.64.0
5
+ platform: ruby
6
+ authors:
7
+ - Gusto Engineers
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-05-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: parse_packwerk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-sorbet
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sorbet-runtime
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: sorbet
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: tapioca
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description: Package protections for Rails apps
168
+ email:
169
+ - stephan.hagemann@gusto.com
170
+ executables: []
171
+ extensions: []
172
+ extra_rdoc_files: []
173
+ files:
174
+ - README.md
175
+ - config/default.yml
176
+ - lib/package_protections.rb
177
+ - lib/package_protections/offense.rb
178
+ - lib/package_protections/per_file_violation.rb
179
+ - lib/package_protections/private.rb
180
+ - lib/package_protections/private/colorized_string.rb
181
+ - lib/package_protections/private/incoming_privacy_protection.rb
182
+ - lib/package_protections/private/metadata_modifiers.rb
183
+ - lib/package_protections/private/multiple_namespaces_protection.rb
184
+ - lib/package_protections/private/outgoing_dependency_protection.rb
185
+ - lib/package_protections/private/output.rb
186
+ - lib/package_protections/private/typed_api_protection.rb
187
+ - lib/package_protections/private/visibility_protection.rb
188
+ - lib/package_protections/protected_package.rb
189
+ - lib/package_protections/protection_interface.rb
190
+ - lib/package_protections/rubocop_protection_interface.rb
191
+ - lib/package_protections/violation_behavior.rb
192
+ - lib/rubocop/cop/package_protections/namespaced_under_package_name.rb
193
+ homepage: https://github.com/bigrails/package_protections
194
+ licenses:
195
+ - MIT
196
+ metadata:
197
+ homepage_uri: https://github.com/bigrails/package_protections
198
+ source_code_uri: https://github.com/bigrails/parse_packwerk
199
+ changelog_uri: https://github.com/bigrails/parse_packwerk/releases
200
+ allowed_push_host: https://rubygems.org
201
+ post_install_message:
202
+ rdoc_options: []
203
+ require_paths:
204
+ - lib
205
+ required_ruby_version: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - ">="
208
+ - !ruby/object:Gem::Version
209
+ version: 2.5.0
210
+ required_rubygems_version: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ requirements: []
216
+ rubygems_version: 3.3.7
217
+ signing_key:
218
+ specification_version: 4
219
+ summary: Package protections for Rails apps
220
+ test_files: []