packwerk 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +36 -8
  3. data/.ruby-version +1 -1
  4. data/Gemfile.lock +11 -6
  5. data/README.md +4 -2
  6. data/Rakefile +10 -1
  7. data/USAGE.md +6 -0
  8. data/dev.yml +1 -1
  9. data/lib/packwerk/application_validator.rb +3 -0
  10. data/lib/packwerk/checker.rb +13 -4
  11. data/lib/packwerk/cli.rb +14 -177
  12. data/lib/packwerk/commands/base_command.rb +69 -0
  13. data/lib/packwerk/commands/check_command.rb +60 -0
  14. data/lib/packwerk/commands/help_command.rb +33 -0
  15. data/lib/packwerk/commands/init_command.rb +42 -0
  16. data/lib/packwerk/commands/lazy_loaded_entry.rb +37 -0
  17. data/lib/packwerk/commands/update_todo_command.rb +60 -0
  18. data/lib/packwerk/commands/uses_parse_run.rb +92 -0
  19. data/lib/packwerk/commands/validate_command.rb +46 -0
  20. data/lib/packwerk/commands/version_command.rb +18 -0
  21. data/lib/packwerk/commands.rb +54 -0
  22. data/lib/packwerk/configuration.rb +4 -1
  23. data/lib/packwerk/file_processor.rb +12 -1
  24. data/lib/packwerk/formatters/default_offenses_formatter.rb +3 -3
  25. data/lib/packwerk/formatters/progress_formatter.rb +11 -0
  26. data/lib/packwerk/generators/templates/packwerk.yml.erb +1 -1
  27. data/lib/packwerk/offense_collection.rb +32 -12
  28. data/lib/packwerk/offenses_formatter.rb +13 -4
  29. data/lib/packwerk/package_todo.rb +87 -60
  30. data/lib/packwerk/parse_run.rb +42 -82
  31. data/lib/packwerk/validator.rb +18 -4
  32. data/lib/packwerk/version.rb +1 -1
  33. data/lib/packwerk.rb +4 -28
  34. data/sorbet/rbi/gems/parser@3.2.2.0.rbi +7250 -0
  35. metadata +14 -5
  36. data/lib/packwerk/cli/result.rb +0 -11
  37. data/sorbet/rbi/gems/parser@3.1.2.1.rbi +0 -9029
@@ -7,16 +7,20 @@ module Packwerk
7
7
  class PackageTodo
8
8
  extend T::Sig
9
9
 
10
- EntriesType = T.type_alias do
11
- T::Hash[String, T.untyped]
10
+ PackageName = T.type_alias { String }
11
+ ConstantName = T.type_alias { String }
12
+ FilePath = T.type_alias { String }
13
+ Entry = T.type_alias { T::Hash[ConstantName, T::Hash[ConstantName, T::Array[FilePath]]] }
14
+ Entries = T.type_alias do
15
+ T::Hash[PackageName, Entry]
12
16
  end
13
17
 
14
- sig { params(package: Packwerk::Package, filepath: String).void }
15
- def initialize(package, filepath)
18
+ sig { params(package: Packwerk::Package, path: String).void }
19
+ def initialize(package, path)
16
20
  @package = package
17
- @filepath = filepath
18
- @new_entries = T.let({}, EntriesType)
19
- @todo_list = T.let(nil, T.nilable(EntriesType))
21
+ @path = path
22
+ @new_entries = T.let({}, Entries)
23
+ @old_entries = T.let(nil, T.nilable(Entries))
20
24
  end
21
25
 
22
26
  sig do
@@ -24,7 +28,7 @@ module Packwerk
24
28
  .returns(T::Boolean)
25
29
  end
26
30
  def listed?(reference, violation_type:)
27
- violated_constants_found = todo_list.dig(reference.constant.package.name, reference.constant.name)
31
+ violated_constants_found = old_entries.dig(reference.constant.package.name, reference.constant.name)
28
32
  return false unless violated_constants_found
29
33
 
30
34
  violated_constant_in_file = violated_constants_found.fetch("files", []).include?(reference.relative_path)
@@ -37,16 +41,16 @@ module Packwerk
37
41
  params(reference: Packwerk::Reference, violation_type: String).returns(T::Boolean)
38
42
  end
39
43
  def add_entries(reference, violation_type)
