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.
- checksums.yaml +7 -0
- data/README.md +149 -0
- data/config/default.yml +8 -0
- data/lib/package_protections/offense.rb +17 -0
- data/lib/package_protections/per_file_violation.rb +39 -0
- data/lib/package_protections/private/colorized_string.rb +97 -0
- data/lib/package_protections/private/incoming_privacy_protection.rb +118 -0
- data/lib/package_protections/private/metadata_modifiers.rb +27 -0
- data/lib/package_protections/private/multiple_namespaces_protection.rb +128 -0
- data/lib/package_protections/private/outgoing_dependency_protection.rb +117 -0
- data/lib/package_protections/private/output.rb +24 -0
- data/lib/package_protections/private/typed_api_protection.rb +106 -0
- data/lib/package_protections/private/visibility_protection.rb +186 -0
- data/lib/package_protections/private.rb +155 -0
- data/lib/package_protections/protected_package.rb +101 -0
- data/lib/package_protections/protection_interface.rb +69 -0
- data/lib/package_protections/rubocop_protection_interface.rb +84 -0
- data/lib/package_protections/violation_behavior.rb +69 -0
- data/lib/package_protections.rb +135 -0
- data/lib/rubocop/cop/package_protections/namespaced_under_package_name.rb +108 -0
- metadata +220 -0
@@ -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
|