packwerk 1.0.0 → 1.1.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/.github/workflows/ci.yml +1 -1
- data/.gitignore +1 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +5 -2
- data/README.md +5 -3
- data/TROUBLESHOOT.md +1 -1
- data/USAGE.md +56 -19
- data/exe/packwerk +1 -1
- data/lib/packwerk.rb +3 -3
- data/lib/packwerk/application_load_paths.rb +68 -0
- data/lib/packwerk/application_validator.rb +96 -70
- data/lib/packwerk/association_inspector.rb +50 -20
- data/lib/packwerk/cache_deprecated_references.rb +55 -0
- data/lib/packwerk/checker.rb +23 -0
- data/lib/packwerk/checking_deprecated_references.rb +5 -2
- data/lib/packwerk/cli.rb +65 -56
- data/lib/packwerk/commands/detect_stale_violations_command.rb +60 -0
- data/lib/packwerk/commands/offense_progress_marker.rb +24 -0
- data/lib/packwerk/commands/result.rb +13 -0
- data/lib/packwerk/commands/update_deprecations_command.rb +81 -0
- data/lib/packwerk/configuration.rb +6 -34
- data/lib/packwerk/const_node_inspector.rb +28 -17
- data/lib/packwerk/dependency_checker.rb +16 -5
- data/lib/packwerk/deprecated_references.rb +24 -1
- data/lib/packwerk/detect_stale_deprecated_references.rb +14 -0
- data/lib/packwerk/file_processor.rb +4 -4
- data/lib/packwerk/formatters/offenses_formatter.rb +48 -0
- data/lib/packwerk/formatters/progress_formatter.rb +6 -2
- data/lib/packwerk/generators/application_validation.rb +2 -2
- data/lib/packwerk/generators/templates/package.yml +4 -0
- data/lib/packwerk/generators/templates/packwerk +2 -2
- data/lib/packwerk/generators/templates/packwerk.yml.erb +1 -1
- data/lib/packwerk/inflector.rb +17 -8
- data/lib/packwerk/node.rb +78 -39
- data/lib/packwerk/node_processor.rb +14 -3
- data/lib/packwerk/node_processor_factory.rb +39 -0
- data/lib/packwerk/offense.rb +4 -6
- data/lib/packwerk/output_style.rb +20 -0
- data/lib/packwerk/output_styles/coloured.rb +29 -0
- data/lib/packwerk/output_styles/plain.rb +26 -0
- data/lib/packwerk/package.rb +8 -1
- data/lib/packwerk/package_set.rb +13 -5
- data/lib/packwerk/parsed_constant_definitions.rb +4 -4
- data/lib/packwerk/parsers/erb.rb +4 -0
- data/lib/packwerk/parsers/factory.rb +10 -1
- data/lib/packwerk/privacy_checker.rb +26 -5
- data/lib/packwerk/run_context.rb +70 -46
- data/lib/packwerk/sanity_checker.rb +1 -1
- data/lib/packwerk/spring_command.rb +1 -1
- data/lib/packwerk/updating_deprecated_references.rb +2 -39
- data/lib/packwerk/version.rb +1 -1
- data/packwerk.gemspec +2 -2
- metadata +15 -8
- data/lib/packwerk/output_styles.rb +0 -41
- 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
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
require "sorbet-runtime"
|
4
|
+
require "packwerk/formatters/progress_formatter"
|
5
|
+
|
6
|
+
module Packwerk
|
7
|
+
module OffenseProgressMarker
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig do
|
11
|
+
params(
|
12
|
+
offenses: T::Array[T.nilable(::Packwerk::Offense)],
|
13
|
+
progress_formatter: ::Packwerk::Formatters::ProgressFormatter
|
14
|
+
).void
|
15
|
+
end
|
16
|
+
def mark_progress(offenses:, progress_formatter:)
|
17
|
+
if offenses.empty?
|
18
|
+
progress_formatter.mark_as_inspected
|
19
|
+
else
|
20
|
+
progress_formatter.mark_as_failed
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
require "benchmark"
|
6
|
+
|
7
|
+
require "packwerk/commands/offense_progress_marker"
|
8
|
+
require "packwerk/commands/result"
|
9
|
+
require "packwerk/run_context"
|
10
|
+
require "packwerk/updating_deprecated_references"
|
11
|
+
|
12
|
+
module Packwerk
|
13
|
+
module Commands
|
14
|
+
class UpdateDeprecationsCommand
|
15
|
+
extend T::Sig
|
16
|
+
include OffenseProgressMarker
|
17
|
+
|
18
|
+
sig do
|
19
|
+
params(
|
20
|
+
files: T::Enumerable[String],
|
21
|
+
configuration: Configuration,
|
22
|
+
offenses_formatter: Formatters::OffensesFormatter,
|
23
|
+
progress_formatter: Formatters::ProgressFormatter
|
24
|
+
).void
|
25
|
+
end
|
26
|
+
def initialize(files:, configuration:, offenses_formatter:, progress_formatter:)
|
27
|
+
@files = files
|
28
|
+
@configuration = configuration
|
29
|
+
@progress_formatter = progress_formatter
|
30
|
+
@offenses_formatter = offenses_formatter
|
31
|
+
@updating_deprecated_references = T.let(nil, T.nilable(UpdatingDeprecatedReferences))
|
32
|
+
@run_context = T.let(nil, T.nilable(RunContext))
|
33
|
+
end
|
34
|
+
|
35
|
+
sig { returns(Result) }
|
36
|
+
def run
|
37
|
+
@progress_formatter.started(@files)
|
38
|
+
|
39
|
+
all_offenses = T.let([], T.untyped)
|
40
|
+
execution_time = Benchmark.realtime do
|
41
|
+
all_offenses = @files.flat_map do |path|
|
42
|
+
run_context.process_file(file: path).tap do |offenses|
|
43
|
+
mark_progress(offenses: offenses, progress_formatter: @progress_formatter)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
updating_deprecated_references.dump_deprecated_references_files
|
48
|
+
end
|
49
|
+
|
50
|
+
@progress_formatter.finished(execution_time)
|
51
|
+
calculate_result(all_offenses)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
sig { returns(RunContext) }
|
57
|
+
def run_context
|
58
|
+
@run_context ||= RunContext.from_configuration(
|
59
|
+
@configuration,
|
60
|
+
reference_lister: updating_deprecated_references
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { returns(UpdatingDeprecatedReferences) }
|
65
|
+
def updating_deprecated_references
|
66
|
+
@updating_deprecated_references ||= UpdatingDeprecatedReferences.new(@configuration.root_path)
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { params(all_offenses: T::Array[T.nilable(::Packwerk::Offense)]).returns(Result) }
|
70
|
+
def calculate_result(all_offenses)
|
71
|
+
result_status = all_offenses.empty?
|
72
|
+
message = <<~EOS
|
73
|
+
#{@offenses_formatter.show_offenses(all_offenses)}
|
74
|
+
✅ `deprecated_references.yml` has been updated.
|
75
|
+
EOS
|
76
|
+
|
77
|
+
Result.new(message: message, status: result_status)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -22,13 +22,16 @@ module Packwerk
|
|
22
22
|
private
|
23
23
|
|
24
24
|
def from_packwerk_config(path)
|
25
|
-
new(
|
25
|
+
new(
|
26
|
+
YAML.load_file(path) || {},
|
27
|
+
config_path: path
|
28
|
+
)
|
26
29
|
end
|
27
30
|
end
|
28
31
|
|
29
32
|
DEFAULT_CONFIG_PATH = "packwerk.yml"
|
30
33
|
DEFAULT_INCLUDE_GLOBS = ["**/*.{rb,rake,erb}"]
|
31
|
-
DEFAULT_EXCLUDE_GLOBS = ["{bin,node_modules,script,tmp}/**/*"]
|
34
|
+
DEFAULT_EXCLUDE_GLOBS = ["{bin,node_modules,script,tmp,vendor}/**/*"]
|
32
35
|
|
33
36
|
attr_reader(
|
34
37
|
:include, :exclude, :root_path, :package_paths, :custom_associations, :load_paths, :inflections_file,
|
@@ -42,41 +45,10 @@ module Packwerk
|
|
42
45
|
@root_path = File.expand_path(root)
|
43
46
|
@package_paths = configs["package_paths"] || "**/"
|
44
47
|
@custom_associations = configs["custom_associations"] || []
|
45
|
-
@load_paths = configs["load_paths"] ||
|
48
|
+
@load_paths = configs["load_paths"] || []
|
46
49
|
@inflections_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
|
47
50
|
|
48
51
|
@config_path = config_path
|
49
52
|
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
|
-
all_paths = all_paths.map do |path_string|
|
62
|
-
# ignore paths outside of the Rails root
|
63
|
-
path = Pathname.new(path_string)
|
64
|
-
if path.exist? && path.realpath.fnmatch(Rails.root.join("**").to_s)
|
65
|
-
path.relative_path_from(Rails.root).to_s
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
all_paths.compact.tap do |paths|
|
70
|
-
if paths.empty?
|
71
|
-
raise <<~EOS
|
72
|
-
No autoload paths have been set up in your Rails app. This is likely a bug, and
|
73
|
-
packwerk is unlikely to work correctly without any autoload paths.
|
74
|
-
|
75
|
-
You can follow the Rails guides on setting up load paths, or manually configure
|
76
|
-
them in `packwerk.yml` with `load_paths`.
|
77
|
-
EOS
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
53
|
end
|
82
54
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "packwerk/constant_name_inspector"
|
@@ -6,23 +6,21 @@ require "packwerk/constant_name_inspector"
|
|
6
6
|
module Packwerk
|
7
7
|
# Extracts a constant name from an AST node of type :const
|
8
8
|
class ConstNodeInspector
|
9
|
+
extend T::Sig
|
9
10
|
include ConstantNameInspector
|
10
11
|
|
12
|
+
sig do
|
13
|
+
override
|
14
|
+
.params(node: AST::Node, ancestors: T::Array[AST::Node])
|
15
|
+
.returns(T.nilable(String))
|
16
|
+
end
|
11
17
|
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`.
|
18
|
+
return nil unless Node.constant?(node)
|
16
19
|
parent = ancestors.first
|
17
|
-
return nil
|
20
|
+
return nil unless root_constant?(parent)
|
18
21
|
|
19
|
-
if constant_in_module_or_class_definition?(node, parent: parent)
|
20
|
-
|
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("::")
|
24
|
-
|
25
|
-
"::" + name
|
22
|
+
if parent && constant_in_module_or_class_definition?(node, parent: parent)
|
23
|
+
fully_qualify_constant(ancestors)
|
26
24
|
else
|
27
25
|
begin
|
28
26
|
Node.constant_name(node)
|
@@ -34,11 +32,24 @@ module Packwerk
|
|
34
32
|
|
35
33
|
private
|
36
34
|
|
35
|
+
# Only process the root `const` node for namespaced constant references. For example, in the
|
36
|
+
# reference `Spam::Eggs::Thing`, we only process the const node associated with `Spam`.
|
37
|
+
sig { params(parent: T.nilable(AST::Node)).returns(T::Boolean) }
|
38
|
+
def root_constant?(parent)
|
39
|
+
!(parent && Node.constant?(parent))
|
40
|
+
end
|
41
|
+
|
42
|
+
sig { params(node: AST::Node, parent: AST::Node).returns(T.nilable(T::Boolean)) }
|
37
43
|
def constant_in_module_or_class_definition?(node, parent:)
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
44
|
+
parent_name = Node.module_name_from_definition(parent)
|
45
|
+
parent_name && parent_name == Node.constant_name(node)
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { params(ancestors: T::Array[AST::Node]).returns(String) }
|
49
|
+
def fully_qualify_constant(ancestors)
|
50
|
+
# We're defining a class with this name, in which case the constant is implicitly fully qualified by its
|
51
|
+
# enclosing namespace
|
52
|
+
"::" + Node.parent_module_name(ancestors: ancestors)
|
42
53
|
end
|
43
54
|
end
|
44
55
|
end
|
@@ -1,22 +1,33 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "packwerk/violation_type"
|
5
|
+
require "packwerk/checker"
|
5
6
|
|
6
7
|
module Packwerk
|
7
8
|
class DependencyChecker
|
9
|
+
extend T::Sig
|
10
|
+
include Checker
|
11
|
+
|
12
|
+
sig { override.returns(ViolationType) }
|
8
13
|
def violation_type
|
9
14
|
ViolationType::Dependency
|
10
15
|
end
|
11
16
|
|
17
|
+
sig do
|
18
|
+
override
|
19
|
+
.params(reference: Packwerk::Reference, reference_lister: Packwerk::ReferenceLister)
|
20
|
+
.returns(T::Boolean)
|
21
|
+
end
|
12
22
|
def invalid_reference?(reference, reference_lister)
|
13
|
-
return unless reference.source_package
|
14
|
-
return unless reference.source_package.enforce_dependencies?
|
15
|
-
return if reference.source_package.dependency?(reference.constant.package)
|
16
|
-
return if reference_lister.listed?(reference, violation_type: violation_type)
|
23
|
+
return false unless reference.source_package
|
24
|
+
return false unless reference.source_package.enforce_dependencies?
|
25
|
+
return false if reference.source_package.dependency?(reference.constant.package)
|
26
|
+
return false if reference_lister.listed?(reference, violation_type: violation_type)
|
17
27
|
true
|
18
28
|
end
|
19
29
|
|
30
|
+
sig { override.params(reference: Packwerk::Reference).returns(String) }
|
20
31
|
def message_for(reference)
|
21
32
|
"Dependency violation: #{reference.constant.name} belongs to '#{reference.constant.package}', but " \
|
22
33
|
"'#{reference.source_package}' does not specify a dependency on " \
|
@@ -13,6 +13,7 @@ module Packwerk
|
|
13
13
|
extend T::Sig
|
14
14
|
include ReferenceLister
|
15
15
|
|
16
|
+
sig { params(package: Packwerk::Package, filepath: String).void }
|
16
17
|
def initialize(package, filepath)
|
17
18
|
@package = package
|
18
19
|
@filepath = filepath
|
@@ -34,6 +35,7 @@ module Packwerk
|
|
34
35
|
violated_constants_found.fetch("violations", []).include?(violation_type.serialize)
|
35
36
|
end
|
36
37
|
|
38
|
+
sig { params(reference: Packwerk::Reference, violation_type: String).void }
|
37
39
|
def add_entries(reference, violation_type)
|
38
40
|
package_violations = @new_entries.fetch(reference.constant.package.name, {})
|
39
41
|
entries_for_file = package_violations[reference.constant.name] ||= {}
|
@@ -47,6 +49,25 @@ module Packwerk
|
|
47
49
|
@new_entries[reference.constant.package.name] = package_violations
|
48
50
|
end
|
49
51
|
|
52
|
+
sig { returns(T::Boolean) }
|
53
|
+
def stale_violations?
|
54
|
+
prepare_entries_for_dump
|
55
|
+
deprecated_references.any? do |package, package_violations|
|
56
|
+
package_violations.any? do |constant_name, entries_for_file|
|
57
|
+
new_entries_violation_types = @new_entries.dig(package, constant_name, "violations")
|
58
|
+
return true if new_entries_violation_types.nil?
|
59
|
+
if entries_for_file["violations"].all? { |type| new_entries_violation_types.include?(type) }
|
60
|
+
stale_violations =
|
61
|
+
entries_for_file["files"] - Array(@new_entries.dig(package, constant_name, "files"))
|
62
|
+
stale_violations.present?
|
63
|
+
else
|
64
|
+
return true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
sig { void }
|
50
71
|
def dump
|
51
72
|
if @new_entries.empty?
|
52
73
|
File.delete(@filepath) if File.exist?(@filepath)
|
@@ -58,7 +79,7 @@ module Packwerk
|
|
58
79
|
#
|
59
80
|
# You can regenerate this file using the following command:
|
60
81
|
#
|
61
|
-
# bundle exec packwerk update #{@package.name}
|
82
|
+
# bundle exec packwerk update-deprecations #{@package.name}
|
62
83
|
MESSAGE
|
63
84
|
File.open(@filepath, "w") do |f|
|
64
85
|
f.write(message)
|
@@ -69,6 +90,7 @@ module Packwerk
|
|
69
90
|
|
70
91
|
private
|
71
92
|
|
93
|
+
sig { returns(Hash) }
|
72
94
|
def prepare_entries_for_dump
|
73
95
|
@new_entries.each do |package_name, package_violations|
|
74
96
|
package_violations.each do |_, entries_for_file|
|
@@ -81,6 +103,7 @@ module Packwerk
|
|
81
103
|
@new_entries = @new_entries.sort.to_h
|
82
104
|
end
|
83
105
|
|
106
|
+
sig { returns(Hash) }
|
84
107
|
def deprecated_references
|
85
108
|
@deprecated_references ||= if File.exist?(@filepath)
|
86
109
|
YAML.load_file(@filepath) || {}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "packwerk/cache_deprecated_references"
|
5
|
+
|
6
|
+
module Packwerk
|
7
|
+
class DetectStaleDeprecatedReferences < CacheDeprecatedReferences
|
8
|
+
extend T::Sig
|
9
|
+
sig { returns(T::Boolean) }
|
10
|
+
def stale_violations?
|
11
|
+
@deprecated_references.values.any?(&:stale_violations?)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
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
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "benchmark"
|
5
|
+
require "sorbet-runtime"
|
6
|
+
|
7
|
+
require "packwerk/inflector"
|
8
|
+
require "packwerk/output_style"
|
9
|
+
require "packwerk/output_styles/plain"
|
10
|
+
|
11
|
+
module Packwerk
|
12
|
+
module Formatters
|
13
|
+
class OffensesFormatter
|
14
|
+
extend T::Sig
|
15
|
+
|
16
|
+
sig { params(style: OutputStyle).void }
|
17
|
+
def initialize(style: OutputStyles::Plain.new)
|
18
|
+
@style = style
|
19
|
+
end
|
20
|
+
|
21
|
+
sig { params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
|
22
|
+
def show_offenses(offenses)
|
23
|
+
return "No offenses detected 🎉" if offenses.empty?
|
24
|
+
|
25
|
+
<<~EOS
|
26
|
+
#{offenses_list(offenses)}
|
27
|
+
#{offenses_summary(offenses)}
|
28
|
+
EOS
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
sig { params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
|
34
|
+
def offenses_list(offenses)
|
35
|
+
offenses
|
36
|
+
.compact
|
37
|
+
.map { |offense| offense.to_s(@style) }
|
38
|
+
.join("\n")
|
39
|
+
end
|
40
|
+
|
41
|
+
sig { params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
|
42
|
+
def offenses_summary(offenses)
|
43
|
+
offenses_string = Inflector.default.pluralize("offense", offenses.length)
|
44
|
+
"#{offenses.length} #{offenses_string} detected"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -4,12 +4,16 @@
|
|
4
4
|
require "benchmark"
|
5
5
|
|
6
6
|
require "packwerk/inflector"
|
7
|
-
require "packwerk/
|
7
|
+
require "packwerk/output_style"
|
8
|
+
require "packwerk/output_styles/plain"
|
8
9
|
|
9
10
|
module Packwerk
|
10
11
|
module Formatters
|
11
12
|
class ProgressFormatter
|
12
|
-
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
sig { params(out: T.any(StringIO, IO), style: OutputStyle).void }
|
16
|
+
def initialize(out, style: OutputStyles::Plain.new)
|
13
17
|
@out = out
|
14
18
|
@style = style
|
15
19
|
end
|