40
- package_violations = @new_entries.fetch(reference.constant.package.name, {})
44
+ package_violations = new_entries.fetch(reference.constant.package.name, {})
41
45
  entries_for_constant = package_violations[reference.constant.name] ||= {}
42
46
 
43
47
  entries_for_constant["violations"] ||= []
44
- entries_for_constant["violations"] << violation_type
48
+ entries_for_constant.fetch("violations") << violation_type
45
49
 
46
50
  entries_for_constant["files"] ||= []
47
- entries_for_constant["files"] << reference.relative_path.to_s
51
+ entries_for_constant.fetch("files") << reference.relative_path.to_s
48
52
 
49
- @new_entries[reference.constant.package.name] = package_violations
53
+ new_entries[reference.constant.package.name] = package_violations
50
54
  listed?(reference, violation_type: violation_type)
51
55
  end
52
56
 
@@ -54,44 +58,22 @@ module Packwerk
54
58
  def stale_violations?(for_files)
55
59
  prepare_entries_for_dump
56
60
 
57
- todo_list.any? do |package, package_violations|
58
- package_violations_for_files = {}
59
- package_violations.each do |constant_name, entries_for_constant|
60
- entries_for_files = for_files & entries_for_constant["files"]
61
- next if entries_for_files.none?
62
-
63
- package_violations_for_files[constant_name] = {
64
- "violations" => entries_for_constant["violations"],
65
- "files" => entries_for_files.to_a,
66
- }
67
- end
61
+ old_entries.any? do |package, violations|
62
+ files = for_files + deleted_files_for(package)
63
+ violations_for_files = package_violations_for(violations, files: files)
68
64
 
69
65
  # We `next false` because if we cannot find existing violations for `for_files` within
70
66
  # the `package_todo.yml` file, then there are no violations that
71
67
  # can be considered stale.
72
- next false if package_violations_for_files.empty?
73
-
74
- package_violations_for_files.any? do |constant_name, entries_for_constant|
75
- new_entries_violation_types = @new_entries.dig(package, constant_name, "violations")
76
- # If there are no NEW entries that match the old entries `for_files`,
77
- # @new_entries is from the list of violations we get when we check this file.
78
- # If this list is empty, we also must have stale violations.
79
- next true if new_entries_violation_types.nil?
80
-
81
- if entries_for_constant["violations"].all? { |type| new_entries_violation_types.include?(type) }
82
- stale_violations =
83
- entries_for_constant["files"] - Array(@new_entries.dig(package, constant_name, "files"))
84
- stale_violations.any?
85
- else
86
- return true
87
- end
88
- end
68
+ next false if violations_for_files.empty?
69
+
70
+ stale_violation_for_package?(package, violations: violations_for_files)
89
71
  end
90
72
  end
91
73
 
92
74
  sig { void }
93
75
  def dump
94
- if @new_entries.empty?
76
+ if new_entries.empty?
95
77
  delete_if_exists
96
78
  else
97
79
  prepare_entries_for_dump
@@ -104,45 +86,90 @@ module Packwerk
104
86
  #
105
87
  # bin/packwerk update-todo
106
88
  MESSAGE
107
- File.open(@filepath, "w") do |f|
89
+ File.open(@path, "w") do |f|
108
90
  f.write(message)
109
- f.write(@new_entries.to_yaml)
91
+ f.write(new_entries.to_yaml)
110
92
  end
111
93
  end
112
94
  end
113
95
 
114
96
  sig { void }
115
97
  def delete_if_exists
116
- File.delete(@filepath) if File.exist?(@filepath)
98
+ File.delete(@path) if File.exist?(@path)
117
99
  end
118
100
 
119
101
  private
120
102
 
