packwerk 2.0.0 → 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/Gemfile.lock +4 -4
- data/README.md +7 -0
- data/USAGE.md +7 -0
- data/lib/packwerk/application_validator.rb +14 -5
- data/lib/packwerk/cache.rb +168 -0
- data/lib/packwerk/configuration.rb +17 -12
- data/lib/packwerk/constant_discovery.rb +3 -3
- data/lib/packwerk/deprecated_references.rb +1 -1
- data/lib/packwerk/file_processor.rb +19 -5
- data/lib/packwerk/generators/templates/packwerk.yml.erb +6 -0
- data/lib/packwerk/node.rb +2 -1
- data/lib/packwerk/node_processor.rb +1 -1
- data/lib/packwerk/node_processor_factory.rb +0 -1
- data/lib/packwerk/node_visitor.rb +3 -0
- data/lib/packwerk/package.rb +1 -1
- data/lib/packwerk/package_set.rb +2 -1
- data/lib/packwerk/parsed_constant_definitions.rb +4 -4
- data/lib/packwerk/reference_extractor.rb +65 -18
- data/lib/packwerk/run_context.rb +25 -7
- data/lib/packwerk/unresolved_reference.rb +10 -0
- data/lib/packwerk/version.rb +1 -1
- data/lib/packwerk.rb +2 -0
- data/packwerk.gemspec +1 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66332315c1155336fbe79f8aeb6e44a0893d4100a1d4b6b5f08c81574c940fe0
|
4
|
+
data.tar.gz: 88da350ce07daaf7556d23edbc318a19ca33b74af71ee624d547a8a8598bc16b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b54eeeac5e68285e03c32916d72f2afdc291d9d6d4993de0d2875d7dbb4a9cdf8cde7ca2677107f35288a20a20c1f8fc2f9ddcc330c9a7fe0dfaf8e245c07b3c
|
7
|
+
data.tar.gz: d8597878958e00a069cb5e51c609e04da9526b3c1933c4824399e4bad1173d718703c88ec582411a9c73fb5c88899707b04ebdb7b296a22b70e25598e11b57d6
|
data/Gemfile.lock
CHANGED
@@ -87,12 +87,13 @@ GIT
|
|
87
87
|
PATH
|
88
88
|
remote: .
|
89
89
|
specs:
|
90
|
-
packwerk (2.
|
90
|
+
packwerk (2.1.0)
|
91
91
|
activesupport (>= 5.2)
|
92
92
|
ast
|
93
93
|
better_html
|
94
94
|
bundler
|
95
95
|
constant_resolver
|
96
|
+
digest
|
96
97
|
parallel
|
97
98
|
parser
|
98
99
|
sorbet-runtime
|
@@ -118,6 +119,7 @@ GEM
|
|
118
119
|
concurrent-ruby (1.1.8)
|
119
120
|
constant_resolver (0.1.5)
|
120
121
|
crass (1.0.6)
|
122
|
+
digest (3.1.0)
|
121
123
|
erubi (1.10.0)
|
122
124
|
globalid (0.4.2)
|
123
125
|
activesupport (>= 4.2.0)
|
@@ -145,8 +147,6 @@ GEM
|
|
145
147
|
nokogiri (1.12.5)
|
146
148
|
mini_portile2 (~> 2.6.1)
|
147
149
|
racc (~> 1.4)
|
148
|
-
nokogiri (1.12.5-x86_64-darwin)
|
149
|
-
racc (~> 1.4)
|
150
150
|
parallel (1.20.1)
|
151
151
|
parlour (6.0.0)
|
152
152
|
commander (~> 4.5)
|
@@ -257,4 +257,4 @@ DEPENDENCIES
|
|
257
257
|
tapioca
|
258
258
|
|
259
259
|
BUNDLED WITH
|
260
|
-
2.
|
260
|
+
2.3.4
|
data/README.md
CHANGED
@@ -56,6 +56,13 @@ Read [USAGE.md](USAGE.md) for usage once Packwerk is installed on your project.
|
|
56
56
|
|
57
57
|
"Packwerk" is pronounced [[ˈpakvɛʁk]](https://cdn.shopify.com/s/files/1/0258/7469/files/packwerk.mp3).
|
58
58
|
|
59
|
+
## Ecosystem
|
60
|
+
|
61
|
+
You can use these third party tools to enhance your packwerk experience:
|
62
|
+
|
63
|
+
- https://github.com/bellroy/graphwerk draws a graph of your package dependencies
|
64
|
+
- https://github.com/Gusto/packwerk-vscode integrates packwerk into Visual Studio Code so you can see violations right in your editor
|
65
|
+
|
59
66
|
## Development
|
60
67
|
|
61
68
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/USAGE.md
CHANGED
@@ -77,6 +77,8 @@ Packwerk reads from the `packwerk.yml` configuration file in the root directory.
|
|
77
77
|
| package_paths | **/ | a single pattern or a list of patterns to find package configuration files, see: [Defining packages](#Defining-packages) |
|
78
78
|
| custom_associations | N/A | list of custom associations, if any |
|
79
79
|
| parallel | true | when true, fork code parsing out to subprocesses |
|
80
|
+
| cache | false | when true, caches the results of parsing files |
|
81
|
+
| cache_directory | tmp/cache/packwerk | the directory that will hold the packwerk cache |
|
80
82
|
|
81
83
|
### Using a custom ERB parser
|
82
84
|
|
@@ -100,6 +102,11 @@ end
|
|
100
102
|
Packwerk::Parsers::Factory.instance.erb_parser_class = CustomParser
|
101
103
|
```
|
102
104
|
|
105
|
+
## Using the cache
|
106
|
+
Packwerk ships with an cache to help speed up file parsing. You can turn this on by setting `cache: true` in `packwerk.yml`.
|
107
|
+
|
108
|
+
This will write to `tmp/cache/packwerk`.
|
109
|
+
|
103
110
|
## Validating the package system
|
104
111
|
|
105
112
|
There are some criteria that an application must meet in order to have a valid package system. These criteria include having a valid autoload path cache, package definition files, and application folder structure. The dependency graph within the package system also has to be acyclic.
|
@@ -39,13 +39,13 @@ module Packwerk
|
|
39
39
|
load_paths: @configuration.load_paths
|
40
40
|
)
|
41
41
|
|
42
|
-
results = []
|
42
|
+
results = T.let([], T::Array[Packwerk::Result])
|
43
43
|
|
44
44
|
privacy_settings.each do |config_file_path, setting|
|
45
45
|
next unless setting.is_a?(Array)
|
46
46
|
constants = setting
|
47
47
|
|
48
|
-
assert_constants_can_be_loaded(constants)
|
48
|
+
results += assert_constants_can_be_loaded(constants, config_file_path)
|
49
49
|
|
50
50
|
constant_locations = constants.map { |c| [c, resolver.resolve(c)&.location] }
|
51
51
|
|
@@ -265,9 +265,18 @@ module Packwerk
|
|
265
265
|
!File.file?(package_path)
|
266
266
|
end
|
267
267
|
|
268
|
-
def assert_constants_can_be_loaded(constants)
|
269
|
-
constants.
|
270
|
-
|
268
|
+
def assert_constants_can_be_loaded(constants, config_file_path)
|
269
|
+
constants.map do |constant|
|
270
|
+
if !constant.start_with?("::")
|
271
|
+
Result.new(
|
272
|
+
false,
|
273
|
+
"'#{constant}', listed in the 'enforce_privacy' option in #{config_file_path}, is invalid.\n"\
|
274
|
+
"Private constants need to be prefixed with the top-level namespace operator `::`."
|
275
|
+
)
|
276
|
+
else
|
277
|
+
constant.try(&:constantize) && Result.new(true)
|
278
|
+
end
|
279
|
+
end
|
271
280
|
end
|
272
281
|
|
273
282
|
def private_constant_unresolvable(name, config_file_path)
|
@@ -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
|
@@ -34,10 +34,21 @@ 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, :config_path
|
37
|
+
:include, :exclude, :root_path, :package_paths, :custom_associations, :config_path, :cache_directory
|
38
38
|
)
|
39
39
|
|
40
40
|
def initialize(configs = {}, config_path: nil)
|
41
|
+
@include = configs["include"] || DEFAULT_INCLUDE_GLOBS
|
42
|
+
@exclude = configs["exclude"] || DEFAULT_EXCLUDE_GLOBS
|
43
|
+
root = config_path ? File.dirname(config_path) : "."
|
44
|
+
@root_path = File.expand_path(root)
|
45
|
+
@package_paths = configs["package_paths"] || "**/"
|
46
|
+
@custom_associations = configs["custom_associations"] || []
|
47
|
+
@parallel = configs.key?("parallel") ? configs["parallel"] : true
|
48
|
+
@cache_enabled = configs.key?("cache") ? configs["cache"] : false
|
49
|
+
@cache_directory = Pathname.new(configs["cache_directory"] || "tmp/cache/packwerk")
|
50
|
+
@config_path = config_path
|
51
|
+
|
41
52
|
if configs["load_paths"]
|
42
53
|
warning = <<~WARNING
|
43
54
|
DEPRECATION WARNING: The 'load_paths' key in `packwerk.yml` is deprecated.
|
@@ -47,7 +58,6 @@ module Packwerk
|
|
47
58
|
warn(warning)
|
48
59
|
end
|
49
60
|
|
50
|
-
inflection_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
|
51
61
|
if configs["inflections_file"]
|
52
62
|
warning = <<~WARNING
|
53
63
|
DEPRECATION WARNING: The 'inflections_file' key in `packwerk.yml` is deprecated.
|
@@ -58,6 +68,7 @@ module Packwerk
|
|
58
68
|
warn(warning)
|
59
69
|
end
|
60
70
|
|
71
|
+
inflection_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
|
61
72
|
if Pathname.new(inflection_file).exist?
|
62
73
|
warning = <<~WARNING
|
63
74
|
DEPRECATION WARNING: Inflections YMLs in packwerk are now deprecated.
|
@@ -66,16 +77,6 @@ module Packwerk
|
|
66
77
|
|
67
78
|
warn(warning)
|
68
79
|
end
|
69
|
-
|
70
|
-
@include = configs["include"] || DEFAULT_INCLUDE_GLOBS
|
71
|
-
@exclude = configs["exclude"] || DEFAULT_EXCLUDE_GLOBS
|
72
|
-
root = config_path ? File.dirname(config_path) : "."
|
73
|
-
@root_path = File.expand_path(root)
|
74
|
-
@package_paths = configs["package_paths"] || "**/"
|
75
|
-
@custom_associations = configs["custom_associations"] || []
|
76
|
-
@parallel = configs.key?("parallel") ? configs["parallel"] : true
|
77
|
-
|
78
|
-
@config_path = config_path
|
79
80
|
end
|
80
81
|
|
81
82
|
def load_paths
|
@@ -85,5 +86,9 @@ module Packwerk
|
|
85
86
|
def parallel?
|
86
87
|
@parallel
|
87
88
|
end
|
89
|
+
|
90
|
+
def cache_enabled?
|
91
|
+
@cache_enabled
|
92
|
+
end
|
88
93
|
end
|
89
94
|
end
|
@@ -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]
|
@@ -16,6 +16,7 @@ module Packwerk
|
|
16
16
|
@package = package
|
17
17
|
@filepath = filepath
|
18
18
|
@new_entries = T.let({}, ENTRIES_TYPE)
|
19
|
+
@deprecated_references = T.let(nil, T.nilable(ENTRIES_TYPE))
|
19
20
|
end
|
20
21
|
|
21
22
|
sig do
|
@@ -103,7 +104,6 @@ module Packwerk
|
|
103
104
|
|
104
105
|
sig { returns(ENTRIES_TYPE) }
|
105
106
|
def deprecated_references
|
106
|
-
@deprecated_references ||= T.let(@deprecated_references, T.nilable(ENTRIES_TYPE))
|
107
107
|
@deprecated_references ||= if File.exist?(@filepath)
|
108
108
|
load_yaml(@filepath)
|
109
109
|
else
|
@@ -13,8 +13,16 @@ module Packwerk
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
-
|
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)
|
17
24
|
@node_processor_factory = node_processor_factory
|
25
|
+
@cache = cache
|
18
26
|
@parser_factory = parser_factory || Packwerk::Parsers::Factory.instance
|
19
27
|
end
|
20
28
|
|
@@ -22,7 +30,7 @@ module Packwerk
|
|
22
30
|
params(file_path: String).returns(
|
23
31
|
T::Array[
|
24
32
|
T.any(
|
25
|
-
Packwerk::
|
33
|
+
Packwerk::UnresolvedReference,
|
26
34
|
Packwerk::Offense,
|
27
35
|
)
|
28
36
|
]
|
@@ -31,16 +39,22 @@ module Packwerk
|
|
31
39
|
def call(file_path)
|
32
40
|
return [UnknownFileTypeResult.new(file: file_path)] if parser_for(file_path).nil?
|
33
41
|
|
34
|
-
|
35
|
-
|
42
|
+
@cache.with_cache(file_path) do
|
43
|
+
node = parse_into_ast(file_path)
|
44
|
+
|
45
|
+
return [] unless node
|
36
46
|
|
37
|
-
|
47
|
+
references_from_ast(node, file_path)
|
48
|
+
end
|
38
49
|
rescue Parsers::ParseError => e
|
39
50
|
[e.result]
|
40
51
|
end
|
41
52
|
|
42
53
|
private
|
43
54
|
|
55
|
+
sig do
|
56
|
+
params(node: Parser::AST::Node, file_path: String).returns(T::Array[UnresolvedReference])
|
57
|
+
end
|
44
58
|
def references_from_ast(node, file_path)
|
45
59
|
references = []
|
46
60
|
|
@@ -15,3 +15,9 @@
|
|
15
15
|
# List of custom associations, if any
|
16
16
|
# custom_associations:
|
17
17
|
# - "cache_belongs_to"
|
18
|
+
|
19
|
+
# Whether or not you want the cache enabled (disabled by default)
|
20
|
+
# cache: true
|
21
|
+
|
22
|
+
# Where you want the cache to be stored (default below)
|
23
|
+
# cache_directory: 'tmp/cache/packwerk'
|
data/lib/packwerk/node.rb
CHANGED
@@ -5,7 +5,7 @@ require "parser"
|
|
5
5
|
require "parser/ast/node"
|
6
6
|
|
7
7
|
module Packwerk
|
8
|
-
# Convenience methods for working with AST nodes.
|
8
|
+
# Convenience methods for working with Parser::AST::Node nodes.
|
9
9
|
module Node
|
10
10
|
class TypeError < ArgumentError; end
|
11
11
|
Location = Struct.new(:line, :column)
|
@@ -264,6 +264,7 @@ module Packwerk
|
|
264
264
|
# "Class.new"
|
265
265
|
# "Module.new"
|
266
266
|
method_call?(node) &&
|
267
|
+
receiver(node) &&
|
267
268
|
constant?(receiver(node)) &&
|
268
269
|
["Class", "Module"].include?(constant_name(receiver(node))) &&
|
269
270
|
method_name(node) == :new
|
@@ -21,7 +21,7 @@ module Packwerk
|
|
21
21
|
params(
|
22
22
|
node: Parser::AST::Node,
|
23
23
|
ancestors: T::Array[Parser::AST::Node]
|
24
|
-
).returns(T.nilable(
|
24
|
+
).returns(T.nilable(UnresolvedReference))
|
25
25
|
end
|
26
26
|
def call(node, ancestors)
|
27
27
|
return unless Node.method_call?(node) || Node.constant?(node)
|
@@ -22,7 +22,6 @@ module Packwerk
|
|
22
22
|
sig { params(node: AST::Node).returns(::Packwerk::ReferenceExtractor) }
|
23
23
|
def reference_extractor(node:)
|
24
24
|
::Packwerk::ReferenceExtractor.new(
|
25
|
-
context_provider: context_provider,
|
26
25
|
constant_name_inspectors: constant_name_inspectors,
|
27
26
|
root_node: node,
|
28
27
|
root_path: root_path,
|
data/lib/packwerk/package.rb
CHANGED
@@ -21,6 +21,7 @@ module Packwerk
|
|
21
21
|
@name = name
|
22
22
|
@config = T.let(config || {}, T::Hash[T.untyped, T.untyped])
|
23
23
|
@dependencies = T.let(Array(@config["dependencies"]).freeze, T::Array[String])
|
24
|
+
@public_path = T.let(nil, T.nilable(String))
|
24
25
|
end
|
25
26
|
|
26
27
|
sig { returns(T.nilable(T.any(T::Boolean, T::Array[String]))) }
|
@@ -46,7 +47,6 @@ module Packwerk
|
|
46
47
|
|
47
48
|
sig { returns(String) }
|
48
49
|
def public_path
|
49
|
-
@public_path = T.let(@public_path, T.nilable(String))
|
50
50
|
@public_path ||= begin
|
51
51
|
unprefixed_public_path = user_defined_public_path || "app/public/"
|
52
52
|
|
data/lib/packwerk/package_set.rb
CHANGED
@@ -79,6 +79,7 @@ module Packwerk
|
|
79
79
|
sorted_packages = packages.sort_by { |package| -package.name.length }
|
80
80
|
packages = sorted_packages.each_with_object({}) { |package, hash| hash[package.name] = package }
|
81
81
|
@packages = T.let(packages, T::Hash[String, Package])
|
82
|
+
@package_from_path = T.let({}, T::Hash[String, T.nilable(Package)])
|
82
83
|
end
|
83
84
|
|
84
85
|
sig { override.params(blk: T.proc.params(arg0: Package).returns(T.untyped)).returns(T.untyped) }
|
@@ -94,7 +95,7 @@ module Packwerk
|
|
94
95
|
sig { params(file_path: T.any(Pathname, String)).returns(T.nilable(Package)) }
|
95
96
|
def package_from_path(file_path)
|
96
97
|
path_string = file_path.to_s
|
97
|
-
packages.values.find { |package| package.package_path?(path_string) }
|
98
|
+
@package_from_path[path_string] ||= packages.values.find { |package| package.package_path?(path_string) }
|
98
99
|
end
|
99
100
|
end
|
100
101
|
end
|
@@ -25,13 +25,13 @@ module Packwerk
|
|
25
25
|
def self.reference_qualifications(constant_name, namespace_path:)
|
26
26
|
return [constant_name] if constant_name.start_with?("::")
|
27
27
|
|
28
|
-
|
28
|
+
resolved_constant_name = "::#{constant_name}"
|
29
29
|
|
30
30
|
possible_namespaces = namespace_path.each_with_object([""]) do |current, acc|
|
31
31
|
acc << "#{acc.last}::#{current}" if acc.last && current
|
32
32
|
end
|
33
33
|
|
34
|
-
possible_namespaces.map { |namespace| namespace +
|
34
|
+
possible_namespaces.map { |namespace| namespace + resolved_constant_name }
|
35
35
|
end
|
36
36
|
|
37
37
|
private
|
@@ -53,9 +53,9 @@ module Packwerk
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def add_definition(constant_name, current_namespace_path, location)
|
56
|
-
|
56
|
+
resolved_constant = [""].concat(current_namespace_path).push(constant_name).join("::")
|
57
57
|
|
58
|
-
@local_definitions[
|
58
|
+
@local_definitions[resolved_constant] = location
|
59
59
|
end
|
60
60
|
end
|
61
61
|
end
|
@@ -8,58 +8,105 @@ module Packwerk
|
|
8
8
|
|
9
9
|
sig do
|
10
10
|
params(
|
11
|
-
context_provider: Packwerk::ConstantDiscovery,
|
12
11
|
constant_name_inspectors: T::Array[Packwerk::ConstantNameInspector],
|
13
12
|
root_node: ::AST::Node,
|
14
13
|
root_path: String,
|
15
14
|
).void
|
16
15
|
end
|
17
16
|
def initialize(
|
18
|
-
context_provider:,
|
19
17
|
constant_name_inspectors:,
|
20
18
|
root_node:,
|
21
19
|
root_path:
|
22
20
|
)
|
23
|
-
@context_provider = context_provider
|
24
21
|
@constant_name_inspectors = constant_name_inspectors
|
25
22
|
@root_path = root_path
|
26
23
|
@local_constant_definitions = ParsedConstantDefinitions.new(root_node: root_node)
|
27
24
|
end
|
28
25
|
|
26
|
+
sig do
|
27
|
+
params(
|
28
|
+
node: Parser::AST::Node,
|
29
|
+
ancestors: T::Array[Parser::AST::Node],
|
30
|
+
file_path: String
|
31
|
+
).returns(T.nilable(UnresolvedReference))
|
32
|
+
end
|
29
33
|
def reference_from_node(node, ancestors:, file_path:)
|
30
34
|
constant_name = T.let(nil, T.nilable(String))
|
31
35
|
|
32
36
|
@constant_name_inspectors.each do |inspector|
|
33
37
|
constant_name = inspector.constant_name_from_node(node, ancestors: ancestors)
|
38
|
+
|
34
39
|
break if constant_name
|
35
40
|
end
|
36
41
|
|
37
42
|
reference_from_constant(constant_name, node: node, ancestors: ancestors, file_path: file_path) if constant_name
|
38
43
|
end
|
39
44
|
|
40
|
-
|
45
|
+
sig do
|
46
|
+
params(
|
47
|
+
unresolved_references_and_offenses: T::Array[T.any(UnresolvedReference, Offense)],
|
48
|
+
context_provider: ConstantDiscovery
|
49
|
+
).returns(T::Array[T.any(Reference, Offense)])
|
50
|
+
end
|
51
|
+
def self.get_fully_qualified_references_and_offenses_from(unresolved_references_and_offenses, context_provider)
|
52
|
+
fully_qualified_references_and_offenses = T.let([], T::Array[T.any(Reference, Offense)])
|
41
53
|
|
42
|
-
|
43
|
-
|
44
|
-
|
54
|
+
unresolved_references_and_offenses.each do |unresolved_references_or_offense|
|
55
|
+
if unresolved_references_or_offense.is_a?(Offense)
|
56
|
+
fully_qualified_references_and_offenses << unresolved_references_or_offense
|
57
|
+
|
58
|
+
next
|
59
|
+
end
|
60
|
+
|
61
|
+
unresolved_reference = unresolved_references_or_offense
|
62
|
+
|
63
|
+
constant =
|
64
|
+
context_provider.context_for(
|
65
|
+
unresolved_reference.constant_name,
|
66
|
+
current_namespace_path: unresolved_reference.namespace_path
|
67
|
+
)
|
68
|
+
|
69
|
+
next if constant&.package.nil?
|
45
70
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
71
|
+
source_package = context_provider.package_from_path(unresolved_reference.relative_path)
|
72
|
+
|
73
|
+
next if source_package == constant.package
|
74
|
+
|
75
|
+
fully_qualified_references_and_offenses << Reference.new(
|
76
|
+
source_package,
|
77
|
+
unresolved_reference.relative_path,
|
78
|
+
constant,
|
79
|
+
unresolved_reference.source_location
|
50
80
|
)
|
81
|
+
end
|
51
82
|
|
52
|
-
|
83
|
+
fully_qualified_references_and_offenses
|
84
|
+
end
|
53
85
|
|
54
|
-
|
55
|
-
|
56
|
-
|
86
|
+
private
|
87
|
+
|
88
|
+
sig do
|
89
|
+
params(
|
90
|
+
constant_name: String,
|
91
|
+
node: Parser::AST::Node,
|
92
|
+
ancestors: T::Array[Parser::AST::Node],
|
93
|
+
file_path: String
|
94
|
+
).returns(T.nilable(UnresolvedReference))
|
95
|
+
end
|
96
|
+
def reference_from_constant(constant_name, node:, ancestors:, file_path:)
|
97
|
+
namespace_path = Node.enclosing_namespace_path(node, ancestors: ancestors)
|
57
98
|
|
58
|
-
|
99
|
+
return if local_reference?(constant_name, Node.name_location(node), namespace_path)
|
59
100
|
|
60
|
-
|
101
|
+
relative_path = Pathname.new(file_path).relative_path_from(@root_path).to_s
|
102
|
+
location = Node.location(node)
|
61
103
|
|
62
|
-
|
104
|
+
UnresolvedReference.new(
|
105
|
+
constant_name,
|
106
|
+
namespace_path,
|
107
|
+
relative_path,
|
108
|
+
location
|
109
|
+
)
|
63
110
|
end
|
64
111
|
|
65
112
|
def local_reference?(constant_name, name_location, namespace_path)
|
data/lib/packwerk/run_context.rb
CHANGED
@@ -25,12 +25,16 @@ module Packwerk
|
|
25
25
|
class << self
|
26
26
|
def from_configuration(configuration)
|
27
27
|
inflector = ActiveSupport::Inflector
|
28
|
+
|
28
29
|
new(
|
29
30
|
root_path: configuration.root_path,
|
30
31
|
load_paths: configuration.load_paths,
|
31
32
|
package_paths: configuration.package_paths,
|
32
33
|
inflector: inflector,
|
33
|
-
custom_associations: configuration.custom_associations
|
34
|
+
custom_associations: configuration.custom_associations,
|
35
|
+
cache_enabled: configuration.cache_enabled?,
|
36
|
+
cache_directory: configuration.cache_directory,
|
37
|
+
config_path: configuration.config_path,
|
34
38
|
)
|
35
39
|
end
|
36
40
|
end
|
@@ -41,7 +45,10 @@ module Packwerk
|
|
41
45
|
package_paths: nil,
|
42
46
|
inflector: nil,
|
43
47
|
custom_associations: [],
|
44
|
-
checker_classes: DEFAULT_CHECKERS
|
48
|
+
checker_classes: DEFAULT_CHECKERS,
|
49
|
+
cache_enabled: false,
|
50
|
+
cache_directory: nil,
|
51
|
+
config_path: nil
|
45
52
|
)
|
46
53
|
@root_path = root_path
|
47
54
|
@load_paths = load_paths
|
@@ -49,21 +56,27 @@ module Packwerk
|
|
49
56
|
@inflector = inflector
|
50
57
|
@custom_associations = custom_associations
|
51
58
|
@checker_classes = checker_classes
|
59
|
+
@cache_enabled = cache_enabled
|
60
|
+
@cache_directory = cache_directory
|
61
|
+
@config_path = config_path
|
52
62
|
end
|
53
63
|
|
54
64
|
sig { params(file: String).returns(T::Array[Packwerk::Offense]) }
|
55
65
|
def process_file(file:)
|
56
|
-
|
57
|
-
|
66
|
+
unresolved_references_and_offenses = file_processor.call(file)
|
67
|
+
references_and_offenses = ReferenceExtractor.get_fully_qualified_references_and_offenses_from(
|
68
|
+
unresolved_references_and_offenses,
|
69
|
+
context_provider
|
70
|
+
)
|
58
71
|
reference_checker = ReferenceChecking::ReferenceChecker.new(checkers)
|
59
|
-
|
72
|
+
references_and_offenses.flat_map { |reference| reference_checker.call(reference) }
|
60
73
|
end
|
61
74
|
|
62
75
|
private
|
63
76
|
|
64
77
|
sig { returns(FileProcessor) }
|
65
78
|
def file_processor
|
66
|
-
@file_processor ||= FileProcessor.new(node_processor_factory: node_processor_factory)
|
79
|
+
@file_processor ||= FileProcessor.new(node_processor_factory: node_processor_factory, cache: cache)
|
67
80
|
end
|
68
81
|
|
69
82
|
sig { returns(NodeProcessorFactory) }
|
@@ -77,7 +90,7 @@ module Packwerk
|
|
77
90
|
|
78
91
|
sig { returns(ConstantDiscovery) }
|
79
92
|
def context_provider
|
80
|
-
::Packwerk::ConstantDiscovery.new(
|
93
|
+
@context_provider ||= ::Packwerk::ConstantDiscovery.new(
|
81
94
|
constant_resolver: resolver,
|
82
95
|
packages: package_set
|
83
96
|
)
|
@@ -92,6 +105,11 @@ module Packwerk
|
|
92
105
|
)
|
93
106
|
end
|
94
107
|
|
108
|
+
sig { returns(Cache) }
|
109
|
+
def cache
|
110
|
+
@cache ||= Cache.new(enable_cache: @cache_enabled, cache_directory: @cache_directory, config_path: @config_path)
|
111
|
+
end
|
112
|
+
|
95
113
|
sig { returns(PackageSet) }
|
96
114
|
def package_set
|
97
115
|
::Packwerk::PackageSet.load_all_from(root_path, package_pathspec: package_paths)
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Packwerk
|
5
|
+
# An unresolved reference from a file in one package to a constant that may be defined in a different package.
|
6
|
+
# Unresolved means that we know how it's referred to in the file,
|
7
|
+
# and we have enough context on that reference to figure out the fully qualified reference such that we
|
8
|
+
# can produce a Reference in a separate pass. However, we have not yet resolved it to its fully qualified version.
|
9
|
+
UnresolvedReference = Struct.new(:constant_name, :namespace_path, :relative_path, :source_location)
|
10
|
+
end
|
data/lib/packwerk/version.rb
CHANGED
data/lib/packwerk.rb
CHANGED
@@ -15,6 +15,7 @@ module Packwerk
|
|
15
15
|
autoload :ApplicationValidator
|
16
16
|
autoload :AssociationInspector
|
17
17
|
autoload :OffenseCollection
|
18
|
+
autoload :Cache
|
18
19
|
autoload :Cli
|
19
20
|
autoload :Configuration
|
20
21
|
autoload :ConstantDiscovery
|
@@ -36,6 +37,7 @@ module Packwerk
|
|
36
37
|
autoload :ParsedConstantDefinitions
|
37
38
|
autoload :Parsers
|
38
39
|
autoload :ParseRun
|
40
|
+
autoload :UnresolvedReference
|
39
41
|
autoload :Reference
|
40
42
|
autoload :ReferenceExtractor
|
41
43
|
autoload :ReferenceOffense
|
data/packwerk.gemspec
CHANGED
@@ -45,6 +45,7 @@ Gem::Specification.new do |spec|
|
|
45
45
|
spec.add_dependency("parallel")
|
46
46
|
spec.add_dependency("sorbet-runtime")
|
47
47
|
spec.add_dependency("bundler")
|
48
|
+
spec.add_dependency("digest")
|
48
49
|
|
49
50
|
spec.add_development_dependency("rake")
|
50
51
|
spec.add_development_dependency("sorbet")
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: packwerk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-02-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: digest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: rake
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -222,6 +236,7 @@ files:
|
|
222
236
|
- lib/packwerk/application_load_paths.rb
|
223
237
|
- lib/packwerk/application_validator.rb
|
224
238
|
- lib/packwerk/association_inspector.rb
|
239
|
+
- lib/packwerk/cache.rb
|
225
240
|
- lib/packwerk/cli.rb
|
226
241
|
- lib/packwerk/configuration.rb
|
227
242
|
- lib/packwerk/const_node_inspector.rb
|
@@ -266,6 +281,7 @@ files:
|
|
266
281
|
- lib/packwerk/run_context.rb
|
267
282
|
- lib/packwerk/sanity_checker.rb
|
268
283
|
- lib/packwerk/spring_command.rb
|
284
|
+
- lib/packwerk/unresolved_reference.rb
|
269
285
|
- lib/packwerk/version.rb
|
270
286
|
- lib/packwerk/violation_type.rb
|
271
287
|
- library.yml
|