packwerk 1.0.1 → 1.0.2
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/.github/pull_request_template.md +8 -7
- data/Gemfile +1 -0
- data/Gemfile.lock +5 -2
- data/USAGE.md +1 -1
- data/lib/packwerk.rb +0 -1
- data/lib/packwerk/application_load_paths.rb +67 -0
- data/lib/packwerk/application_validator.rb +9 -10
- data/lib/packwerk/association_inspector.rb +1 -1
- data/lib/packwerk/cli.rb +7 -4
- data/lib/packwerk/configuration.rb +1 -36
- data/lib/packwerk/const_node_inspector.rb +26 -16
- data/lib/packwerk/file_processor.rb +4 -4
- data/lib/packwerk/inflector.rb +17 -8
- data/lib/packwerk/node.rb +54 -37
- data/lib/packwerk/node_processor.rb +2 -3
- data/lib/packwerk/node_processor_factory.rb +39 -0
- data/lib/packwerk/package_set.rb +2 -1
- data/lib/packwerk/parsed_constant_definitions.rb +2 -2
- data/lib/packwerk/run_context.rb +68 -46
- data/lib/packwerk/version.rb +1 -1
- data/packwerk.gemspec +1 -1
- metadata +4 -7
- data/static/packwerk-check-demo.png +0 -0
- data/static/packwerk_check.gif +0 -0
- data/static/packwerk_check_violation.gif +0 -0
- data/static/packwerk_update.gif +0 -0
- data/static/packwerk_validate.gif +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b54c31e7c1902a03feb01304c85c9f2363b769fdb5f3c3cea7f38c67ff77af3
|
4
|
+
data.tar.gz: efefa11bf554021227ce63a876c891d5466ee7bcd173e319deb086c8ec9732a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5765e07827c5c5fc6d297f916e80c6cd2a6e09984ba91a1f2186ce1209d7965c7d906b6ff06a6b50fd37c299cdb9f82188d57273faf6e0fe7c654983ff8c0e8d
|
7
|
+
data.tar.gz: b44c1ba5e1c5b61dd2d3e4755b5c215f245cb412a05f272513a6eba83e561097d6f6e624d65f5e8fa7021479616433f0e37ca9b818f00363efd11f24f8b6bca6
|
@@ -7,21 +7,22 @@
|
|
7
7
|
## What should reviewers focus on?
|
8
8
|
|
9
9
|
|
10
|
-
|
11
10
|
## Type of Change
|
12
11
|
|
13
|
-
- [ ]
|
14
|
-
- [ ] New feature
|
15
|
-
- [ ]
|
16
|
-
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
17
|
-
- [ ] This change requires a documentation update
|
12
|
+
- [ ] Bugfix
|
13
|
+
- [ ] New feature
|
14
|
+
- [ ] Non-breaking change (a change that doesn't alter functionality - i.e., code refactor, configs, etc.)
|
18
15
|
|
19
16
|
### Additional Release Notes
|
20
17
|
|
18
|
+
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
19
|
+
|
21
20
|
Include any notes here to include in the release description. For example, if you selected "breaking change" above, leave notes on how users can transition to this version.
|
22
21
|
|
23
22
|
If no additional notes are necessary, delete this section or leave it unchanged.
|
24
23
|
|
25
24
|
## Checklist
|
26
25
|
|
27
|
-
- [ ]
|
26
|
+
- [ ] I have updated the documentation accordingly.
|
27
|
+
- [ ] I have added tests to cover my changes.
|
28
|
+
- [ ] It is safe to rollback this change.
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -85,7 +85,7 @@ GIT
|
|
85
85
|
PATH
|
86
86
|
remote: .
|
87
87
|
specs:
|
88
|
-
packwerk (1.0.
|
88
|
+
packwerk (1.0.2)
|
89
89
|
activesupport (>= 5.2)
|
90
90
|
ast
|
91
91
|
better_html
|
@@ -137,6 +137,8 @@ GEM
|
|
137
137
|
mini_mime (1.0.2)
|
138
138
|
mini_portile2 (2.4.0)
|
139
139
|
minitest (5.14.0)
|
140
|
+
minitest-focus (1.2.1)
|
141
|
+
minitest (>= 4, < 6)
|
140
142
|
mocha (1.11.2)
|
141
143
|
nio4r (2.5.2)
|
142
144
|
nokogiri (1.10.9)
|
@@ -180,7 +182,7 @@ GEM
|
|
180
182
|
smart_properties (1.15.0)
|
181
183
|
sorbet (0.5.5898)
|
182
184
|
sorbet-static (= 0.5.5898)
|
183
|
-
sorbet-runtime (0.5.
|
185
|
+
sorbet-runtime (0.5.6049)
|
184
186
|
sorbet-static (0.5.5898-universal-darwin-19)
|
185
187
|
spoom (1.0.4)
|
186
188
|
colorize
|
@@ -220,6 +222,7 @@ DEPENDENCIES
|
|
220
222
|
byebug
|
221
223
|
constant_resolver
|
222
224
|
m
|
225
|
+
minitest-focus
|
223
226
|
mocha
|
224
227
|
packwerk!
|
225
228
|
rails!
|
data/USAGE.md
CHANGED
@@ -70,7 +70,7 @@ Packwerk reads from the `packwerk.yml` configuration file in the root directory.
|
|
70
70
|
|
71
71
|
### Inflections
|
72
72
|
|
73
|
-
Packwerk requires custom inflections to be defined in `inflections.yml` instead of the traditional `inflections.rb`. This is because Packwerk accounts for custom inflections, such as acronyms, when resolving constants. Additionally, Packwerk interprets Active Record associations as references to constants. For example, `has_many :birds` is reference to the `
|
73
|
+
Packwerk requires custom inflections to be defined in `inflections.yml` instead of the traditional `inflections.rb`. This is because Packwerk accounts for custom inflections, such as acronyms, when resolving constants. Additionally, Packwerk interprets Active Record associations as references to constants. For example, `has_many :birds` is a reference to the `Bird` constant.
|
74
74
|
|
75
75
|
In order to make your custom inflections compatible with Active Support and Packwerk, you must create a `config/inflections.yml` file and point `ActiveSupport::Inflector` to that file.
|
76
76
|
|
data/lib/packwerk.rb
CHANGED
@@ -14,7 +14,6 @@ require "packwerk/cli"
|
|
14
14
|
require "packwerk/configuration"
|
15
15
|
require "packwerk/const_node_inspector"
|
16
16
|
require "packwerk/constant_discovery"
|
17
|
-
require "packwerk/constant_name_inspector"
|
18
17
|
require "packwerk/dependency_checker"
|
19
18
|
require "packwerk/deprecated_references"
|
20
19
|
require "packwerk/files_for_processing"
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler"
|
5
|
+
|
6
|
+
module Packwerk
|
7
|
+
module ApplicationLoadPaths
|
8
|
+
class << self
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { returns(T::Array[String]) }
|
12
|
+
def extract_relevant_paths
|
13
|
+
assert_application_booted
|
14
|
+
all_paths = extract_application_autoload_paths
|
15
|
+
relevant_paths = filter_relevant_paths(all_paths)
|
16
|
+
assert_load_paths_present(relevant_paths)
|
17
|
+
relative_path_strings(relevant_paths)
|
18
|
+
end
|
19
|
+
|
20
|
+
sig { void }
|
21
|
+
def assert_application_booted
|
22
|
+
raise "The application needs to be booted to extract load paths" unless defined?(::Rails)
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { returns(T::Array[String]) }
|
26
|
+
def extract_application_autoload_paths
|
27
|
+
Rails.application.railties
|
28
|
+
.select { |railtie| railtie.is_a?(Rails::Engine) }
|
29
|
+
.push(Rails.application)
|
30
|
+
.flat_map do |engine|
|
31
|
+
(engine.config.autoload_paths + engine.config.eager_load_paths + engine.config.autoload_once_paths).uniq
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
sig do
|
36
|
+
params(all_paths: T::Array[String], bundle_path: Pathname, rails_root: Pathname)
|
37
|
+
.returns(T::Array[Pathname])
|
38
|
+
end
|
39
|
+
def filter_relevant_paths(all_paths, bundle_path: Bundler.bundle_path, rails_root: Rails.root)
|
40
|
+
bundle_path_match = bundle_path.join("**")
|
41
|
+
rails_root_match = rails_root.join("**")
|
42
|
+
|
43
|
+
all_paths
|
44
|
+
.map { |path| Pathname.new(path).expand_path }
|
45
|
+
.select { |path| path.fnmatch(rails_root_match.to_s) } # path needs to be in application directory
|
46
|
+
.reject { |path| path.fnmatch(bundle_path_match.to_s) } # reject paths from vendored gems
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { params(paths: T::Array[Pathname], rails_root: Pathname).returns(T::Array[String]) }
|
50
|
+
def relative_path_strings(paths, rails_root: Rails.root)
|
51
|
+
paths
|
52
|
+
.map { |path| path.relative_path_from(rails_root).to_s }
|
53
|
+
.uniq
|
54
|
+
end
|
55
|
+
|
56
|
+
sig { params(paths: T::Array[T.untyped]).void }
|
57
|
+
def assert_load_paths_present(paths)
|
58
|
+
if paths.empty?
|
59
|
+
raise <<~EOS
|
60
|
+
We could not extract autoload paths from your Rails app. This is likely a configuration error.
|
61
|
+
Packwerk will not work correctly without any autoload paths.
|
62
|
+
EOS
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -9,15 +9,15 @@ require "yaml"
|
|
9
9
|
require "packwerk/package_set"
|
10
10
|
require "packwerk/graph"
|
11
11
|
require "packwerk/inflector"
|
12
|
+
require "packwerk/application_load_paths"
|
12
13
|
|
13
14
|
module Packwerk
|
14
15
|
class ApplicationValidator
|
15
|
-
def initialize(config_file_path:,
|
16
|
+
def initialize(config_file_path:, configuration:)
|
16
17
|
@config_file_path = config_file_path
|
17
18
|
@configuration = configuration
|
18
19
|
|
19
|
-
|
20
|
-
@application_load_paths = application_load_paths.sort.uniq
|
20
|
+
@application_load_paths = ApplicationLoadPaths.extract_relevant_paths
|
21
21
|
end
|
22
22
|
|
23
23
|
Result = Struct.new(:ok?, :error_value)
|
@@ -170,14 +170,12 @@ module Packwerk
|
|
170
170
|
def check_inflection_file
|
171
171
|
inflections_file = @configuration.inflections_file
|
172
172
|
|
173
|
-
|
174
|
-
|
175
|
-
Packwerk::Inflections::Default.apply_to(test_inflections)
|
176
|
-
Packwerk::Inflections::Custom.new(inflections_file).apply_to(test_inflections)
|
173
|
+
application_inflections = ActiveSupport::Inflector.inflections
|
174
|
+
packwerk_inflections = Packwerk::Inflector.from_file(inflections_file).inflections
|
177
175
|
|
178
176
|
results = %i(plurals singulars uncountables humans acronyms).map do |type|
|
179
|
-
expected =
|
180
|
-
actual =
|
177
|
+
expected = application_inflections.public_send(type).to_set
|
178
|
+
actual = packwerk_inflections.public_send(type).to_set
|
181
179
|
|
182
180
|
if expected == actual
|
183
181
|
Result.new(true)
|
@@ -331,7 +329,8 @@ module Packwerk
|
|
331
329
|
end
|
332
330
|
|
333
331
|
def package_manifests(glob_pattern)
|
334
|
-
PackageSet.package_paths(@configuration.root_path, glob_pattern)
|
332
|
+
PackageSet.package_paths(@configuration.root_path, glob_pattern)
|
333
|
+
.map { |f| File.realpath(f) }
|
335
334
|
end
|
336
335
|
|
337
336
|
def relative_paths(paths)
|
@@ -16,7 +16,7 @@ module Packwerk
|
|
16
16
|
has_and_belongs_to_many
|
17
17
|
).to_set
|
18
18
|
|
19
|
-
def initialize(inflector
|
19
|
+
def initialize(inflector:, custom_associations: Set.new)
|
20
20
|
@inflector = inflector
|
21
21
|
@associations = RAILS_ASSOCIATIONS + custom_associations
|
22
22
|
end
|
data/lib/packwerk/cli.rb
CHANGED
@@ -11,6 +11,7 @@ require "packwerk/inflector"
|
|
11
11
|
require "packwerk/output_styles"
|
12
12
|
require "packwerk/run_context"
|
13
13
|
require "packwerk/updating_deprecated_references"
|
14
|
+
require "packwerk/checking_deprecated_references"
|
14
15
|
|
15
16
|
module Packwerk
|
16
17
|
class Cli
|
@@ -21,7 +22,10 @@ module Packwerk
|
|
21
22
|
@err_out = err_out
|
22
23
|
@style = style
|
23
24
|
@configuration = configuration || Configuration.from_path
|
24
|
-
@run_context = run_context || Packwerk::RunContext.from_configuration(
|
25
|
+
@run_context = run_context || Packwerk::RunContext.from_configuration(
|
26
|
+
@configuration,
|
27
|
+
reference_lister: ::Packwerk::CheckingDeprecatedReferences.new(@configuration.root_path),
|
28
|
+
)
|
25
29
|
@progress_formatter = Formatters::ProgressFormatter.new(@out, style: style)
|
26
30
|
end
|
27
31
|
|
@@ -138,7 +142,7 @@ module Packwerk
|
|
138
142
|
all_offenses = T.let([], T.untyped)
|
139
143
|
execution_time = Benchmark.realtime do
|
140
144
|
all_offenses = files.flat_map do |path|
|
141
|
-
@run_context.
|
145
|
+
@run_context.process_file(file: path).tap { |offenses| mark_progress(offenses) }
|
142
146
|
end
|
143
147
|
|
144
148
|
updating_deprecated_references.dump_deprecated_references_files
|
@@ -160,7 +164,7 @@ module Packwerk
|
|
160
164
|
all_offenses = T.let([], T.untyped)
|
161
165
|
execution_time = Benchmark.realtime do
|
162
166
|
files.each do |path|
|
163
|
-
@run_context.
|
167
|
+
@run_context.process_file(file: path).tap do |offenses|
|
164
168
|
mark_progress(offenses)
|
165
169
|
all_offenses.concat(offenses)
|
166
170
|
end
|
@@ -200,7 +204,6 @@ module Packwerk
|
|
200
204
|
@progress_formatter.started_validation do
|
201
205
|
checker = Packwerk::ApplicationValidator.new(
|
202
206
|
config_file_path: @configuration.config_path,
|
203
|
-
application_load_paths: @configuration.all_application_autoload_paths,
|
204
207
|
configuration: @configuration
|
205
208
|
)
|
206
209
|
result = checker.check_all
|
@@ -42,45 +42,10 @@ module Packwerk
|
|
42
42
|
@root_path = File.expand_path(root)
|
43
43
|
@package_paths = configs["package_paths"] || "**/"
|
44
44
|
@custom_associations = configs["custom_associations"] || []
|
45
|
-
@load_paths = configs["load_paths"]
|
45
|
+
@load_paths = configs["load_paths"]
|
46
46
|
@inflections_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
|
47
47
|
|
48
48
|
@config_path = config_path
|
49
49
|
end
|
50
|
-
|
51
|
-
def all_application_autoload_paths
|
52
|
-
return [] unless defined?(::Rails)
|
53
|
-
|
54
|
-
all_paths = Rails.application.railties
|
55
|
-
.select { |railtie| railtie.is_a?(Rails::Engine) }
|
56
|
-
.push(Rails.application)
|
57
|
-
.flat_map do |engine|
|
58
|
-
(engine.config.autoload_paths + engine.config.eager_load_paths + engine.config.autoload_once_paths).uniq
|
59
|
-
end
|
60
|
-
|
61
|
-
bundle_path_match = Bundler.bundle_path.join("**").to_s
|
62
|
-
|
63
|
-
all_paths = all_paths.each_with_object([]) do |path_string, paths|
|
64
|
-
# ignore paths inside gems
|
65
|
-
path = Pathname.new(path_string)
|
66
|
-
|
67
|
-
next unless path.exist?
|
68
|
-
next if path.realpath.fnmatch(bundle_path_match)
|
69
|
-
|
70
|
-
paths << path.relative_path_from(Rails.root).to_s
|
71
|
-
end
|
72
|
-
|
73
|
-
all_paths.tap do |paths|
|
74
|
-
if paths.empty?
|
75
|
-
raise <<~EOS
|
76
|
-
No autoload paths have been set up in your Rails app. This is likely a bug, and
|
77
|
-
packwerk is unlikely to work correctly without any autoload paths.
|
78
|
-
|
79
|
-
You can follow the Rails guides on setting up load paths, or manually configure
|
80
|
-
them in `packwerk.yml` with `load_paths`.
|
81
|
-
EOS
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
50
|
end
|
86
51
|
end
|
@@ -9,20 +9,12 @@ module Packwerk
|
|
9
9
|
include ConstantNameInspector
|
10
10
|
|
11
11
|
def constant_name_from_node(node, ancestors:)
|
12
|
-
return nil unless Node.
|
13
|
-
|
14
|
-
# Only process the root `const` node for namespaced constant references. For example, in the
|
15
|
-
# reference `Spam::Eggs::Thing`, we only process the const node associated with `Spam`.
|
12
|
+
return nil unless Node.constant?(node)
|
16
13
|
parent = ancestors.first
|
17
|
-
return nil
|
18
|
-
|
19
|
-
if constant_in_module_or_class_definition?(node, parent: parent)
|
20
|
-
# We're defining a class with this name, in which case the constant is implicitly fully qualified by its
|
21
|
-
# enclosing namespace
|
22
|
-
name = Node.parent_module_name(ancestors: ancestors)
|
23
|
-
name ||= Node.enclosing_namespace_path(node, ancestors: ancestors).push(Node.constant_name(node)).join("::")
|
14
|
+
return nil unless root_constant?(parent)
|
24
15
|
|
25
|
-
|
16
|
+
if parent && constant_in_module_or_class_definition?(node, parent: parent)
|
17
|
+
fully_qualify_constant(node, ancestors: ancestors)
|
26
18
|
else
|
27
19
|
begin
|
28
20
|
Node.constant_name(node)
|
@@ -34,11 +26,29 @@ module Packwerk
|
|
34
26
|
|
35
27
|
private
|
36
28
|
|
29
|
+
# Only process the root `const` node for namespaced constant references. For example, in the
|
30
|
+
# reference `Spam::Eggs::Thing`, we only process the const node associated with `Spam`.
|
31
|
+
def root_constant?(parent)
|
32
|
+
!(parent && Node.constant?(parent))
|
33
|
+
end
|
34
|
+
|
37
35
|
def constant_in_module_or_class_definition?(node, parent:)
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
36
|
+
parent_name = Node.module_name_from_definition(parent)
|
37
|
+
parent_name && parent_name == Node.constant_name(node)
|
38
|
+
end
|
39
|
+
|
40
|
+
def fully_qualify_constant(node, ancestors:)
|
41
|
+
# We're defining a class with this name, in which case the constant is implicitly fully qualified by its
|
42
|
+
# enclosing namespace
|
43
|
+
name = Node.parent_module_name(ancestors: ancestors)
|
44
|
+
name ||= generate_qualified_constant(node, ancestors)
|
45
|
+
"::" + name
|
46
|
+
end
|
47
|
+
|
48
|
+
def generate_qualified_constant(node, ancestors:)
|
49
|
+
namespace_path = Node.enclosing_namespace_path(node, ancestors: ancestors)
|
50
|
+
constant_name = Node.constant_name(node)
|
51
|
+
namespace_path.push(constant_name).join("::")
|
42
52
|
end
|
43
53
|
end
|
44
54
|
end
|
@@ -15,8 +15,8 @@ module Packwerk
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
def initialize(
|
19
|
-
@
|
18
|
+
def initialize(node_processor_factory:, parser_factory: nil)
|
19
|
+
@node_processor_factory = node_processor_factory
|
20
20
|
@parser_factory = parser_factory || Packwerk::Parsers::Factory.instance
|
21
21
|
end
|
22
22
|
|
@@ -32,8 +32,8 @@ module Packwerk
|
|
32
32
|
|
33
33
|
result = []
|
34
34
|
if node
|
35
|
-
|
36
|
-
node_visitor = Packwerk::NodeVisitor.new(node_processor:
|
35
|
+
node_processor = @node_processor_factory.for(filename: file_path, node: node)
|
36
|
+
node_visitor = Packwerk::NodeVisitor.new(node_processor: node_processor)
|
37
37
|
|
38
38
|
node_visitor.visit(node, ancestors: [], result: result)
|
39
39
|
end
|
data/lib/packwerk/inflector.rb
CHANGED
@@ -8,20 +8,31 @@ require "packwerk/inflections/custom"
|
|
8
8
|
module Packwerk
|
9
9
|
class Inflector
|
10
10
|
class << self
|
11
|
+
extend T::Sig
|
12
|
+
|
11
13
|
def default
|
12
|
-
@default ||= new
|
14
|
+
@default ||= new(custom_inflector: Inflections::Custom.new)
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { params(inflections_file: String).returns(::Packwerk::Inflector) }
|
18
|
+
def from_file(inflections_file)
|
19
|
+
new(custom_inflector: Inflections::Custom.new(inflections_file))
|
13
20
|
end
|
14
21
|
end
|
15
22
|
|
16
|
-
|
17
|
-
include ::ActiveSupport::Inflector
|
23
|
+
extend T::Sig
|
24
|
+
include ::ActiveSupport::Inflector # For #camelize, #classify, #pluralize, #singularize
|
18
25
|
|
19
|
-
|
26
|
+
sig do
|
27
|
+
params(
|
28
|
+
custom_inflector: Inflections::Custom
|
29
|
+
).void
|
30
|
+
end
|
31
|
+
def initialize(custom_inflector:)
|
20
32
|
@inflections = ::ActiveSupport::Inflector::Inflections.new
|
21
33
|
|
22
34
|
Inflections::Default.apply_to(@inflections)
|
23
|
-
|
24
|
-
Inflections::Custom.new(custom_inflection_file).apply_to(@inflections)
|
35
|
+
custom_inflector.apply_to(@inflections)
|
25
36
|
end
|
26
37
|
|
27
38
|
def pluralize(word, count = nil)
|
@@ -32,8 +43,6 @@ module Packwerk
|
|
32
43
|
end
|
33
44
|
end
|
34
45
|
|
35
|
-
private
|
36
|
-
|
37
46
|
def inflections(_ = nil)
|
38
47
|
@inflections
|
39
48
|
end
|
data/lib/packwerk/node.rb
CHANGED
@@ -5,24 +5,12 @@ require "parser/ast/node"
|
|
5
5
|
|
6
6
|
module Packwerk
|
7
7
|
module Node
|
8
|
-
BLOCK = :block
|
9
|
-
CLASS = :class
|
10
|
-
CONSTANT = :const
|
11
|
-
CONSTANT_ASSIGNMENT = :casgn
|
12
|
-
CONSTANT_ROOT_NAMESPACE = :cbase
|
13
|
-
HASH = :hash
|
14
|
-
HASH_PAIR = :pair
|
15
|
-
METHOD_CALL = :send
|
16
|
-
MODULE = :module
|
17
|
-
STRING = :str
|
18
|
-
SYMBOL = :sym
|
19
|
-
|
20
8
|
class TypeError < ArgumentError; end
|
21
9
|
Location = Struct.new(:line, :column)
|
22
10
|
|
23
11
|
class << self
|
24
12
|
def class_or_module_name(class_or_module_node)
|
25
|
-
case
|
13
|
+
case type_of(class_or_module_node)
|
26
14
|
when CLASS, MODULE
|
27
15
|
# (class (const nil :Foo) (const nil :Bar) (nil))
|
28
16
|
# "class Foo < Bar; end"
|
@@ -36,7 +24,7 @@ module Packwerk
|
|
36
24
|
end
|
37
25
|
|
38
26
|
def constant_name(constant_node)
|
39
|
-
case
|
27
|
+
case type_of(constant_node)
|
40
28
|
when CONSTANT_ROOT_NAMESPACE
|
41
29
|
""
|
42
30
|
when CONSTANT, CONSTANT_ASSIGNMENT
|
@@ -74,19 +62,19 @@ module Packwerk
|
|
74
62
|
end
|
75
63
|
|
76
64
|
def enclosing_namespace_path(starting_node, ancestors:)
|
77
|
-
ancestors.select { |n| [CLASS, MODULE].include?(
|
65
|
+
ancestors.select { |n| [CLASS, MODULE].include?(type_of(n)) }
|
78
66
|
.each_with_object([]) do |node, namespace|
|
79
67
|
# when evaluating `class Child < Parent`, the const node for `Parent` is a child of the class
|
80
68
|
# node, so it'll be an ancestor, but `Parent` is not evaluated in the namespace of `Child`, so
|
81
69
|
# we need to skip it here
|
82
|
-
next if
|
70
|
+
next if type_of(node) == CLASS && parent_class(node) == starting_node
|
83
71
|
|
84
72
|
namespace.prepend(class_or_module_name(node))
|
85
73
|
end
|
86
74
|
end
|
87
75
|
|
88
76
|
def literal_value(string_or_symbol_node)
|
89
|
-
case
|
77
|
+
case type_of(string_or_symbol_node)
|
90
78
|
when STRING, SYMBOL
|
91
79
|
# (str "foo")
|
92
80
|
# "'foo'"
|
@@ -103,20 +91,32 @@ module Packwerk
|
|
103
91
|
Location.new(location.line, location.column)
|
104
92
|
end
|
105
93
|
|
94
|
+
def constant?(node)
|
95
|
+
type_of(node) == CONSTANT
|
96
|
+
end
|
97
|
+
|
98
|
+
def constant_assignment?(node)
|
99
|
+
type_of(node) == CONSTANT_ASSIGNMENT
|
100
|
+
end
|
101
|
+
|
102
|
+
def class?(node)
|
103
|
+
type_of(node) == CLASS
|
104
|
+
end
|
105
|
+
|
106
106
|
def method_call?(node)
|
107
|
-
|
107
|
+
type_of(node) == METHOD_CALL
|
108
108
|
end
|
109
109
|
|
110
110
|
def hash?(node)
|
111
|
-
|
111
|
+
type_of(node) == HASH
|
112
112
|
end
|
113
113
|
|
114
114
|
def string?(node)
|
115
|
-
|
115
|
+
type_of(node) == STRING
|
116
116
|
end
|
117
117
|
|
118
118
|
def symbol?(node)
|
119
|
-
|
119
|
+
type_of(node) == SYMBOL
|
120
120
|
end
|
121
121
|
|
122
122
|
def method_arguments(method_call_node)
|
@@ -136,7 +136,7 @@ module Packwerk
|
|
136
136
|
end
|
137
137
|
|
138
138
|
def module_name_from_definition(node)
|
139
|
-
case
|
139
|
+
case type_of(node)
|
140
140
|
when CLASS, MODULE
|
141
141
|
# "class My::Class; end"
|
142
142
|
# "module My::Module; end"
|
@@ -146,7 +146,7 @@ module Packwerk
|
|
146
146
|
# "My::Module = ..."
|
147
147
|
rvalue = node.children.last
|
148
148
|
|
149
|
-
case
|
149
|
+
case type_of(rvalue)
|
150
150
|
when METHOD_CALL
|
151
151
|
# "Class.new"
|
152
152
|
# "Module.new"
|
@@ -169,7 +169,7 @@ module Packwerk
|
|
169
169
|
end
|
170
170
|
|
171
171
|
def parent_class(class_node)
|
172
|
-
raise TypeError unless
|
172
|
+
raise TypeError unless type_of(class_node) == CLASS
|
173
173
|
|
174
174
|
# (class (const nil :Foo) (const nil :Bar) (nil))
|
175
175
|
# "class Foo < Bar; end"
|
@@ -178,7 +178,7 @@ module Packwerk
|
|
178
178
|
|
179
179
|
def parent_module_name(ancestors:)
|
180
180
|
definitions = ancestors
|
181
|
-
.select { |n| [CLASS, MODULE, CONSTANT_ASSIGNMENT, BLOCK].include?(
|
181
|
+
.select { |n| [CLASS, MODULE, CONSTANT_ASSIGNMENT, BLOCK].include?(type_of(n)) }
|
182
182
|
|
183
183
|
names = definitions.map do |definition|
|
184
184
|
name_part_from_definition(definition)
|
@@ -187,10 +187,6 @@ module Packwerk
|
|
187
187
|
names.empty? ? "Object" : names.reverse.join("::")
|
188
188
|
end
|
189
189
|
|
190
|
-
def type(node)
|
191
|
-
node.type
|
192
|
-
end
|
193
|
-
|
194
190
|
def value_from_hash(hash_node, key)
|
195
191
|
raise TypeError unless hash?(hash_node)
|
196
192
|
pair = hash_pairs(hash_node).detect { |pair_node| literal_value(hash_pair_key(pair_node)) == key }
|
@@ -199,8 +195,29 @@ module Packwerk
|
|
199
195
|
|
200
196
|
private
|
201
197
|
|
198
|
+
BLOCK = :block
|
199
|
+
CLASS = :class
|
200
|
+
CONSTANT = :const
|
201
|
+
CONSTANT_ASSIGNMENT = :casgn
|
202
|
+
CONSTANT_ROOT_NAMESPACE = :cbase
|
203
|
+
HASH = :hash
|
204
|
+
HASH_PAIR = :pair
|
205
|
+
METHOD_CALL = :send
|
206
|
+
MODULE = :module
|
207
|
+
STRING = :str
|
208
|
+
SYMBOL = :sym
|
209
|
+
|
210
|
+
private_constant(
|
211
|
+
:BLOCK, :CLASS, :CONSTANT, :CONSTANT_ASSIGNMENT, :CONSTANT_ROOT_NAMESPACE, :HASH, :HASH_PAIR, :METHOD_CALL,
|
212
|
+
:MODULE, :STRING, :SYMBOL,
|
213
|
+
)
|
214
|
+
|
215
|
+
def type_of(node)
|
216
|
+
node.type
|
217
|
+
end
|
218
|
+
|
202
219
|
def hash_pair_key(hash_pair_node)
|
203
|
-
raise TypeError unless
|
220
|
+
raise TypeError unless type_of(hash_pair_node) == HASH_PAIR
|
204
221
|
|
205
222
|
# (pair (int 1) (int 2))
|
206
223
|
# "1 => 2"
|
@@ -210,7 +227,7 @@ module Packwerk
|
|
210
227
|
end
|
211
228
|
|
212
229
|
def hash_pair_value(hash_pair_node)
|
213
|
-
raise TypeError unless
|
230
|
+
raise TypeError unless type_of(hash_pair_node) == HASH_PAIR
|
214
231
|
|
215
232
|
# (pair (int 1) (int 2))
|
216
233
|
# "1 => 2"
|
@@ -224,11 +241,11 @@ module Packwerk
|
|
224
241
|
|
225
242
|
# (hash (pair (int 1) (int 2)) (pair (int 3) (int 4)))
|
226
243
|
# "{1 => 2, 3 => 4}"
|
227
|
-
hash_node.children.select { |n|
|
244
|
+
hash_node.children.select { |n| type_of(n) == HASH_PAIR }
|
228
245
|
end
|
229
246
|
|
230
247
|
def method_call_node(block_node)
|
231
|
-
raise TypeError unless
|
248
|
+
raise TypeError unless type_of(block_node) == BLOCK
|
232
249
|
|
233
250
|
# (block (send (lvar :foo) :bar) (args) (int 42))
|
234
251
|
# "foo.bar do 42 end"
|
@@ -239,7 +256,7 @@ module Packwerk
|
|
239
256
|
# "Class.new"
|
240
257
|
# "Module.new"
|
241
258
|
method_call?(node) &&
|
242
|
-
|
259
|
+
constant?(receiver(node)) &&
|
243
260
|
["Class", "Module"].include?(constant_name(receiver(node))) &&
|
244
261
|
method_name(node) == :new
|
245
262
|
end
|
@@ -247,12 +264,12 @@ module Packwerk
|
|
247
264
|
def name_from_block_definition(node)
|
248
265
|
if method_name(method_call_node(node)) == :class_eval
|
249
266
|
receiver = receiver(node)
|
250
|
-
constant_name(receiver) if receiver &&
|
267
|
+
constant_name(receiver) if receiver && constant?(receiver)
|
251
268
|
end
|
252
269
|
end
|
253
270
|
|
254
271
|
def name_part_from_definition(node)
|
255
|
-
case
|
272
|
+
case type_of(node)
|
256
273
|
when CLASS, MODULE, CONSTANT_ASSIGNMENT
|
257
274
|
module_name_from_definition(node)
|
258
275
|
when BLOCK
|
@@ -261,7 +278,7 @@ module Packwerk
|
|
261
278
|
end
|
262
279
|
|
263
280
|
def receiver(method_call_or_block_node)
|
264
|
-
case
|
281
|
+
case type_of(method_call_or_block_node)
|
265
282
|
when METHOD_CALL
|
266
283
|
method_call_or_block_node.children[0]
|
267
284
|
when BLOCK
|
@@ -26,8 +26,7 @@ module Packwerk
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def call(node, ancestors:)
|
29
|
-
|
30
|
-
when Node::METHOD_CALL, Node::CONSTANT
|
29
|
+
if Node.method_call?(node) || Node.constant?(node)
|
31
30
|
reference = @reference_extractor.reference_from_node(node, ancestors: ancestors, file_path: @filename)
|
32
31
|
check_reference(reference, node) if reference
|
33
32
|
end
|
@@ -42,7 +41,7 @@ module Packwerk
|
|
42
41
|
|
43
42
|
Packwerk::Offense.new(
|
44
43
|
location: Node.location(node),
|
45
|
-
file:
|
44
|
+
file: reference.relative_path,
|
46
45
|
message: <<~EOS
|
47
46
|
#{message}
|
48
47
|
Inference details: this is a reference to #{constant.name} which seems to be defined in #{constant.location}.
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "packwerk/constant_name_inspector"
|
5
|
+
require "packwerk/checker"
|
6
|
+
|
7
|
+
module Packwerk
|
8
|
+
class NodeProcessorFactory < T::Struct
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
const :root_path, String
|
12
|
+
const :reference_lister, ReferenceLister
|
13
|
+
const :context_provider, Packwerk::ConstantDiscovery
|
14
|
+
const :constant_name_inspectors, T::Array[ConstantNameInspector]
|
15
|
+
const :checkers, T::Array[Checker]
|
16
|
+
|
17
|
+
sig { params(filename: String, node: AST::Node).returns(NodeProcessor) }
|
18
|
+
def for(filename:, node:)
|
19
|
+
::Packwerk::NodeProcessor.new(
|
20
|
+
reference_extractor: reference_extractor(node: node),
|
21
|
+
reference_lister: reference_lister,
|
22
|
+
filename: filename,
|
23
|
+
checkers: checkers,
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
sig { params(node: AST::Node).returns(::Packwerk::ReferenceExtractor) }
|
30
|
+
def reference_extractor(node:)
|
31
|
+
::Packwerk::ReferenceExtractor.new(
|
32
|
+
context_provider: context_provider,
|
33
|
+
constant_name_inspectors: constant_name_inspectors,
|
34
|
+
root_node: node,
|
35
|
+
root_path: root_path,
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/packwerk/package_set.rb
CHANGED
@@ -29,7 +29,8 @@ module Packwerk
|
|
29
29
|
bundle_path_match = Bundler.bundle_path.join("**").to_s
|
30
30
|
|
31
31
|
Dir.glob(File.join(root_path, package_pathspec, PACKAGE_CONFIG_FILENAME))
|
32
|
-
.map { |path| Pathname.new(path) }
|
32
|
+
.map { |path| Pathname.new(path) }
|
33
|
+
.reject { |path| path.realpath.fnmatch(bundle_path_match) }
|
33
34
|
end
|
34
35
|
|
35
36
|
private
|
@@ -38,12 +38,12 @@ module Packwerk
|
|
38
38
|
private
|
39
39
|
|
40
40
|
def collect_local_definitions_from_root(node, current_namespace_path = [])
|
41
|
-
if Node.
|
41
|
+
if Node.constant_assignment?(node)
|
42
42
|
add_definition(Node.constant_name(node), current_namespace_path, Node.name_location(node))
|
43
43
|
elsif Node.module_name_from_definition(node)
|
44
44
|
# handle compact constant nesting (e.g. "module Sales::Order")
|
45
45
|
tempnode = node
|
46
|
-
while (tempnode = Node.each_child(tempnode).find { |n| Node.
|
46
|
+
while (tempnode = Node.each_child(tempnode).find { |n| Node.constant?(n) })
|
47
47
|
add_definition(Node.constant_name(tempnode), current_namespace_path, Node.name_location(tempnode))
|
48
48
|
end
|
49
49
|
|
data/lib/packwerk/run_context.rb
CHANGED
@@ -1,30 +1,31 @@
|
|
1
1
|
# typed: true
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require "active_support/inflector"
|
5
4
|
require "constant_resolver"
|
6
5
|
|
7
6
|
require "packwerk/association_inspector"
|
8
|
-
require "packwerk/checking_deprecated_references"
|
9
7
|
require "packwerk/constant_discovery"
|
10
8
|
require "packwerk/const_node_inspector"
|
11
9
|
require "packwerk/dependency_checker"
|
12
10
|
require "packwerk/file_processor"
|
13
|
-
require "packwerk/
|
11
|
+
require "packwerk/inflector"
|
14
12
|
require "packwerk/package_set"
|
15
13
|
require "packwerk/privacy_checker"
|
16
14
|
require "packwerk/reference_extractor"
|
15
|
+
require "packwerk/node_processor_factory"
|
17
16
|
|
18
17
|
module Packwerk
|
19
18
|
class RunContext
|
19
|
+
extend T::Sig
|
20
|
+
|
20
21
|
attr_reader(
|
21
|
-
:checkers,
|
22
|
-
:constant_name_inspectors,
|
23
|
-
:context_provider,
|
24
22
|
:root_path,
|
25
|
-
:
|
26
|
-
:
|
27
|
-
:
|
23
|
+
:load_paths,
|
24
|
+
:package_paths,
|
25
|
+
:inflector,
|
26
|
+
:custom_associations,
|
27
|
+
:checker_classes,
|
28
|
+
:reference_lister,
|
28
29
|
)
|
29
30
|
|
30
31
|
DEFAULT_CHECKERS = [
|
@@ -33,16 +34,15 @@ module Packwerk
|
|
33
34
|
]
|
34
35
|
|
35
36
|
class << self
|
36
|
-
def from_configuration(configuration, reference_lister:
|
37
|
-
|
38
|
-
::Packwerk::CheckingDeprecatedReferences.new(configuration.root_path)
|
37
|
+
def from_configuration(configuration, reference_lister:)
|
38
|
+
inflector = ::Packwerk::Inflector.from_file(configuration.inflections_file)
|
39
39
|
new(
|
40
40
|
root_path: configuration.root_path,
|
41
41
|
load_paths: configuration.load_paths,
|
42
42
|
package_paths: configuration.package_paths,
|
43
|
-
inflector:
|
43
|
+
inflector: inflector,
|
44
44
|
custom_associations: configuration.custom_associations,
|
45
|
-
reference_lister:
|
45
|
+
reference_lister: reference_lister,
|
46
46
|
)
|
47
47
|
end
|
48
48
|
end
|
@@ -54,51 +54,73 @@ module Packwerk
|
|
54
54
|
inflector: nil,
|
55
55
|
custom_associations: [],
|
56
56
|
checker_classes: DEFAULT_CHECKERS,
|
57
|
-
|
58
|
-
reference_lister: nil
|
57
|
+
reference_lister:
|
59
58
|
)
|
60
|
-
@root_path =
|
59
|
+
@root_path = root_path
|
60
|
+
@load_paths = load_paths
|
61
|
+
@package_paths = package_paths
|
62
|
+
@inflector = inflector
|
63
|
+
@custom_associations = custom_associations
|
64
|
+
@checker_classes = checker_classes
|
65
|
+
@reference_lister = reference_lister
|
66
|
+
end
|
61
67
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
68
|
+
sig { params(file: String).returns(T::Array[T.nilable(::Packwerk::Offense)]) }
|
69
|
+
def process_file(file:)
|
70
|
+
file_processor.call(file)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
67
74
|
|
68
|
-
|
75
|
+
sig { returns(FileProcessor) }
|
76
|
+
def file_processor
|
77
|
+
@file_processor ||= FileProcessor.new(node_processor_factory: node_processor_factory)
|
78
|
+
end
|
69
79
|
|
70
|
-
|
80
|
+
sig { returns(NodeProcessorFactory) }
|
81
|
+
def node_processor_factory
|
82
|
+
NodeProcessorFactory.new(
|
83
|
+
context_provider: context_provider,
|
84
|
+
checkers: checkers,
|
85
|
+
root_path: root_path,
|
86
|
+
constant_name_inspectors: constant_name_inspectors,
|
87
|
+
reference_lister: reference_lister
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
sig { returns(ConstantDiscovery) }
|
92
|
+
def context_provider
|
93
|
+
::Packwerk::ConstantDiscovery.new(
|
71
94
|
constant_resolver: resolver,
|
72
95
|
packages: package_set
|
73
96
|
)
|
97
|
+
end
|
98
|
+
|
99
|
+
sig { returns(ConstantResolver) }
|
100
|
+
def resolver
|
101
|
+
ConstantResolver.new(
|
102
|
+
root_path: root_path,
|
103
|
+
load_paths: load_paths,
|
104
|
+
inflector: inflector,
|
105
|
+
)
|
106
|
+
end
|
74
107
|
|
75
|
-
|
108
|
+
sig { returns(PackageSet) }
|
109
|
+
def package_set
|
110
|
+
::Packwerk::PackageSet.load_all_from(root_path, package_pathspec: package_paths)
|
111
|
+
end
|
76
112
|
|
77
|
-
|
113
|
+
sig { returns(T::Array[Checker]) }
|
114
|
+
def checkers
|
115
|
+
checker_classes.map(&:new)
|
116
|
+
end
|
78
117
|
|
79
|
-
|
118
|
+
sig { returns(T::Array[ConstantNameInspector]) }
|
119
|
+
def constant_name_inspectors
|
120
|
+
[
|
80
121
|
::Packwerk::ConstNodeInspector.new,
|
81
122
|
::Packwerk::AssociationInspector.new(inflector: inflector, custom_associations: custom_associations),
|
82
123
|
]
|
83
|
-
|
84
|
-
@node_processor_class = node_processor_class
|
85
|
-
@file_processor = FileProcessor.new(run_context: self)
|
86
|
-
end
|
87
|
-
|
88
|
-
def node_processor_for(filename:, ast_node:)
|
89
|
-
reference_extractor = ::Packwerk::ReferenceExtractor.new(
|
90
|
-
context_provider: context_provider,
|
91
|
-
constant_name_inspectors: constant_name_inspectors,
|
92
|
-
root_node: ast_node,
|
93
|
-
root_path: root_path,
|
94
|
-
)
|
95
|
-
|
96
|
-
node_processor_class.new(
|
97
|
-
reference_extractor: reference_extractor,
|
98
|
-
reference_lister: @reference_lister,
|
99
|
-
filename: filename,
|
100
|
-
checkers: checkers,
|
101
|
-
)
|
102
124
|
end
|
103
125
|
end
|
104
126
|
end
|
data/lib/packwerk/version.rb
CHANGED
data/packwerk.gemspec
CHANGED
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.executables << "packwerk"
|
35
35
|
|
36
36
|
spec.files = Dir.chdir(__dir__) do
|
37
|
-
%x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
37
|
+
%x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features|static)/}) }
|
38
38
|
end
|
39
39
|
spec.require_paths = %w(lib)
|
40
40
|
|
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: 1.0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -183,6 +183,7 @@ files:
|
|
183
183
|
- docs/cohesion.png
|
184
184
|
- exe/packwerk
|
185
185
|
- lib/packwerk.rb
|
186
|
+
- lib/packwerk/application_load_paths.rb
|
186
187
|
- lib/packwerk/application_validator.rb
|
187
188
|
- lib/packwerk/association_inspector.rb
|
188
189
|
- lib/packwerk/checker.rb
|
@@ -212,6 +213,7 @@ files:
|
|
212
213
|
- lib/packwerk/inflector.rb
|
213
214
|
- lib/packwerk/node.rb
|
214
215
|
- lib/packwerk/node_processor.rb
|
216
|
+
- lib/packwerk/node_processor_factory.rb
|
215
217
|
- lib/packwerk/node_visitor.rb
|
216
218
|
- lib/packwerk/offense.rb
|
217
219
|
- lib/packwerk/output_styles.rb
|
@@ -307,11 +309,6 @@ files:
|
|
307
309
|
- sorbet/rbi/gems/websocket-extensions@0.1.4.rbi
|
308
310
|
- sorbet/rbi/gems/zeitwerk@2.3.0.rbi
|
309
311
|
- sorbet/tapioca/require.rb
|
310
|
-
- static/packwerk-check-demo.png
|
311
|
-
- static/packwerk_check.gif
|
312
|
-
- static/packwerk_check_violation.gif
|
313
|
-
- static/packwerk_update.gif
|
314
|
-
- static/packwerk_validate.gif
|
315
312
|
homepage: https://github.com/Shopify/packwerk
|
316
313
|
licenses:
|
317
314
|
- MIT
|
Binary file
|
data/static/packwerk_check.gif
DELETED
Binary file
|
Binary file
|
data/static/packwerk_update.gif
DELETED
Binary file
|
Binary file
|