121
- sig { returns(EntriesType) }
103
+ sig { returns(Entries) }
104
+ attr_reader(:new_entries)
105
+
106
+ sig { params(package: String).returns(T::Array[String]) }
107
+ def deleted_files_for(package)
108
+ old_files = old_entries.fetch(package, {}).values.flat_map { |violation| violation.fetch("files") }
109
+ new_files = new_entries.fetch(package, {}).values.flat_map { |violation| violation.fetch("files") }
110
+ old_files - new_files
111
+ end
112
+
113
+ sig { params(package: String, violations: Entry).returns(T::Boolean) }
114
+ def stale_violation_for_package?(package, violations:)
115
+ violations.any? do |constant_name, entries_for_constant|
116
+ new_entries_violation_types = T.cast(
117
+ new_entries.dig(package, constant_name, "violations"),
118
+ T.nilable(T::Array[String]),
119
+ )
120
+ # If there are no NEW entries that match the old entries `for_files`,
121
+ # new_entries is from the list of violations we get when we check this file.
122
+ # If this list is empty, we also must have stale violations.
123
+ next true if new_entries_violation_types.nil?
124
+
125
+ if entries_for_constant.fetch("violations").all? { |type| new_entries_violation_types.include?(type) }
126
+ stale_violations =
127
+ entries_for_constant.fetch("files") - Array(new_entries.dig(package, constant_name, "files"))
128
+ stale_violations.any?
129
+ else
130
+ return true
131
+ end
132
+ end
133
+ end
134
+
135
+ sig { params(package_violations: Entry, files: T::Set[String]).returns(Entry) }
136
+ def package_violations_for(package_violations, files:)
137
+ {}.tap do |package_violations_for_files|
138
+ package_violations_for_files = T.cast(package_violations_for_files, Entry)
139
+
140
+ package_violations.each do |constant_name, entries_for_constant|
141
+ entries_for_files = files & entries_for_constant.fetch("files")
142
+ next if entries_for_files.none?
143
+
144
+ package_violations_for_files[constant_name] = {
145
+ "violations" => entries_for_constant["violations"],
146
+ "files" => entries_for_files.to_a,
147
+ }
148
+ end
149
+ end
150
+ end
151
+
152
+ sig { returns(Entries) }
122
153
  def prepare_entries_for_dump
123
- @new_entries.each do |package_name, package_violations|
154
+ new_entries.each do |package_name, package_violations|
124
155
  package_violations.each do |_, entries_for_constant|
125
- entries_for_constant["violations"].sort!.uniq!
126
- entries_for_constant["files"].sort!.uniq!
156
+ entries_for_constant.fetch("violations").sort!.uniq!
157
+ entries_for_constant.fetch("files").sort!.uniq!
127
158
  end
128
- @new_entries[package_name] = package_violations.sort.to_h
159
+ new_entries[package_name] = package_violations.sort.to_h
129
160
  end
130
161
 
131
- @new_entries = @new_entries.sort.to_h
162
+ @new_entries = new_entries.sort.to_h
132
163
  end
133
164
 
134
- sig { returns(EntriesType) }
135
- def todo_list
136
- @todo_list ||= if File.exist?(@filepath)
137
- load_yaml(@filepath)
138
- else
139
- {}
140
- end
165
+ sig { returns(Entries) }
166
+ def old_entries
167
+ @old_entries ||= load_yaml_file(@path)
141
168
  end
142
169
 
143
- sig { params(filepath: String).returns(EntriesType) }
144
- def load_yaml(filepath)
145
- YAML.load_file(filepath) || {}
170
+ sig { params(path: String).returns(Entries) }
171
+ def load_yaml_file(path)
172
+ File.exist?(path) && YAML.load_file(path) || {}
146
173
  rescue Psych::Exception
147
174
  {}
148
175
  end
@@ -14,93 +14,62 @@ module Packwerk
14
14
  sig do
15
15
  params(
16
16
  relative_file_set: FilesForProcessing::RelativeFileSet,
17
- configuration: Configuration,
18
- file_set_specified: T::Boolean,
19
- offenses_formatter: T.nilable(OffensesFormatter),
20
- progress_formatter: Formatters::ProgressFormatter,
17
+ parallel: T::Boolean,
21
18
  ).void
22
19
  end
23
- def initialize(
24
- relative_file_set:,
25
- configuration:,
26
- file_set_specified: false,
27
- offenses_formatter: nil,
28
- progress_formatter: Formatters::ProgressFormatter.new(StringIO.new)
29
- )
30
-
31
- @configuration = configuration
32
- @progress_formatter = progress_formatter
33
- @offenses_formatter = T.let(offenses_formatter || configuration.offenses_formatter, Packwerk::OffensesFormatter)
20
+ def initialize(relative_file_set:, parallel:)
34
21
  @relative_file_set = relative_file_set
35
- @file_set_specified = file_set_specified
22
+ @parallel = parallel
36
23
  end
37
24
 
