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,117 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PackageProtections
5
+ module Private
6
+ class OutgoingDependencyProtection
7
+ extend T::Sig
8
+
9
+ include ProtectionInterface
10
+
11
+ IDENTIFIER = 'prevent_this_package_from_violating_its_stated_dependencies'
12
+
13
+ sig { override.returns(String) }
14
+ def identifier
15
+ IDENTIFIER
16
+ end
17
+
18
+ sig { override.params(behavior: ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
19
+ def unmet_preconditions_for_behavior(behavior, package)
20
+ if behavior.enabled? && !package.enforces_dependencies?
21
+ "Package #{package.name} must have `enforce_dependencies: true` to use this protection"
22
+ elsif !behavior.enabled? && package.enforces_dependencies?
23
+ "Package #{package.name} must have `enforce_dependencies: false` to turn this protection off"
24
+ else
25
+ nil
26
+ end
27
+ end
28
+
29
+ sig { override.returns(String) }
30
+ def humanized_protection_name
31
+ 'Dependency Violations'
32
+ end
33
+
34
+ sig { override.returns(String) }
35
+ def humanized_protection_description
36
+ <<~MESSAGE
37
+ To resolve these violations, should you add a dependency in the client's `package.yml`?
38
+ Is the code referencing the constant, and the referenced constant, in the right packages?
39
+ See https://go/packwerk_cheatsheet_dependency for more info.
40
+ MESSAGE
41
+ end
42
+
43
+ sig do
44
+ override.params(
45
+ new_violations: T::Array[PerFileViolation]
46
+ ).returns(T::Array[Offense])
47
+ end
48
+ def get_offenses_for_new_violations(new_violations)
49
+ new_violations.select(&:dependency?).flat_map do |per_file_violation|
50
+ reference_source_package = Private.get_package_with_name(per_file_violation.reference_source_package.name)
51
+ violation_behavior = reference_source_package.violation_behavior_for(identifier)
52
+
53
+ case violation_behavior
54
+ when ViolationBehavior::FailNever
55
+ next []
56
+ when ViolationBehavior::FailOnNew
57
+ message = message_for_fail_on_new(per_file_violation)
58
+ when ViolationBehavior::FailOnAny
59
+ message = message_for_fail_on_any(per_file_violation)
60
+ else
61
+ T.absurd(violation_behavior)
62
+ end
63
+
64
+ Offense.new(
65
+ file: per_file_violation.filepath,
66
+ message: message,
67
+ violation_type: identifier,
68
+ package: reference_source_package.original_package
69
+ )
70
+ end
71
+ end
72
+
73
+ sig do
74
+ override.params(
75
+ protected_packages: T::Array[ProtectedPackage]
76
+ ).returns(T::Array[Offense])
77
+ end
78
+ def get_offenses_for_existing_violations(protected_packages)
79
+ protected_packages.flat_map do |protected_package|
80
+ violation_behavior = protected_package.violation_behavior_for(identifier)
81
+
82
+ case violation_behavior
83
+ when ViolationBehavior::FailNever, ViolationBehavior::FailOnNew
84
+ []
85
+ when ViolationBehavior::FailOnAny
86
+ listed_violations = protected_package.violations.select(&:dependency?).flat_map do |violation|
87
+ PerFileViolation.from(violation, protected_package.original_package)
88
+ end
89
+
90
+ listed_violations.flat_map do |per_file_violation|
91
+ Offense.new(
92
+ file: per_file_violation.filepath,
93
+ message: message_for_fail_on_any(per_file_violation),
94
+ violation_type: identifier,
95
+ package: protected_package.original_package
96
+ )
97
+ end
98
+ else
99
+ T.absurd(violation_behavior)
100
+ end
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ sig { params(per_file_violation: PerFileViolation).returns(String) }
107
+ def message_for_fail_on_any(per_file_violation)
108
+ "#{message_for_fail_on_new(per_file_violation)} (`#{per_file_violation.reference_source_package.name}` set to `fail_on_any`)"
109
+ end
110
+
111
+ sig { params(per_file_violation: PerFileViolation).returns(String) }
112
+ def message_for_fail_on_new(per_file_violation)
113
+ "`#{per_file_violation.filepath}` depends on `#{per_file_violation.class_name}` from `#{per_file_violation.constant_source_package}`"
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: strict
4
+ module PackageProtections
5
+ module Private
6
+ class Output
7
+ extend T::Sig
8
+
9
+ sig { params(str: String).void }
10
+ def self.p(str)
11
+ puts str
12
+ end
13
+
14
+ sig { params(str: ColorizedString, colorized: T::Boolean).void }
15
+ def self.p_colorized(str, colorized:)
16
+ if colorized
17
+ p str.colorized_to_s
18
+ else
19
+ p str.to_s
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,106 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PackageProtections
5
+ module Private
6
+ class TypedApiProtection
7
+ extend T::Sig
8
+
9
+ include ProtectionInterface
10
+ include RubocopProtectionInterface
11
+
12
+ IDENTIFIER = 'prevent_this_package_from_exposing_an_untyped_api'
13
+ COP_NAME = 'Sorbet/StrictSigil'
14
+
15
+ sig { override.returns(String) }
16
+ def identifier
17
+ IDENTIFIER
18
+ end
19
+
20
+ sig { override.params(behavior: ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
21
+ def unmet_preconditions_for_behavior(behavior, package)
22
+ # We might decide that we should check that `package.enforces_privacy?` is true here too, since that signifies the app has decided they want
23
+ # a public api in `app/public`. For now, we say there are no preconditions, because the user can still make `app/public` even if they are not yet
24
+ # ready to enforce privacy, and they might want to enforce a typed API.
25
+ nil
26
+ end
27
+
28
+ sig do
29
+ override
30
+ .params(packages: T::Array[ProtectedPackage])
31
+ .returns(T::Array[CopConfig])
32
+ end
33
+ def cop_configs(packages)
34
+ include_paths = T.let([], T::Array[String])
35
+ packages.each do |p|
36
+ if p.violation_behavior_for(identifier).enabled?
37
+ directory = p.original_package.directory
38
+ include_paths << directory.join('app', 'public', '**', '*').to_s
39
+ end
40
+ end
41
+
42
+ [
43
+ CopConfig.new(
44
+ name: COP_NAME,
45
+ enabled: include_paths.any?,
46
+ include_paths: include_paths
47
+ )
48
+ ]
49
+ end
50
+
51
+ sig do
52
+ override.params(
53
+ protected_packages: T::Array[ProtectedPackage]
54
+ ).returns(T::Array[Offense])
55
+ end
56
+ def get_offenses_for_existing_violations(protected_packages)
57
+ exclude_list = exclude_for_rule(COP_NAME)
58
+
59
+ offenses = T.let([], T::Array[Offense])
60
+ protected_packages.flat_map do |protected_package|
61
+ violation_behavior = protected_package.violation_behavior_for(identifier)
62
+
63
+ case violation_behavior
64
+ when ViolationBehavior::FailNever, ViolationBehavior::FailOnNew
65
+ next
66
+ when ViolationBehavior::FailOnAny
67
+ # Continue
68
+ else
69
+ T.absurd(violation_behavior)
70
+ end
71
+
72
+ protected_package.original_package.directory.glob('app/public/**/**/*.*').each do |relative_path_to_file|
73
+ next unless exclude_list.include?(relative_path_to_file.to_s)
74
+
75
+ file = relative_path_to_file.to_s
76
+ offenses << Offense.new(
77
+ file: file,
78
+ message: "#{file} should be `typed: strict`",
79
+ violation_type: identifier,
80
+ package: protected_package.original_package
81
+ )
82
+ end
83
+ end
84
+
85
+ offenses
86
+ end
87
+
88
+ sig { override.returns(String) }
89
+ def humanized_protection_name
90
+ 'Typed API Violations'
91
+ end
92
+
93
+ sig { override.returns(String) }
94
+ def humanized_protection_description
95
+ <<~MESSAGE
96
+ These files cannot have ANY Ruby files in the public API that are not typed strict or higher.
97
+ This is failing because these files are in `.rubocop_todo.yml` under `#{COP_NAME}`.
98
+ If you want to be able to ignore these files, you'll need to open the file's package's `package.yml` file and
99
+ change `#{IDENTIFIER}` to `#{ViolationBehavior::FailOnNew.serialize}`
100
+
101
+ See https://go/packwerk_cheatsheet_typed_api for more info.
102
+ MESSAGE
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,186 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PackageProtections
5
+ module Private
6
+ class VisibilityProtection
7
+ extend T::Sig
8
+
9
+ include ProtectionInterface
10
+
11
+ IDENTIFIER = 'prevent_other_packages_from_using_this_package_without_explicit_visibility'
12
+
13
+ sig { override.returns(String) }
14
+ def identifier
15
+ IDENTIFIER
16
+ end
17
+
18
+ sig { override.params(behavior: ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
19
+ def unmet_preconditions_for_behavior(behavior, package)
20
+ # This protection relies on seeing privacy violations in other packages.
21
+ # We also require that the other package enforces dependencies, as otherwise, if the client is using public API, it won't show up
22
+ # as a privacy OR dependency violation. For now, we don't have the system structure to support that requirement.
23
+ if behavior.enabled? && !package.enforces_privacy?
24
+ "Package #{package.name} must have `enforce_privacy: true` to use this protection"
25
+ elsif !behavior.enabled? && !package.metadata['visible_to'].nil?
26
+ "Invalid configuration for package `#{package.name}`. `#{identifier}` must be turned on to use `visible_to` configuration."
27
+ else
28
+ nil
29
+ end
30
+ end
31
+
32
+ #
33
+ # By default, this protection does not show up when creating a new package, and its default behavior is FailNever
34
+ # A package that uses this protection is not considered strictly better -- in general, we want to design packages that
35
+ # are consumable by all packages. Therefore, a package that is consumable by all packages is the happy path.
36
+ #
37
+ # If a user wants to turn on package visibility, they must do it explicitly.
38
+ #
39
+ sig { returns(ViolationBehavior) }
40
+ def default_behavior
41
+ ViolationBehavior::FailNever
42
+ end
43
+
44
+ sig { override.returns(String) }
45
+ def humanized_protection_name
46
+ 'Visibility Violations'
47
+ end
48
+
49
+ sig { override.returns(String) }
50
+ def humanized_protection_description
51
+ <<~MESSAGE
52
+ These files are using a constant from a package that restricts its usage through the `visible_to` flag in its `package.yml`
53
+ To resolve these violations, work with the team who owns the package you are trying to use and to figure out the
54
+ preferred public API for the behavior you want.
55
+
56
+ See https://go/packwerk_cheatsheet_visibility for more info.
57
+ MESSAGE
58
+ end
59
+
60
+ sig do
61
+ override.params(
62
+ new_violations: T::Array[PerFileViolation]
63
+ ).returns(T::Array[Offense])
64
+ end
65
+ def get_offenses_for_new_violations(new_violations)
66
+ new_violations.flat_map do |per_file_violation|
67
+ depended_on_package = Private.get_package_with_name(per_file_violation.constant_source_package)
68
+ violation_behavior = depended_on_package.violation_behavior_for(identifier)
69
+ visible_to = depended_on_package.visible_to
70
+ next [] if visible_to.include?(per_file_violation.reference_source_package.name)
71
+
72
+ case violation_behavior
73
+ when ViolationBehavior::FailNever
74
+ next []
75
+ when ViolationBehavior::FailOnNew
76
+ message = message_for_fail_on_new(per_file_violation)
77
+ when ViolationBehavior::FailOnAny
78
+ message = message_for_fail_on_any(per_file_violation)
79
+ else
80
+ T.absurd(violation_behavior)
81
+ end
82
+
83
+ Offense.new(
84
+ file: per_file_violation.filepath,
85
+ message: message,
86
+ violation_type: identifier,
87
+ package: depended_on_package.original_package
88
+ )
89
+ end
90
+ end
91
+
92
+ sig do
93
+ override.params(
94
+ protected_packages: T::Array[ProtectedPackage]
95
+ ).returns(T::Array[Offense])
96
+ end
97
+ def get_offenses_for_existing_violations(protected_packages)
98
+ all_offenses = T.let([], T::Array[Offense])
99
+
100
+ all_listed_violations = protected_packages.flat_map do |protected_package|
101
+ protected_package.violations.flat_map do |violation|
102
+ PerFileViolation.from(violation, protected_package.original_package)
103
+ end
104
+ end
105
+
106
+ #
107
+ # First we get offenses related to violations between packages, looking at all dependency and privacy
108
+ # violations between packages.
109
+ #
110
+
111
+ # We only care about looking at each edge once. Since an edge can show up twice if its both a privacy and dependency violation,
112
+ # we only look at each combination of class from package (A) that's referenced in package (B)
113
+ unique_per_file_violations = all_listed_violations.uniq do |per_file_violation|
114
+ [per_file_violation.reference_source_package.name, per_file_violation.constant_source_package, per_file_violation.class_name]
115
+ end
116
+
117
+ all_offenses += unique_per_file_violations.flat_map do |per_file_violation|
118
+ depended_on_package = Private.get_package_with_name(per_file_violation.constant_source_package)
119
+ violation_behavior = depended_on_package.violation_behavior_for(identifier)
120
+ visible_to = depended_on_package.visible_to
121
+ next [] if visible_to.include?(per_file_violation.reference_source_package.name)
122
+
123
+ case violation_behavior
124
+ when ViolationBehavior::FailNever, ViolationBehavior::FailOnNew
125
+ next []
126
+ when ViolationBehavior::FailOnAny
127
+ message = message_for_fail_on_any(per_file_violation)
128
+ else
129
+ T.absurd(violation_behavior)
130
+ end
131
+
132
+ Offense.new(
133
+ file: per_file_violation.filepath,
134
+ message: message,
135
+ violation_type: identifier,
136
+ package: depended_on_package.original_package
137
+ )
138
+ end
139
+
140
+ #
141
+ # Then we get offenses from stated dependencies
142
+ #
143
+ all_offenses += protected_packages.flat_map do |protected_package|
144
+ protected_package.dependencies.flat_map do |package_dependency_name|
145
+ depended_on_package = Private.get_package_with_name(package_dependency_name)
146
+ visible_to = depended_on_package.visible_to
147
+ next [] if visible_to.include?(protected_package.name)
148
+
149
+ violation_behavior = depended_on_package.violation_behavior_for(identifier)
150
+
151
+ case violation_behavior
152
+ when ViolationBehavior::FailNever
153
+ next []
154
+ when ViolationBehavior::FailOnAny, ViolationBehavior::FailOnNew
155
+ # continue
156
+ else
157
+ T.absurd(violation_behavior)
158
+ end
159
+
160
+ message = "`#{protected_package.name}` cannot state a dependency on `#{depended_on_package.name}`, as it violates package visibility in `#{depended_on_package.yml}`"
161
+ Offense.new(
162
+ file: protected_package.yml.to_s,
163
+ message: message,
164
+ violation_type: identifier,
165
+ package: depended_on_package.original_package
166
+ )
167
+ end
168
+ end
169
+
170
+ all_offenses
171
+ end
172
+
173
+ private
174
+
175
+ sig { params(per_file_violation: PerFileViolation).returns(String) }
176
+ def message_for_fail_on_any(per_file_violation)
177
+ "#{message_for_fail_on_new(per_file_violation)} (`#{per_file_violation.constant_source_package}` set to `fail_on_any`)"
178
+ end
179
+
180
+ sig { params(per_file_violation: PerFileViolation).returns(String) }
181
+ def message_for_fail_on_new(per_file_violation)
182
+ "`#{per_file_violation.filepath}` references non-visible `#{per_file_violation.class_name}` from `#{per_file_violation.constant_source_package}`"
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,155 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'package_protections/private/colorized_string'
5
+ require 'package_protections/private/output'
6
+ require 'package_protections/private/typed_api_protection'
7
+ require 'package_protections/private/incoming_privacy_protection'
8
+ require 'package_protections/private/outgoing_dependency_protection'
9
+ require 'package_protections/private/metadata_modifiers'
10
+ require 'package_protections/private/multiple_namespaces_protection'
11
+ require 'package_protections/private/visibility_protection'
12
+
13
+ module PackageProtections
14
+ #
15
+ # This module cannot be accessed by clients of `PackageProtections` -- only within the `PackageProtections` module itself.
16
+ # All implementation details are in here to keep the main `PackageProtections` module easily scannable and to keep the private things private.
17
+ #
18
+ module Private
19
+ extend T::Sig
20
+
21
+ sig do
22
+ params(
23
+ packages: T::Array[ParsePackwerk::Package],
24
+ new_violations: T::Array[PerFileViolation]
25
+ ).returns(T::Array[Offense])
26
+ end
27
+ def self.get_offenses(packages:, new_violations:)
28
+ protected_packages = packages.map { |p| ProtectedPackage.from(p) }
29
+
30
+ PackageProtections.all.flat_map do |protector|
31
+ protector.get_offenses(protected_packages, new_violations)
32
+ end
33
+ end
34
+
35
+ sig do
36
+ params(
37
+ packages: T::Array[ParsePackwerk::Package],
38
+ protection_identifiers: T::Array[String],
39
+ verbose: T::Boolean
40
+ ).void
41
+ end
42
+ def self.set_defaults!(packages, protection_identifiers:, verbose:)
43
+ information = <<~INFO
44
+ We will attempt to set the defaults for #{packages.count} packages!
45
+ INFO
46
+ if verbose
47
+ Private::Output.p_colorized Private::ColorizedString.new(information).yellow, colorized: true
48
+ end
49
+
50
+ new_protected_packages = []
51
+
52
+ packages.each_with_index do |package, i|
53
+ if verbose
54
+ Private::Output.p_colorized Private::ColorizedString.new("[#{i + 1}/#{packages.count}] Setting defaults for #{package.name}").yellow, colorized: true
55
+ end
56
+
57
+ package_protections = PackageProtections.all.select { |p| protection_identifiers.include?(p.identifier) }
58
+ package_protections.each do |protection|
59
+ # We don't set defaults when the behavior is fail never because
60
+ next if protection.default_behavior.fail_never?
61
+
62
+ protections = package.metadata['protections'] || {}
63
+ current_behavior = protections[protection.identifier]
64
+ next if current_behavior.present?
65
+
66
+ package = Private::MetadataModifiers.package_with_modified_protection(package, protection.identifier, protection.default_behavior)
67
+ end
68
+
69
+ package = ParsePackwerk::Package.new(
70
+ name: package.name,
71
+ # We set these values to be true always by default
72
+ enforce_dependencies: true,
73
+ enforce_privacy: true,
74
+ dependencies: package.dependencies,
75
+ metadata: package.metadata
76
+ )
77
+
78
+ new_protected_packages << ProtectedPackage.from(package)
79
+ end
80
+
81
+ new_protected_packages.each do |package|
82
+ ParsePackwerk.write_package_yml!(package.original_package)
83
+ end
84
+ end
85
+
86
+ sig do
87
+ params(
88
+ package_names: T::Array[String],
89
+ all_packages: T::Array[ParsePackwerk::Package]
90
+ ).returns(T::Array[ParsePackwerk::Package])
91
+ end
92
+ def self.packages_for_names(package_names, all_packages)
93
+ all_packages_indexed_by_name = {}
94
+ all_packages.each { |package| all_packages_indexed_by_name[package.name] = package }
95
+
96
+ package_names.map do |package_name|
97
+ clean_pack_name = package_name.gsub(%r{/$}, '')
98
+ package = all_packages_indexed_by_name[clean_pack_name]
99
+ if package.nil?
100
+ 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}"
101
+ end
102
+
103
+ package
104
+ end
105
+ end
106
+
107
+ sig { params(root_pathname: Pathname).returns(String) }
108
+ def self.rubocop_yml(root_pathname:)
109
+ protected_packages = Dir.chdir(root_pathname) { all_protected_packages }
110
+ package_protection = T.cast(PackageProtections.all.select { |p| p.is_a?(RubocopProtectionInterface) }, T::Array[RubocopProtectionInterface])
111
+ cop_configs = package_protection.flat_map { |p| p.cop_configs(protected_packages) }
112
+ cop_configs.map(&:to_rubocop_yml_compatible_format).join("\n\n")
113
+ end
114
+
115
+ sig do
116
+ returns(T::Array[ProtectedPackage])
117
+ end
118
+ def self.all_protected_packages
119
+ # Note -- we should get rid of Package in favor of SimplePackage
120
+ # Benchmark ParsePackwerk.all with package vs simple package
121
+ # convert tools to use ParsePackwerk::DeprecatedReferences.from(packs.directory)
122
+ # that should make this faster and not affect rubocop as much
123
+ ParsePackwerk.all.map do |p|
124
+ ProtectedPackage.from(p)
125
+ end
126
+ end
127
+
128
+ sig { params(name: String).returns(ProtectedPackage) }
129
+ def self.get_package_with_name(name)
130
+ @protected_packages_indexed_by_name ||= T.let(@protected_packages_indexed_by_name, T.nilable(T::Hash[String, ProtectedPackage]))
131
+ @protected_packages_indexed_by_name ||= all_protected_packages.each_with_object({}) { |package, index|
132
+ index[package.name] = package
133
+ }
134
+ @protected_packages_indexed_by_name[name] || raise(StandardError, "Could not find package #{name}")
135
+ end
136
+
137
+ sig { void }
138
+ def self.bust_cache!
139
+ @protected_packages_indexed_by_name = nil
140
+ @private_cop_config = nil
141
+ end
142
+
143
+ sig { params(identifier: Identifier).returns(T::Hash[T.untyped, T.untyped]) }
144
+ def self.private_cop_config(identifier)
145
+ @private_cop_config ||= T.let(@private_cop_config, T.nilable(T::Hash[T.untyped, T.untyped]))
146
+ @private_cop_config ||= begin
147
+ protected_packages = all_protected_packages
148
+ protection = T.cast(PackageProtections.with_identifier(identifier), PackageProtections::RubocopProtectionInterface)
149
+ protected_packages.map { |p| [p.name, protection.custom_cop_config(p)] }.to_h
150
+ end
151
+ end
152
+ end
153
+
154
+ private_constant :Private
155
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: strict
4
+ module PackageProtections
5
+ class ProtectedPackage < T::Struct
6
+ extend T::Sig
7
+
8
+ const :original_package, ParsePackwerk::Package
9
+ const :protections, T::Hash[Identifier, ViolationBehavior]
10
+ const :deprecated_references, ParsePackwerk::DeprecatedReferences
11
+
12
+ sig { params(original_package: ParsePackwerk::Package).returns(ProtectedPackage) }
13
+ def self.from(original_package)
14
+ metadata = original_package.metadata['protections'] || {}
15
+
16
+ valid_identifiers = PackageProtections.all.map(&:identifier)
17
+ invalid_identifiers = metadata.keys - valid_identifiers
18
+
19
+ if invalid_identifiers.any?
20
+ raise IncorrectPublicApiUsageError.new("Invalid configuration for package `#{original_package.name}`. The metadata keys #{invalid_identifiers.inspect} are not valid behaviors under the `protection` metadata namespace. Valid keys are #{valid_identifiers.inspect}. See https://github.com/bigrails/package_protections#readme for more info") # rubocop:disable Style/RaiseArgs
21
+ end
22
+
23
+ protections = {}
24
+ metadata.each_key do |protection_key|
25
+ protection = PackageProtections.with_identifier(protection_key)
26
+ if !protection
27
+ raise IncorrectPublicApiUsageError.new("Invalid configuration for package `#{original_package.name}`. The metadata key #{protection_key} is not a valid behaviors under the `protection` metadata namespace. Valid keys are #{valid_identifiers.inspect}. See https://github.com/bigrails/package_protections#readme for more info") # rubocop:disable Style/RaiseArgs
28
+ end
29
+
30
+ protections[protection.identifier] = get_violation_behavior(protection, metadata, original_package)
31
+ end
32
+
33
+ unspecified_protections = valid_identifiers - protections.keys
34
+ protections_requiring_explicit_configuration = T.let([], T::Array[Identifier])
35
+ unspecified_protections.each do |protection_key|
36
+ protection = PackageProtections.with_identifier(protection_key)
37
+ if !protection.default_behavior.fail_never?
38
+ protections_requiring_explicit_configuration << protection.identifier
39
+ end
40
+ protections[protection_key] = protection.default_behavior
41
+ end
42
+
43
+ if protections_requiring_explicit_configuration.any?
44
+ error = "All protections must explicitly set unless their default behavior is `fail_never`. Missing protections: #{protections_requiring_explicit_configuration.join(', ')}"
45
+ raise IncorrectPublicApiUsageError, error
46
+ end
47
+
48
+ new(
49
+ original_package: original_package,
50
+ protections: protections,
51
+ deprecated_references: ParsePackwerk::DeprecatedReferences.for(original_package)
52
+ )
53
+ end
54
+
55
+ sig { params(protection: ProtectionInterface, metadata: T::Hash[T.untyped, T.untyped], package: ParsePackwerk::Package).returns(ViolationBehavior) }
56
+ def self.get_violation_behavior(protection, metadata, package)
57
+ behavior = ViolationBehavior.from_raw_value(metadata[protection.identifier])
58
+ unmet_preconditions = protection.unmet_preconditions_for_behavior(behavior, package)
59
+ if !unmet_preconditions.nil?
60
+ raise IncorrectPublicApiUsageError.new("#{protection.identifier} protection does not have the valid preconditions. #{unmet_preconditions}. See https://github.com/bigrails/package_protections#readme for more info") # rubocop:disable Style/RaiseArgs
61
+ end
62
+
63
+ behavior
64
+ end
65
+
66
+ sig { params(key: Identifier).returns(ViolationBehavior) }
67
+ def violation_behavior_for(key)
68
+ protections.fetch(key)
69
+ end
70
+
71
+ sig { returns(String) }
72
+ def name
73
+ original_package.name
74
+ end
75
+
76
+ sig { returns(ParsePackwerk::MetadataYmlType) }
77
+ def metadata
78
+ original_package.metadata
79
+ end
80
+
81
+ sig { returns(Pathname) }
82
+ def yml
83
+ original_package.yml
84
+ end
85
+
86
+ sig { returns(T::Array[String]) }
87
+ def dependencies
88
+ original_package.dependencies
89
+ end
90
+
91
+ sig { returns(T::Set[String]) }
92
+ def visible_to
93
+ Set.new(metadata['visible_to'] || [])
94
+ end
95
+
96
+ sig { returns(T::Array[ParsePackwerk::Violation]) }
97
+ def violations
98
+ deprecated_references.violations
99
+ end
100
+ end
101
+ end