packwerk 3.0.1 → 3.1.0

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