38
- sig { returns(Cli::Result) }
39
- def update_todo
40
- if @file_set_specified
41
- message = <<~MSG.squish
42
- ⚠️ update-todo must be called without any file arguments.
43
- MSG
44
-
45
- return Cli::Result.new(message: message, status: false)
46
- end
47
-
48
- run_context = RunContext.from_configuration(@configuration)
49
- offense_collection = find_offenses(run_context)
50
- offense_collection.persist_package_todo_files(run_context.package_set)
51
-
52
- message = <<~EOS
53
- #{@offenses_formatter.show_offenses(offense_collection.errors)}
54
- ✅ `package_todo.yml` has been updated.
55
- EOS
56
-
57
- Cli::Result.new(message: message, status: offense_collection.errors.empty?)
25
+ sig do
26
+ params(
27
+ run_context: RunContext,
28
+ on_interrupt: T.nilable(T.proc.void),
29
+ block: T.nilable(T.proc.params(
30
+ offenses: T::Array[Packwerk::Offense],
31
+ ).void)
32
+ ).returns(T::Array[Offense])
58
33
  end
34
+ def find_offenses(run_context, on_interrupt: nil, &block)
35
+ process_file_proc = process_file_proc(run_context, &block)
59
36
 
60
- sig { returns(Cli::Result) }
61
- def check
62
- run_context = RunContext.from_configuration(@configuration)
63
- offense_collection = find_offenses(run_context, show_errors: true)
64
-
65
- messages = [
66
- @offenses_formatter.show_offenses(offense_collection.outstanding_offenses),
67
- @offenses_formatter.show_stale_violations(offense_collection, @relative_file_set),
68
- @offenses_formatter.show_strict_mode_violations(offense_collection.strict_mode_violations),
69
- ]
70
-
71
- result_status = offense_collection.outstanding_offenses.empty? &&
72
- !offense_collection.stale_violations?(@relative_file_set) && offense_collection.strict_mode_violations.empty?
37
+ offenses = if @parallel
38
+ Parallel.flat_map(@relative_file_set, &process_file_proc)
39
+ else
40
+ serial_find_offenses(on_interrupt: on_interrupt, &process_file_proc)
41
+ end
73
42
 
74
- Cli::Result.new(message: messages.select(&:present?).join("\n") + "\n", status: result_status)
43
+ offenses
75
44
  end
76
45
 
77
46
  private
78
47
 
79
- sig { params(run_context: RunContext, show_errors: T::Boolean).returns(OffenseCollection) }
80
- def find_offenses(run_context, show_errors: false)
81
- offense_collection = OffenseCollection.new(@configuration.root_path)
82
- all_offenses = T.let([], T::Array[Offense])
83
- process_file = T.let(->(relative_file) do
84
- run_context.process_file(relative_file: relative_file).tap do |offenses|
85
- failed = show_errors && offenses.any? { |offense| !offense_collection.listed?(offense) }
86
- update_progress(failed: failed)
87
- end
88
- end, ProcessFileProc)
89
-
90
- @progress_formatter.started_inspection(@relative_file_set) do
91
- all_offenses = if @configuration.parallel?
92
- Parallel.flat_map(@relative_file_set, &process_file)
93
- else
94
- serial_find_offenses(&process_file)
95
- end
48
+ sig do
49
+ params(
50
+ run_context: RunContext,
51
+ block: T.nilable(T.proc.params(offenses: T::Array[Offense]).void)
52
+ ).returns(ProcessFileProc)
53
+ end
54
+ def process_file_proc(run_context, &block)
55
+ if block
56
+ T.let(proc do |relative_file|
57
+ run_context.process_file(relative_file: relative_file).tap(&block)
58
+ end, ProcessFileProc)
59
+ else
60
+ T.let(proc do |relative_file|
61
+ run_context.process_file(relative_file: relative_file)
62
+ end, ProcessFileProc)
96
63
  end
97
-
98
- all_offenses.each { |offense| offense_collection.add_offense(offense) }
99
- offense_collection
100
64
  end
101
65
 
102
- sig { params(block: ProcessFileProc).returns(T::Array[Offense]) }
103
- def serial_find_offenses(&block)
66
+ sig do
67
+ params(
68
+ on_interrupt: T.nilable(T.proc.void),
69
+ block: ProcessFileProc
70
+ ).returns(T::Array[Offense])
71
+ end
72
+ def serial_find_offenses(on_interrupt: nil, &block)
104
73
  all_offenses = T.let([], T::Array[Offense])
105
74
  begin
