packwerk 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|