Skip to content

Commit 5bbedf1

Browse files
author
Alex Evanczuk
committed
Extract privacy validations from ApplicationValidator
1 parent 618baf0 commit 5bbedf1

7 files changed

+362
-223
lines changed

lib/packwerk.rb

+8
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,12 @@ module Checkers
8585
autoload :PrivacyChecker
8686
end
8787
end
88+
89+
class ApplicationValidator
90+
extend ActiveSupport::Autoload
91+
92+
autoload :Result
93+
autoload :CheckPackageManifestsForPrivacy
94+
autoload :Helpers
95+
end
8896
end

lib/packwerk/application_validator.rb

+26-147
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ module Packwerk
1111
class ApplicationValidator
1212
extend T::Sig
1313

14+
# This is a temporary API as we migrate validators to their own files.
15+
# Later, we can expose an API to get package sets to pass into validators when testing
16+
# This API would likely just be `PackageSet.load_all_from(configruation)`, but we might want to clean
17+
# up that API a bit (it looks like there are some unnecessary input variables).
18+
sig { returns(PackageSet) }
19+
attr_reader :package_set
20+
1421
sig do
1522
params(
1623
config_file_path: String,
@@ -22,26 +29,18 @@ def initialize(config_file_path:, configuration:, environment:)
2229
@config_file_path = config_file_path
2330
@configuration = configuration
2431
@environment = environment
25-
@package_set = T.let(PackageSet.load_all_from(@configuration.root_path, package_pathspec: package_glob),
26-
PackageSet)
27-
end
28-
29-
class Result < T::Struct
30-
extend T::Sig
31-
32-
const :ok, T::Boolean
33-
const :error_value, T.nilable(String)
32+
package_set = PackageSet.load_all_from(
33+
@configuration.root_path,
34+
package_pathspec: Helpers.package_glob(configuration)
35+
)
3436

35-
sig { returns(T::Boolean) }
36-
def ok?
37-
ok
38-
end
37+
@package_set = T.let(package_set, PackageSet)
3938
end
4039

4140
sig { returns(Result) }
4241
def check_all
4342
results = [
44-
check_package_manifests_for_privacy,
43+
CheckPackageManifestsForPrivacy.call(@package_set, @configuration),
4544
check_package_manifest_syntax,
4645
check_application_structure,
4746
check_acyclic_graph,
@@ -50,50 +49,23 @@ def check_all
5049
check_root_package_exists,
5150
]
5251

53-
merge_results(results)
54-
end
55-
56-
sig { returns(Result) }
57-
def check_package_manifests_for_privacy
58-
privacy_settings = package_manifests_settings_for("enforce_privacy")
59-
60-
resolver = ConstantResolver.new(
61-
root_path: @configuration.root_path,
62-
load_paths: @configuration.load_paths
63-
)
64-
65-
results = T.let([], T::Array[Result])
66-
67-
privacy_settings.each do |config_file_path, setting|
68-
next unless setting.is_a?(Array)
69-
70-
constants = setting
71-
72-
results += assert_constants_can_be_loaded(constants, config_file_path)
73-
74-
constant_locations = constants.map { |c| [c, resolver.resolve(c)&.location] }
75-
76-
constant_locations.each do |name, location|
77-
results << if location
78-
check_private_constant_location(name, location, config_file_path)
79-
else
80-
private_constant_unresolvable(name, config_file_path)
81-
end
82-
end
83-
end
84-
85-
merge_results(results, separator: "\n---\n")
52+
Helpers.merge_results(results)
8653
end
8754

8855
sig { returns(Result) }
8956
def check_package_manifest_syntax
9057
errors = []
9158

92-
package_manifests.each do |f|
59+
Helpers.package_manifests(@configuration).each do |f|
9360
hash = YAML.load_file(f)
9461
next unless hash
9562

96-
known_keys = ["enforce_privacy", "enforce_dependencies", "public_path", "dependencies", "metadata"]
63+
known_keys = [
64+
*CheckPackageManifestsForPrivacy.permitted_keys,
65+
"enforce_dependencies",
66+
"dependencies",
67+
"metadata",
68+
]
9769
unknown_keys = hash.keys - known_keys
9870

9971
unless unknown_keys.empty?
@@ -102,24 +74,12 @@ def check_package_manifest_syntax
10274
"open an issue in https://github.com/Shopify/packwerk"
10375
end
10476

105-
if hash.key?("enforce_privacy")
106-
unless [TrueClass, FalseClass, Array].include?(hash["enforce_privacy"].class)
107-
errors << "Invalid 'enforce_privacy' option in #{f.inspect}: #{hash["enforce_privacy"].inspect}"
108-
end
109-
end
110-
11177
if hash.key?("enforce_dependencies")
11278
unless [TrueClass, FalseClass].include?(hash["enforce_dependencies"].class)
11379
errors << "Invalid 'enforce_dependencies' option in #{f.inspect}: #{hash["enforce_dependencies"].inspect}"
11480
end
11581
end
11682

117-
if hash.key?("public_path")
118-
unless hash["public_path"].is_a?(String)
119-
errors << "'public_path' option must be a string in #{f.inspect}: #{hash["public_path"].inspect}"
120-
end
121-
end
122-
12383
next unless hash.key?("dependencies")
12484
next if hash["dependencies"].is_a?(Array)
12585

@@ -173,8 +133,8 @@ def check_acyclic_graph
173133

174134
sig { returns(Result) }
175135
def check_package_manifest_paths
176-
all_package_manifests = package_manifests("**/")
177-
package_paths_package_manifests = package_manifests(package_glob)
136+
all_package_manifests = Helpers.package_manifests(@configuration, "**/")
137+
package_paths_package_manifests = Helpers.package_manifests(@configuration, Helpers.package_glob(@configuration))
178138

179139
difference = all_package_manifests - package_paths_package_manifests
180140

@@ -194,7 +154,7 @@ def check_package_manifest_paths
194154

195155
sig { returns(Result) }
196156
def check_valid_package_dependencies
197-
packages_dependencies = package_manifests_settings_for("dependencies")
157+
packages_dependencies = Helpers.package_manifests_settings_for(@configuration, "dependencies")
198158
.delete_if { |_, deps| deps.nil? }
199159

200160
packages_with_invalid_dependencies =
@@ -231,7 +191,7 @@ def check_valid_package_dependencies
231191
sig { returns(Result) }
232192
def check_root_package_exists
233193
root_package_path = File.join(@configuration.root_path, "package.yml")
234-
all_packages_manifests = package_manifests(package_glob)
194+
all_packages_manifests = Helpers.package_manifests(@configuration, Helpers.package_glob(@configuration))
235195

236196
if all_packages_manifests.include?(root_package_path)
237197
Result.new(ok: true)
@@ -263,35 +223,14 @@ def build_cycle_strings(cycles)
263223
end
264224
end
265225

266-
sig { params(setting: T.untyped).returns(T.untyped) }
267-
def package_manifests_settings_for(setting)
268-
package_manifests.map { |f| [f, (YAML.load_file(File.join(f)) || {})[setting]] }
269-
end
270-
271226
sig { params(list: T.untyped).returns(T.untyped) }
272227
def format_yaml_strings(list)
273228
list.sort.map { |p| "- \"#{p}\"" }.join("\n")
274229
end
275230

276-
sig { returns(T.any(T::Array[String], String)) }
277-
def package_glob
278-
@configuration.package_paths || "**"
279-
end
280-
281-
sig { params(glob_pattern: T.any(T::Array[String], String)).returns(T::Array[String]) }
282-
def package_manifests(glob_pattern = package_glob)
283-
PackageSet.package_paths(@configuration.root_path, glob_pattern, @configuration.exclude)
284-
.map { |f| File.realpath(f) }
285-
end
286-
287231
sig { params(paths: T::Array[String]).returns(T::Array[Pathname]) }
288232
def relative_paths(paths)
289-
paths.map { |path| relative_path(path) }
290-
end
291-
292-
sig { params(path: String).returns(Pathname) }
293-
def relative_path(path)
294-
Pathname.new(path).relative_path_from(@configuration.root_path)
233+
paths.map { |path| Helpers.relative_path(@configuration, path) }
295234
end
296235

297236
sig { params(path: T.untyped).returns(T::Boolean) }
@@ -302,65 +241,5 @@ def invalid_package_path?(path)
302241
package_path = File.join(@configuration.root_path, path, PackageSet::PACKAGE_CONFIG_FILENAME)
303242
!File.file?(package_path)
304243
end
305-
306-
sig { params(constants: T.untyped, config_file_path: String).returns(T::Array[Result]) }
307-
def assert_constants_can_be_loaded(constants, config_file_path)
308-
constants.map do |constant|
309-
if !constant.start_with?("::")
310-
Result.new(
311-
ok: false,
312-
error_value: "'#{constant}', listed in the 'enforce_privacy' option in #{config_file_path}, is invalid.\n"\
313-
"Private constants need to be prefixed with the top-level namespace operator `::`."
314-
)
315-
else
316-
constant.try(&:constantize) && Result.new(ok: true)
317-
end
318-
end
319-
end
320-
321-
sig { params(name: T.untyped, config_file_path: T.untyped).returns(Result) }
322-
def private_constant_unresolvable(name, config_file_path)
323-
explicit_filepath = (name.start_with?("::") ? name[2..-1] : name).underscore + ".rb"
324-
325-
Result.new(
326-
ok: false,
327-
error_value: "'#{name}', listed in #{config_file_path}, could not be resolved.\n"\
328-
"This is probably because it is an autovivified namespace - a namespace module that doesn't have a\n"\
329-
"file explicitly defining it. Packwerk currently doesn't support declaring autovivified namespaces as\n"\
330-
"private. Add a #{explicit_filepath} file to explicitly define the constant."
331-
)
332-
end
333-
334-
sig { params(name: T.untyped, location: T.untyped, config_file_path: T.untyped).returns(Result) }
335-
def check_private_constant_location(name, location, config_file_path)
336-
declared_package = @package_set.package_from_path(relative_path(config_file_path))
337-
constant_package = @package_set.package_from_path(location)
338-
339-
if constant_package == declared_package
340-
Result.new(ok: true)
341-
else
342-
Result.new(
343-
ok: false,
344-
error_value: "'#{name}' is declared as private in the '#{declared_package}' package but appears to be "\
345-
"defined\nin the '#{constant_package}' package. Packwerk resolved it to #{location}."
346-
)
347-
end
348-
end
349-
350-
sig do
351-
params(results: T::Array[Result], separator: String, errors_headline: String).returns(Result)
352-
end
353-
def merge_results(results, separator: "\n===\n", errors_headline: "")
354-
results.reject!(&:ok?)
355-
356-
if results.empty?
357-
Result.new(ok: true)
358-
else
359-
Result.new(
360-
ok: false,
361-
error_value: errors_headline + results.map(&:error_value).join(separator)
362-
)
363-
end
364-
end
365244
end
366245
end

0 commit comments

Comments
 (0)