packwerk 1.0.0 → 1.1.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/.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
|