106
75
  @relative_file_set.each do |relative_file|
@@ -108,20 +77,11 @@ module Packwerk
108
77
  all_offenses.concat(offenses)
109
78
  end
110
79
  rescue Interrupt
111
- @progress_formatter.interrupted
80
+ on_interrupt&.call
112
81
  all_offenses
113
82
  end
114
83
  all_offenses
115
84
  end
116
-
117
- sig { params(failed: T::Boolean).void }
118
- def update_progress(failed: false)
119
- if failed
120
- @progress_formatter.mark_as_failed
121
- else
122
- @progress_formatter.mark_as_inspected
123
- end
124
- end
125
85
  end
126
86
 
127
87
  private_constant :ParseRun
@@ -9,6 +9,9 @@ module Packwerk
9
9
  module Validator
10
10
  extend T::Sig
11
11
  extend T::Helpers
12
+ extend ActiveSupport::Autoload
13
+
14
+ autoload :Result
12
15
 
13
16
  abstract!
14
17
 
@@ -17,14 +20,25 @@ module Packwerk
17
20
 
18
21
  sig { params(base: Class).void }
19
22
  def included(base)
20
- @validators ||= T.let(@validators, T.nilable(T::Array[Class]))
21
- @validators ||= []
22
- @validators << base
23
+ validators << base
23
24
  end
24
25
 
25
26
  sig { returns(T::Array[Validator]) }
26
27
  def all
27
- T.unsafe(@validators).map(&:new)
28
+ load_defaults
29
+ T.cast(validators.map(&:new), T::Array[Validator])
30
+ end
31
+
32
+ private
33
+
34
+ sig { void }
35
+ def load_defaults
36
+ require("packwerk/validators/dependency_validator")
37
+ end
38
+
39
+ sig { returns(T::Array[Class]) }
40
+ def validators
41
+ @validators ||= T.let([], T.nilable(T::Array[Class]))
28
42
  end
29
43
  end
30
44
 
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
- VERSION = "3.0.1"
5
+ VERSION = "3.1.0"
6
6
  end
data/lib/packwerk.rb CHANGED
@@ -18,6 +18,7 @@ module Packwerk
18
18
  autoload :Cli
19
19
  autoload :Configuration
20
20
  autoload :ConstantContext
21
+ autoload :Commands
21
22
  autoload :Node
22
23
  autoload :Offense
23
24
  autoload :OffenseCollection
@@ -32,12 +33,6 @@ module Packwerk
32
33
  autoload :ReferenceOffense
33
34
  autoload :Validator
34
35
 
35
- class Cli
36
- extend ActiveSupport::Autoload
37
-
38
- autoload :Result
39
- end
40
-
41
36
  module OutputStyles
42
37
  extend ActiveSupport::Autoload
43
38
 
@@ -45,20 +40,17 @@ module Packwerk
45
40
  autoload :Plain
46
41
  end
47
42
 
48
- autoload_under "commands" do
49
- autoload :OffenseProgressMarker
50
- end
51
-
52
43
  module Formatters
53
44
  extend ActiveSupport::Autoload
54
45
 
46
+ autoload :DefaultOffensesFormatter
55
47
  autoload :ProgressFormatter
56
48
  end
57
49
 
58
- module Validator
50
+ module Validators
59
51
  extend ActiveSupport::Autoload
60
52
 
61
- autoload :Result
53
+ autoload :DependencyValidator
62
54
  end
63
55
 
64
56
  # Private APIs
@@ -101,26 +93,10 @@ module Packwerk
101
93
  extend ActiveSupport::Autoload
102
94
 
103
95
  autoload :DependencyChecker
104
- autoload :PrivacyChecker
105
96
  end
106
97
  end
107
98
 
108
99
  private_constant :ReferenceChecking
109
-
110
- class ApplicationValidator
111
- extend ActiveSupport::Autoload
112
-
113
- autoload :Helpers
114
- end
115
100
  end
116
101
 
117
102
  require "packwerk/version"
118
-
119
- # Required to register the DefaultOffensesFormatter
120
- # We put this at the *end* of the file to specify all autoloads first
121
- require "packwerk/formatters/default_offenses_formatter"
122
-
123
- # Required to register the default DependencyChecker
124
- require "packwerk/reference_checking/checkers/dependency_checker"
125
- # Required to register the default DependencyValidator
126
- require "packwerk/validators/dependency_validator"