packwerk 1.3.1 → 2.1.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 +4 -4
- data/CHANGELOG.md +1 -0
- data/Gemfile.lock +12 -9
- data/README.md +9 -3
- data/TROUBLESHOOT.md +3 -3
- data/UPGRADING.md +54 -0
- data/USAGE.md +32 -51
- data/exe/packwerk +7 -1
- data/lib/packwerk/application_load_paths.rb +1 -0
- data/lib/packwerk/application_validator.rb +17 -59
- data/lib/packwerk/association_inspector.rb +1 -1
- data/lib/packwerk/cache.rb +168 -0
- data/lib/packwerk/cli.rb +37 -20
- data/lib/packwerk/configuration.rb +40 -5
- data/lib/packwerk/const_node_inspector.rb +3 -2
- data/lib/packwerk/constant_discovery.rb +4 -4
- data/lib/packwerk/constant_name_inspector.rb +1 -1
- data/lib/packwerk/deprecated_references.rb +18 -6
- data/lib/packwerk/file_processor.rb +53 -14
- data/lib/packwerk/files_for_processing.rb +15 -4
- data/lib/packwerk/formatters/offenses_formatter.rb +1 -1
- data/lib/packwerk/formatters/progress_formatter.rb +1 -1
- data/lib/packwerk/generators/configuration_file.rb +4 -19
- data/lib/packwerk/generators/templates/package.yml +1 -1
- data/lib/packwerk/generators/templates/packwerk.yml.erb +5 -5
- data/lib/packwerk/graph.rb +2 -0
- data/lib/packwerk/node.rb +2 -0
- data/lib/packwerk/node_processor.rb +10 -22
- data/lib/packwerk/node_processor_factory.rb +0 -3
- data/lib/packwerk/node_visitor.rb +7 -2
- data/lib/packwerk/package.rb +25 -4
- data/lib/packwerk/package_set.rb +44 -8
- data/lib/packwerk/parsed_constant_definitions.rb +5 -4
- data/lib/packwerk/reference.rb +2 -1
- data/lib/packwerk/reference_checking/checkers/checker.rb +21 -0
- data/lib/packwerk/reference_checking/checkers/dependency_checker.rb +31 -0
- data/lib/packwerk/reference_checking/checkers/privacy_checker.rb +58 -0
- data/lib/packwerk/reference_checking/reference_checker.rb +33 -0
- data/lib/packwerk/reference_extractor.rb +66 -19
- data/lib/packwerk/reference_offense.rb +1 -0
- data/lib/packwerk/run_context.rb +32 -11
- data/lib/packwerk/sanity_checker.rb +1 -1
- data/lib/packwerk/spring_command.rb +28 -0
- data/lib/packwerk/unresolved_reference.rb +10 -0
- data/lib/packwerk/version.rb +1 -1
- data/lib/packwerk/violation_type.rb +1 -1
- data/lib/packwerk.rb +19 -12
- data/packwerk.gemspec +4 -1
- data/service.yml +0 -2
- data/sorbet/rbi/gems/psych@3.3.2.rbi +24 -0
- metadata +41 -11
- data/lib/packwerk/checker.rb +0 -17
- data/lib/packwerk/dependency_checker.rb +0 -26
- data/lib/packwerk/generators/inflections_file.rb +0 -43
- data/lib/packwerk/generators/templates/inflections.yml +0 -6
- data/lib/packwerk/inflections/custom.rb +0 -33
- data/lib/packwerk/inflections/default.rb +0 -73
- data/lib/packwerk/inflector.rb +0 -48
- data/lib/packwerk/privacy_checker.rb +0 -53
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require "digest"
|
5
|
+
|
6
|
+
module Packwerk
|
7
|
+
class Cache
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
class CacheContents < T::Struct
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
const :file_contents_digest, String
|
14
|
+
const :unresolved_references, T::Array[UnresolvedReference]
|
15
|
+
|
16
|
+
sig { returns(String) }
|
17
|
+
def serialize
|
18
|
+
to_json
|
19
|
+
end
|
20
|
+
|
21
|
+
sig { params(serialized_cache_contents: String).returns(CacheContents) }
|
22
|
+
def self.deserialize(serialized_cache_contents)
|
23
|
+
cache_contents_json = JSON.parse(serialized_cache_contents)
|
24
|
+
unresolved_references = cache_contents_json["unresolved_references"].map do |json|
|
25
|
+
UnresolvedReference.new(
|
26
|
+
json["constant_name"],
|
27
|
+
json["namespace_path"],
|
28
|
+
json["relative_path"],
|
29
|
+
Node::Location.new(json["source_location"]["line"], json["source_location"]["column"],)
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
CacheContents.new(
|
34
|
+
file_contents_digest: cache_contents_json["file_contents_digest"],
|
35
|
+
unresolved_references: unresolved_references,
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
CACHE_SHAPE = T.type_alias do
|
41
|
+
T::Hash[
|
42
|
+
String,
|
43
|
+
CacheContents
|
44
|
+
]
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { params(enable_cache: T::Boolean, cache_directory: Pathname, config_path: String).void }
|
48
|
+
def initialize(enable_cache:, cache_directory:, config_path:)
|
49
|
+
@enable_cache = enable_cache
|
50
|
+
@cache = T.let({}, CACHE_SHAPE)
|
51
|
+
@files_by_digest = T.let({}, T::Hash[String, String])
|
52
|
+
@config_path = config_path
|
53
|
+
@cache_directory = cache_directory
|
54
|
+
|
55
|
+
if @enable_cache
|
56
|
+
create_cache_directory!
|
57
|
+
bust_cache_if_packwerk_yml_has_changed!
|
58
|
+
bust_cache_if_inflections_have_changed!
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { void }
|
63
|
+
def bust_cache!
|
64
|
+
FileUtils.rm_rf(@cache_directory)
|
65
|
+
end
|
66
|
+
|
67
|
+
sig do
|
68
|
+
params(
|
69
|
+
file_path: String,
|
70
|
+
block: T.proc.returns(T::Array[UnresolvedReference])
|
71
|
+
).returns(T::Array[UnresolvedReference])
|
72
|
+
end
|
73
|
+
def with_cache(file_path, &block)
|
74
|
+
return block.call unless @enable_cache
|
75
|
+
|
76
|
+
cache_location = @cache_directory.join(digest_for_string(file_path))
|
77
|
+
|
78
|
+
cache_contents = if cache_location.exist?
|
79
|
+
T.let(CacheContents.deserialize(cache_location.read),
|
80
|
+
CacheContents)
|
81
|
+
end
|
82
|
+
|
83
|
+
file_contents_digest = digest_for_file(file_path)
|
84
|
+
|
85
|
+
if !cache_contents.nil? && cache_contents.file_contents_digest == file_contents_digest
|
86
|
+
Debug.out("Cache hit for #{file_path}")
|
87
|
+
|
88
|
+
cache_contents.unresolved_references
|
89
|
+
else
|
90
|
+
Debug.out("Cache miss for #{file_path}")
|
91
|
+
|
92
|
+
unresolved_references = block.call
|
93
|
+
|
94
|
+
cache_contents = CacheContents.new(
|
95
|
+
file_contents_digest: file_contents_digest,
|
96
|
+
unresolved_references: unresolved_references,
|
97
|
+
)
|
98
|
+
cache_location.write(cache_contents.serialize)
|
99
|
+
|
100
|
+
unresolved_references
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
sig { params(file: String).returns(String) }
|
105
|
+
def digest_for_file(file)
|
106
|
+
digest_for_string(File.read(file))
|
107
|
+
end
|
108
|
+
|
109
|
+
sig { params(str: String).returns(String) }
|
110
|
+
def digest_for_string(str)
|
111
|
+
# MD5 appears to be the fastest
|
112
|
+
# https://gist.github.com/morimori/1330095
|
113
|
+
Digest::MD5.hexdigest(str)
|
114
|
+
end
|
115
|
+
|
116
|
+
sig { void }
|
117
|
+
def bust_cache_if_packwerk_yml_has_changed!
|
118
|
+
bust_cache_if_contents_have_changed(File.read(@config_path), :packwerk_yml)
|
119
|
+
end
|
120
|
+
|
121
|
+
sig { void }
|
122
|
+
def bust_cache_if_inflections_have_changed!
|
123
|
+
bust_cache_if_contents_have_changed(YAML.dump(ActiveSupport::Inflector.inflections), :inflections)
|
124
|
+
end
|
125
|
+
|
126
|
+
sig { params(contents: String, contents_key: Symbol).void }
|
127
|
+
def bust_cache_if_contents_have_changed(contents, contents_key)
|
128
|
+
current_digest = digest_for_string(contents)
|
129
|
+
cached_digest_path = @cache_directory.join(contents_key.to_s)
|
130
|
+
|
131
|
+
if !cached_digest_path.exist?
|
132
|
+
# In this case, we have nothing cached
|
133
|
+
# We save the current digest. This way the next time we compare current digest to cached digest,
|
134
|
+
# we can accurately determine if we should bust the cache
|
135
|
+
cached_digest_path.write(current_digest)
|
136
|
+
|
137
|
+
nil
|
138
|
+
elsif cached_digest_path.read == current_digest
|
139
|
+
Debug.out("#{contents_key} contents have NOT changed, preserving cache")
|
140
|
+
else
|
141
|
+
Debug.out("#{contents_key} contents have changed, busting cache")
|
142
|
+
|
143
|
+
bust_cache!
|
144
|
+
create_cache_directory!
|
145
|
+
|
146
|
+
cached_digest_path.write(current_digest)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
sig { void }
|
151
|
+
def create_cache_directory!
|
152
|
+
FileUtils.mkdir_p(@cache_directory)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class Debug
|
157
|
+
extend T::Sig
|
158
|
+
|
159
|
+
sig { params(out: String).void }
|
160
|
+
def self.out(out)
|
161
|
+
if ENV["DEBUG_PACKWERK_CACHE"]
|
162
|
+
puts(out)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
private_constant :Debug
|
168
|
+
end
|
data/lib/packwerk/cli.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
# typed: true
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "optparse"
|
5
|
+
|
4
6
|
module Packwerk
|
7
|
+
# A command-line interface to Packwerk.
|
5
8
|
class Cli
|
6
9
|
extend T::Sig
|
7
10
|
|
@@ -50,8 +53,6 @@ module Packwerk
|
|
50
53
|
output_result(parse_run(args).check)
|
51
54
|
when "detect-stale-violations"
|
52
55
|
output_result(parse_run(args).detect_stale_violations)
|
53
|
-
when "update"
|
54
|
-
update(args)
|
55
56
|
when "update-deprecations"
|
56
57
|
output_result(parse_run(args).update_deprecations)
|
57
58
|
when "validate"
|
@@ -85,19 +86,18 @@ module Packwerk
|
|
85
86
|
|
86
87
|
def generate_configs
|
87
88
|
configuration_file = Packwerk::Generators::ConfigurationFile.generate(
|
88
|
-
load_paths: Packwerk::ApplicationLoadPaths.extract_relevant_paths(@configuration.root_path, @environment),
|
89
89
|
root: @configuration.root_path,
|
90
90
|
out: @out
|
91
91
|
)
|
92
|
-
|
92
|
+
|
93
93
|
root_package = Packwerk::Generators::RootPackage.generate(root: @configuration.root_path, out: @out)
|
94
94
|
|
95
|
-
success = configuration_file &&
|
95
|
+
success = configuration_file && root_package
|
96
96
|
|
97
97
|
result = if success
|
98
98
|
<<~EOS
|
99
99
|
|
100
|
-
🎉 Packwerk is ready to be used. You can start defining packages and run `packwerk check`.
|
100
|
+
🎉 Packwerk is ready to be used. You can start defining packages and run `bin/packwerk check`.
|
101
101
|
For more information on how to use Packwerk, see: https://github.com/Shopify/packwerk/blob/main/USAGE.md
|
102
102
|
EOS
|
103
103
|
else
|
@@ -112,19 +112,18 @@ module Packwerk
|
|
112
112
|
success
|
113
113
|
end
|
114
114
|
|
115
|
-
def update(paths)
|
116
|
-
warn("`packwerk update` is deprecated in favor of `packwerk update-deprecations`.")
|
117
|
-
output_result(parse_run(paths).update_deprecations)
|
118
|
-
end
|
119
|
-
|
120
115
|
def output_result(result)
|
121
116
|
@out.puts
|
122
117
|
@out.puts(result.message)
|
123
118
|
result.status
|
124
119
|
end
|
125
120
|
|
126
|
-
def fetch_files_to_process(paths)
|
127
|
-
files = FilesForProcessing.fetch(
|
121
|
+
def fetch_files_to_process(paths, ignore_nested_packages)
|
122
|
+
files = FilesForProcessing.fetch(
|
123
|
+
paths: paths,
|
124
|
+
ignore_nested_packages: ignore_nested_packages,
|
125
|
+
configuration: @configuration
|
126
|
+
)
|
128
127
|
abort("No files found or given. "\
|
129
128
|
"Specify files or check the include and exclude glob in the config file.") if files.empty?
|
130
129
|
files
|
@@ -132,11 +131,6 @@ module Packwerk
|
|
132
131
|
|
133
132
|
def validate(_paths)
|
134
133
|
@progress_formatter.started_validation do
|
135
|
-
checker = Packwerk::ApplicationValidator.new(
|
136
|
-
config_file_path: @configuration.config_path,
|
137
|
-
configuration: @configuration,
|
138
|
-
environment: @environment,
|
139
|
-
)
|
140
134
|
result = checker.check_all
|
141
135
|
|
142
136
|
list_validation_errors(result)
|
@@ -145,6 +139,14 @@ module Packwerk
|
|
145
139
|
end
|
146
140
|
end
|
147
141
|
|
142
|
+
def checker
|
143
|
+
Packwerk::ApplicationValidator.new(
|
144
|
+
config_file_path: @configuration.config_path,
|
145
|
+
configuration: @configuration,
|
146
|
+
environment: @environment,
|
147
|
+
)
|
148
|
+
end
|
149
|
+
|
148
150
|
def list_validation_errors(result)
|
149
151
|
@out.puts
|
150
152
|
if result.ok?
|
@@ -155,9 +157,24 @@ module Packwerk
|
|
155
157
|
end
|
156
158
|
end
|
157
159
|
|
158
|
-
def parse_run(
|
160
|
+
def parse_run(params)
|
161
|
+
paths = T.let([], T::Array[String])
|
162
|
+
ignore_nested_packages = nil
|
163
|
+
|
164
|
+
if params.any? { |p| p.include?("--packages") }
|
165
|
+
OptionParser.new do |parser|
|
166
|
+
parser.on("--packages=PACKAGESLIST", Array, "package names, comma separated") do |p|
|
167
|
+
paths = p
|
168
|
+
end
|
169
|
+
end.parse!(params)
|
170
|
+
ignore_nested_packages = true
|
171
|
+
else
|
172
|
+
paths = params
|
173
|
+
ignore_nested_packages = false
|
174
|
+
end
|
175
|
+
|
159
176
|
ParseRun.new(
|
160
|
-
files: fetch_files_to_process(paths),
|
177
|
+
files: fetch_files_to_process(paths, ignore_nested_packages),
|
161
178
|
configuration: @configuration,
|
162
179
|
progress_formatter: @progress_formatter,
|
163
180
|
offenses_formatter: @offenses_formatter
|
@@ -34,8 +34,7 @@ module Packwerk
|
|
34
34
|
DEFAULT_EXCLUDE_GLOBS = ["{bin,node_modules,script,tmp,vendor}/**/*"]
|
35
35
|
|
36
36
|
attr_reader(
|
37
|
-
:include, :exclude, :root_path, :package_paths, :custom_associations, :
|
38
|
-
:config_path,
|
37
|
+
:include, :exclude, :root_path, :package_paths, :custom_associations, :config_path, :cache_directory
|
39
38
|
)
|
40
39
|
|
41
40
|
def initialize(configs = {}, config_path: nil)
|
@@ -45,15 +44,51 @@ module Packwerk
|
|
45
44
|
@root_path = File.expand_path(root)
|
46
45
|
@package_paths = configs["package_paths"] || "**/"
|
47
46
|
@custom_associations = configs["custom_associations"] || []
|
48
|
-
@load_paths = configs["load_paths"] || []
|
49
|
-
@inflections_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
|
50
47
|
@parallel = configs.key?("parallel") ? configs["parallel"] : true
|
51
|
-
|
48
|
+
@cache_enabled = configs.key?("cache") ? configs["cache"] : false
|
49
|
+
@cache_directory = Pathname.new(configs["cache_directory"] || "tmp/cache/packwerk")
|
52
50
|
@config_path = config_path
|
51
|
+
|
52
|
+
if configs["load_paths"]
|
53
|
+
warning = <<~WARNING
|
54
|
+
DEPRECATION WARNING: The 'load_paths' key in `packwerk.yml` is deprecated.
|
55
|
+
This value is no longer cached, and you can remove the key from `packwerk.yml`.
|
56
|
+
WARNING
|
57
|
+
|
58
|
+
warn(warning)
|
59
|
+
end
|
60
|
+
|
61
|
+
if configs["inflections_file"]
|
62
|
+
warning = <<~WARNING
|
63
|
+
DEPRECATION WARNING: The 'inflections_file' key in `packwerk.yml` is deprecated.
|
64
|
+
This value is no longer cached, and you can remove the key from `packwerk.yml`.
|
65
|
+
You can also delete #{configs["inflections_file"]}.
|
66
|
+
WARNING
|
67
|
+
|
68
|
+
warn(warning)
|
69
|
+
end
|
70
|
+
|
71
|
+
inflection_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
|
72
|
+
if Pathname.new(inflection_file).exist?
|
73
|
+
warning = <<~WARNING
|
74
|
+
DEPRECATION WARNING: Inflections YMLs in packwerk are now deprecated.
|
75
|
+
This value is no longer cached, and you can now delete #{inflection_file}
|
76
|
+
WARNING
|
77
|
+
|
78
|
+
warn(warning)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def load_paths
|
83
|
+
@load_paths ||= ApplicationLoadPaths.extract_relevant_paths(@root_path, "test")
|
53
84
|
end
|
54
85
|
|
55
86
|
def parallel?
|
56
87
|
@parallel
|
57
88
|
end
|
89
|
+
|
90
|
+
def cache_enabled?
|
91
|
+
@cache_enabled
|
92
|
+
end
|
58
93
|
end
|
59
94
|
end
|
@@ -15,6 +15,9 @@ module Packwerk
|
|
15
15
|
def constant_name_from_node(node, ancestors:)
|
16
16
|
return nil unless Node.constant?(node)
|
17
17
|
parent = ancestors.first
|
18
|
+
|
19
|
+
# Only process the root `const` node for namespaced constant references. For example, in the
|
20
|
+
# reference `Spam::Eggs::Thing`, we only process the const node associated with `Spam`.
|
18
21
|
return nil unless root_constant?(parent)
|
19
22
|
|
20
23
|
if parent && constant_in_module_or_class_definition?(node, parent: parent)
|
@@ -30,8 +33,6 @@ module Packwerk
|
|
30
33
|
|
31
34
|
private
|
32
35
|
|
33
|
-
# Only process the root `const` node for namespaced constant references. For example, in the
|
34
|
-
# reference `Spam::Eggs::Thing`, we only process the const node associated with `Spam`.
|
35
36
|
sig { params(parent: T.nilable(AST::Node)).returns(T::Boolean) }
|
36
37
|
def root_constant?(parent)
|
37
38
|
!(parent && Node.constant?(parent))
|
@@ -4,7 +4,7 @@
|
|
4
4
|
require "constant_resolver"
|
5
5
|
|
6
6
|
module Packwerk
|
7
|
-
# Get information about
|
7
|
+
# Get information about unresolved constants without loading the application code.
|
8
8
|
# Information gathered: Fully qualified name, path to file containing the definition, package,
|
9
9
|
# and visibility (public/private to the package).
|
10
10
|
#
|
@@ -35,9 +35,9 @@ module Packwerk
|
|
35
35
|
end
|
36
36
|
|
37
37
|
# Analyze a constant via its name.
|
38
|
-
# If the
|
38
|
+
# If the constant is unresolved, we need the current namespace path to correctly infer its full name
|
39
39
|
#
|
40
|
-
# @param const_name [String] The constant's name
|
40
|
+
# @param const_name [String] The unresolved constant's name.
|
41
41
|
# @param current_namespace_path [Array<String>] (optional) The namespace of the context in which the constant is
|
42
42
|
# used, e.g. ["Apps", "Models"] for `Apps::Models`. Defaults to [] which means top level.
|
43
43
|
# @return [Packwerk::ConstantDiscovery::ConstantContext]
|
@@ -45,7 +45,7 @@ module Packwerk
|
|
45
45
|
begin
|
46
46
|
constant = @resolver.resolve(const_name, current_namespace_path: current_namespace_path)
|
47
47
|
rescue ConstantResolver::Error => e
|
48
|
-
raise(ConstantResolver::Error, e.message
|
48
|
+
raise(ConstantResolver::Error, e.message)
|
49
49
|
end
|
50
50
|
|
51
51
|
return unless constant
|
@@ -4,7 +4,7 @@
|
|
4
4
|
require "ast"
|
5
5
|
|
6
6
|
module Packwerk
|
7
|
-
# An interface describing
|
7
|
+
# An interface describing an object that can extract a constant name from an AST node.
|
8
8
|
module ConstantNameInspector
|
9
9
|
extend T::Sig
|
10
10
|
extend T::Helpers
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "yaml"
|
@@ -7,11 +7,16 @@ module Packwerk
|
|
7
7
|
class DeprecatedReferences
|
8
8
|
extend T::Sig
|
9
9
|
|
10
|
+
ENTRIES_TYPE = T.type_alias do
|
11
|
+
T::Hash[String, T.untyped]
|
12
|
+
end
|
13
|
+
|
10
14
|
sig { params(package: Packwerk::Package, filepath: String).void }
|
11
15
|
def initialize(package, filepath)
|
12
16
|
@package = package
|
13
17
|
@filepath = filepath
|
14
|
-
@new_entries = {}
|
18
|
+
@new_entries = T.let({}, ENTRIES_TYPE)
|
19
|
+
@deprecated_references = T.let(nil, T.nilable(ENTRIES_TYPE))
|
15
20
|
end
|
16
21
|
|
17
22
|
sig do
|
@@ -73,7 +78,7 @@ module Packwerk
|
|
73
78
|
#
|
74
79
|
# You can regenerate this file using the following command:
|
75
80
|
#
|
76
|
-
# packwerk update-deprecations #{@package.name}
|
81
|
+
# bin/packwerk update-deprecations #{@package.name}
|
77
82
|
MESSAGE
|
78
83
|
File.open(@filepath, "w") do |f|
|
79
84
|
f.write(message)
|
@@ -84,7 +89,7 @@ module Packwerk
|
|
84
89
|
|
85
90
|
private
|
86
91
|
|
87
|
-
sig { returns(
|
92
|
+
sig { returns(ENTRIES_TYPE) }
|
88
93
|
def prepare_entries_for_dump
|
89
94
|
@new_entries.each do |package_name, package_violations|
|
90
95
|
package_violations.each do |_, entries_for_file|
|
@@ -97,13 +102,20 @@ module Packwerk
|
|
97
102
|
@new_entries = @new_entries.sort.to_h
|
98
103
|
end
|
99
104
|
|
100
|
-
sig { returns(
|
105
|
+
sig { returns(ENTRIES_TYPE) }
|
101
106
|
def deprecated_references
|
102
107
|
@deprecated_references ||= if File.exist?(@filepath)
|
103
|
-
|
108
|
+
load_yaml(@filepath)
|
104
109
|
else
|
105
110
|
{}
|
106
111
|
end
|
107
112
|
end
|
113
|
+
|
114
|
+
sig { params(filepath: String).returns(ENTRIES_TYPE) }
|
115
|
+
def load_yaml(filepath)
|
116
|
+
YAML.load_file(filepath) || {}
|
117
|
+
rescue Psych::Exception
|
118
|
+
{}
|
119
|
+
end
|
108
120
|
end
|
109
121
|
end
|
@@ -1,39 +1,78 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: true
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "ast/node"
|
5
5
|
|
6
6
|
module Packwerk
|
7
7
|
class FileProcessor
|
8
|
+
extend T::Sig
|
9
|
+
|
8
10
|
class UnknownFileTypeResult < Offense
|
9
11
|
def initialize(file:)
|
10
12
|
super(file: file, message: "unknown file type")
|
11
13
|
end
|
12
14
|
end
|
13
15
|
|
14
|
-
|
16
|
+
sig do
|
17
|
+
params(
|
18
|
+
node_processor_factory: NodeProcessorFactory,
|
19
|
+
cache: Cache,
|
20
|
+
parser_factory: T.nilable(Parsers::Factory)
|
21
|
+
).void
|
22
|
+
end
|
23
|
+
def initialize(node_processor_factory:, cache:, parser_factory: nil)
|
15
24
|
@node_processor_factory = node_processor_factory
|
25
|
+
@cache = cache
|
16
26
|
@parser_factory = parser_factory || Packwerk::Parsers::Factory.instance
|
17
27
|
end
|
18
28
|
|
29
|
+
sig do
|
30
|
+
params(file_path: String).returns(
|
31
|
+
T::Array[
|
32
|
+
T.any(
|
33
|
+
Packwerk::UnresolvedReference,
|
34
|
+
Packwerk::Offense,
|
35
|
+
)
|
36
|
+
]
|
37
|
+
)
|
38
|
+
end
|
19
39
|
def call(file_path)
|
20
|
-
|
21
|
-
|
40
|
+
return [UnknownFileTypeResult.new(file: file_path)] if parser_for(file_path).nil?
|
41
|
+
|
42
|
+
@cache.with_cache(file_path) do
|
43
|
+
node = parse_into_ast(file_path)
|
22
44
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
return [e.result]
|
45
|
+
return [] unless node
|
46
|
+
|
47
|
+
references_from_ast(node, file_path)
|
27
48
|
end
|
49
|
+
rescue Parsers::ParseError => e
|
50
|
+
[e.result]
|
51
|
+
end
|
28
52
|
|
29
|
-
|
30
|
-
if node
|
31
|
-
node_processor = @node_processor_factory.for(filename: file_path, node: node)
|
32
|
-
node_visitor = Packwerk::NodeVisitor.new(node_processor: node_processor)
|
53
|
+
private
|
33
54
|
|
34
|
-
|
55
|
+
sig do
|
56
|
+
params(node: Parser::AST::Node, file_path: String).returns(T::Array[UnresolvedReference])
|
57
|
+
end
|
58
|
+
def references_from_ast(node, file_path)
|
59
|
+
references = []
|
60
|
+
|
61
|
+
node_processor = @node_processor_factory.for(filename: file_path, node: node)
|
62
|
+
node_visitor = Packwerk::NodeVisitor.new(node_processor: node_processor)
|
63
|
+
node_visitor.visit(node, ancestors: [], result: references)
|
64
|
+
|
65
|
+
references
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_into_ast(file_path)
|
69
|
+
File.open(file_path, "r", nil, external_encoding: Encoding::UTF_8) do |file|
|
70
|
+
parser_for(file_path).call(io: file, file_path: file_path)
|
35
71
|
end
|
36
|
-
|
72
|
+
end
|
73
|
+
|
74
|
+
def parser_for(file_path)
|
75
|
+
@parser_factory.for_path(file_path)
|
37
76
|
end
|
38
77
|
end
|
39
78
|
end
|
@@ -4,14 +4,15 @@
|
|
4
4
|
module Packwerk
|
5
5
|
class FilesForProcessing
|
6
6
|
class << self
|
7
|
-
def fetch(paths:, configuration:)
|
8
|
-
new(paths, configuration).files
|
7
|
+
def fetch(paths:, configuration:, ignore_nested_packages: false)
|
8
|
+
new(paths, configuration, ignore_nested_packages).files
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
def initialize(paths, configuration)
|
12
|
+
def initialize(paths, configuration, ignore_nested_packages)
|
13
13
|
@paths = paths
|
14
14
|
@configuration = configuration
|
15
|
+
@ignore_nested_packages = ignore_nested_packages
|
15
16
|
end
|
16
17
|
|
17
18
|
def files
|
@@ -43,11 +44,21 @@ module Packwerk
|
|
43
44
|
File.expand_path(glob, @configuration.root_path)
|
44
45
|
end
|
45
46
|
|
46
|
-
Dir.glob([File.join(path, "**", "*")]).select do |file_path|
|
47
|
+
files = Dir.glob([File.join(path, "**", "*")]).select do |file_path|
|
47
48
|
absolute_includes.any? do |pattern|
|
48
49
|
File.fnmatch?(pattern, file_path, File::FNM_EXTGLOB)
|
49
50
|
end
|
50
51
|
end
|
52
|
+
|
53
|
+
if @ignore_nested_packages
|
54
|
+
nested_packages_paths = Dir.glob(File.join(path, "*", "**", "package.yml"))
|
55
|
+
nested_packages_globs = nested_packages_paths.map { |npp| npp.gsub("package.yml", "**/*") }
|
56
|
+
nested_packages_globs.each do |glob|
|
57
|
+
files -= Dir.glob(glob)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
files
|
51
62
|
end
|
52
63
|
|
53
64
|
def configured_included_files
|
@@ -44,7 +44,7 @@ module Packwerk
|
|
44
44
|
|
45
45
|
sig { params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
|
46
46
|
def offenses_summary(offenses)
|
47
|
-
offenses_string =
|
47
|
+
offenses_string = "offense".pluralize(offenses.length)
|
48
48
|
"#{offenses.length} #{offenses_string} detected"
|
49
49
|
end
|
50
50
|
end
|
@@ -16,7 +16,7 @@ module Packwerk
|
|
16
16
|
|
17
17
|
def started(target_files)
|
18
18
|
files_size = target_files.size
|
19
|
-
files_string =
|
19
|
+
files_string = "file".pluralize(files_size)
|
20
20
|
@out.puts("📦 Packwerk is inspecting #{files_size} #{files_string}")
|
21
21
|
end
|
22
22
|
|
@@ -11,18 +11,15 @@ module Packwerk
|
|
11
11
|
CONFIGURATION_TEMPLATE_FILE_PATH = "templates/packwerk.yml.erb"
|
12
12
|
|
13
13
|
class << self
|
14
|
-
def generate(
|
15
|
-
new(
|
14
|
+
def generate(root:, out:)
|
15
|
+
new(root: root, out: out).generate
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
sig { params(
|
20
|
-
def initialize(
|
21
|
-
@load_paths = load_paths
|
19
|
+
sig { params(root: String, out: T.any(StringIO, IO)).void }
|
20
|
+
def initialize(root:, out: $stdout)
|
22
21
|
@root = root
|
23
22
|
@out = out
|
24
|
-
|
25
|
-
set_template_variables
|
26
23
|
end
|
27
24
|
|
28
25
|
sig { returns(T::Boolean) }
|
@@ -43,18 +40,6 @@ module Packwerk
|
|
43
40
|
|
44
41
|
private
|
45
42
|
|
46
|
-
def set_template_variables
|
47
|
-
@load_paths_formatted = if @load_paths.empty?
|
48
|
-
"# load_paths:\n# - 'app/models'\n"
|
49
|
-
else
|
50
|
-
@load_paths.map { |path| "- #{path}\n" }.join
|
51
|
-
end
|
52
|
-
|
53
|
-
@load_paths_comment = unless @load_paths.empty?
|
54
|
-
"# These load paths were auto generated by Packwerk.\nload_paths:\n"
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
43
|
def render
|
59
44
|
ERB.new(template, trim_mode: "-").result(binding)
|
60
45
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# This file represents the root package of the application
|
2
|
-
# Please validate the configuration using `
|
2
|
+
# Please validate the configuration using `packwerk validate` (for Rails applications) or running the auto generated
|
3
3
|
# test case (for non-Rails projects). You can then use `packwerk check` to check your code.
|
4
4
|
|
5
5
|
# Turn on dependency checks for this package
|