package_protections 0.64.0

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