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
@@ -0,0 +1,37 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Commands
6
+ class LazyLoadedEntry
7
+ extend T::Sig
8
+
9
+ sig { returns(String) }
10
+ attr_reader :name
11
+
12
+ sig { params(name: String, aliases: T::Array[String]).void }
13
+ def initialize(name, aliases: [])
14
+ @name = name
15
+ @aliases = aliases
16
+ end
17
+
18
+ sig { returns(T.class_of(BaseCommand)) }
19
+ def command_class
20
+ classname = @name.sub(" ", "_").underscore.classify + "Command"
21
+ Commands.const_get(classname) # rubocop:disable Sorbet/ConstantsFromStrings
22
+ end
23
+
24
+ sig { returns(String) }
25
+ def description
26
+ command_class.description
27
+ end
28
+
29
+ sig { params(name_or_alias: String).returns(T::Boolean) }
30
+ def matches_command?(name_or_alias)
31
+ @name == name_or_alias || @aliases.include?(name_or_alias)
32
+ end
33
+ end
34
+
35
+ private_constant :LazyLoadedEntry
36
+ end
37
+ end
@@ -0,0 +1,60 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Commands
6
+ class UpdateTodoCommand < BaseCommand
7
+ extend T::Sig
8
+ include UsesParseRun
9
+
10
+ description "update package_todo.yml files"
11
+
12
+ sig { override.returns(T::Boolean) }
13
+ def run
14
+ if @files_for_processing.files_specified?
15
+ out.puts(<<~MSG.squish)
16
+ ⚠️ update-todo must be called without any file arguments.
17
+ MSG
18
+
19
+ return false
20
+ end
21
+ if @files_for_processing.files.empty?
22
+ out.puts(<<~MSG.squish)
23
+ No files found or given.
24
+ Specify files or check the include and exclude glob in the config file.
25
+ MSG
26
+
27
+ return true
28
+ end
29
+
30
+ run_context = RunContext.from_configuration(configuration)
31
+ offenses = T.let([], T::Array[Offense])
32
+ progress_formatter.started_inspection(@files_for_processing.files) do
33
+ offenses = parse_run.find_offenses(run_context, on_interrupt: -> { progress_formatter.interrupted }) do
34
+ progress_formatter.increment_progress
35
+ end
36
+ end
37
+
38
+ offense_collection = OffenseCollection.new(configuration.root_path)
39
+ offense_collection.add_offenses(offenses)
40
+ offense_collection.persist_package_todo_files(run_context.package_set)
41
+
42
+ unlisted_strict_mode_violations = offense_collection.unlisted_strict_mode_violations
43
+
44
+ messages = [
45
+ offenses_formatter.show_offenses(offense_collection.errors + unlisted_strict_mode_violations),
46
+ ]
47
+
48
+ messages << if unlisted_strict_mode_violations.any?
49
+ "⚠️ `package_todo.yml` has been updated, but unlisted strict mode violations were not added."
50
+ else
51
+ "✅ `package_todo.yml` has been updated."
52
+ end
53
+
54
+ out.puts(messages.select(&:present?).join("\n") + "\n")
55
+
56
+ unlisted_strict_mode_violations.empty? && offense_collection.errors.empty?
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,92 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+
6
+ module Packwerk
7
+ module Commands
8
+ module UsesParseRun
9
+ extend T::Sig
10
+ extend T::Helpers
11
+
12
+ requires_ancestor { BaseCommand }
13
+
14
+ sig do
15
+ params(
16
+ args: T::Array[String],
17
+ configuration: Configuration,
18
+ out: T.any(StringIO, IO),
19
+ err_out: T.any(StringIO, IO),
20
+ progress_formatter: Formatters::ProgressFormatter,
21
+ offenses_formatter: OffensesFormatter,
22
+ ).void
23
+ end
24
+ def initialize(args, configuration:, out:, err_out:, progress_formatter:, offenses_formatter:)
25
+ super
26
+ @files_for_processing = T.let(fetch_files_to_process, FilesForProcessing)
27
+ @offenses_formatter = T.let(offenses_formatter_from_options || @offenses_formatter, OffensesFormatter)
28
+ configuration.parallel = parsed_options[:parallel]
29
+ end
30
+
31
+ private
32
+
33
+ sig { returns(FilesForProcessing) }
34
+ def fetch_files_to_process
35
+ FilesForProcessing.fetch(
36
+ relative_file_paths: parsed_options[:relative_file_paths],
37
+ ignore_nested_packages: parsed_options[:ignore_nested_packages],
38
+ configuration: configuration
39
+ )
40
+ end
41
+
42
+ sig { returns(T.nilable(OffensesFormatter)) }
43
+ def offenses_formatter_from_options
44
+ OffensesFormatter.find(parsed_options[:formatter_name]) if parsed_options[:formatter_name]
45
+ end
46
+
47
+ sig { returns(ParseRun) }
48
+ def parse_run
49
+ ParseRun.new(
50
+ relative_file_set: @files_for_processing.files,
51
+ parallel: configuration.parallel?,
52
+ )
53
+ end
54
+
55
+ sig { returns(T::Hash[Symbol, T.untyped]) }
56
+ def parsed_options
57
+ return @parsed_options if @parsed_options
58
+
59
+ @parsed_options = T.let(nil, T.nilable(T::Hash[Symbol, T.untyped]))
60
+
61
+ @parsed_options = {
62
+ relative_file_paths: T.let([], T::Array[String]),
63
+ ignore_nested_packages: T.let(false, T::Boolean),
64
+ formatter_name: T.let(nil, T.nilable(String)),
65
+ parallel: T.let(configuration.parallel?, T::Boolean),
66
+ }
67
+
68
+ OptionParser.new do |parser|
69
+ parser.on("--packages=PACKAGESLIST", Array, "package names, comma separated") do |p|
70
+ @parsed_options[:relative_file_paths] = p
71
+ @parsed_options[:ignore_nested_packages] = true
72
+ end
73
+
74
+ parser.on("--offenses-formatter=FORMATTER", String,
75
+ "identifier of offenses formatter to use") do |formatter_name|
76
+ @parsed_options[:formatter_name] = formatter_name
77
+ end
78
+
79
+ parser.on("--[no-]parallel", TrueClass, "parallel processing") do |parallel|
80
+ @parsed_options[:parallel] = parallel
81
+ end
82
+ end.parse!(args)
83
+
84
+ @parsed_options[:relative_file_paths] = args if @parsed_options[:relative_file_paths].empty?
85
+
86
+ @parsed_options
87
+ end
88
+ end
89
+
90
+ private_constant :UsesParseRun
91
+ end
92
+ end
@@ -0,0 +1,46 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Commands
6
+ class ValidateCommand < BaseCommand
7
+ extend T::Sig
8
+
9
+ description "verify integrity of packwerk and package configuration"
10
+
11
+ sig { override.returns(T::Boolean) }
12
+ def run
13
+ validator_result = T.let(nil, T.nilable(Validator::Result))
14
+
15
+ progress_formatter.started_validation do
16
+ validator_result = validator.check_all(package_set, configuration)
17
+ end
18
+
19
+ validator_result = T.must(validator_result)
20
+
21
+ if validator_result.ok?
22
+ out.puts("Validation successful 🎉")
23
+ else
24
+ out.puts("Validation failed ❗\n\n#{validator_result.error_value}")
25
+ end
26
+
27
+ validator_result.ok?
28
+ end
29
+
30
+ private
31
+
32
+ sig { returns(ApplicationValidator) }
33
+ def validator
34
+ ApplicationValidator.new
35
+ end
36
+
37
+ sig { returns(PackageSet) }
38
+ def package_set
39
+ PackageSet.load_all_from(
40
+ configuration.root_path,
41
+ package_pathspec: configuration.package_paths
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Commands
6
+ class VersionCommand < BaseCommand
7
+ extend T::Sig
8
+
9
+ description "output packwerk version"
10
+
11
+ sig { override.returns(T::Boolean) }
12
+ def run
13
+ out.puts(Packwerk::VERSION)
14
+ true
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,54 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Commands
6
+ extend T::Sig
7
+ extend ActiveSupport::Autoload
8
+
9
+ autoload :BaseCommand
10
+ autoload :CheckCommand
11
+ autoload :HelpCommand
12
+ autoload :InitCommand
13
+ autoload :LazyLoadedEntry
14
+ autoload :UpdateTodoCommand
15
+ autoload :UsesParseRun
16
+ autoload :ValidateCommand
17
+ autoload :VersionCommand
18
+
19
+ class << self
20
+ extend T::Sig
21
+
22
+ sig { params(name: String, aliases: T::Array[String]).void }
23
+ def register(name, aliases: [])
24
+ registry << LazyLoadedEntry.new(name, aliases: aliases)
25
+ end
26
+
27
+ sig { params(name_or_alias: String).returns(T.nilable(T.class_of(BaseCommand))) }
28
+ def for(name_or_alias)
29
+ registry
30
+ .find { |command| command.matches_command?(name_or_alias) }
31
+ &.command_class
32
+ end
33
+
34
+ sig { returns(T::Array[LazyLoadedEntry]) }
35
+ def all
36
+ registry.dup
37
+ end
38
+
39
+ private
40
+
41
+ sig { returns(T::Array[LazyLoadedEntry]) }
42
+ def registry
43
+ @registry ||= T.let([], T.nilable(T::Array[LazyLoadedEntry]))
44
+ end
45
+ end
46
+
47
+ register("init")
48
+ register("check")
49
+ register("update-todo", aliases: ["update"])
50
+ register("validate")
51
+ register("version")
52
+ register("help")
53
+ end
54
+ end
@@ -60,6 +60,9 @@ module Packwerk
60
60
  sig { returns(Pathname) }
61
61
  attr_reader(:cache_directory)
62
62
 
63
+ sig { params(parallel: T::Boolean).returns(T::Boolean) }
64
+ attr_writer(:parallel)
65
+
63
66
  sig do
64
67
  params(
65
68
  configs: T::Hash[String, T.untyped],
@@ -72,7 +75,7 @@ module Packwerk
72
75
  root = config_path ? File.dirname(config_path) : "."
73
76
  @root_path = T.let(File.expand_path(root), String)
74
77
  @package_paths = T.let(configs["package_paths"] || "**/", T.any(String, T::Array[String]))
75
- @custom_associations = T.let(configs["custom_associations"] || [], T::Array[Symbol])
78
+ @custom_associations = T.let((configs["custom_associations"] || []).map(&:to_sym), T::Array[Symbol])
76
79
  @parallel = T.let(configs.key?("parallel") ? configs["parallel"] : true, T::Boolean)
77
80
  @cache_enabled = T.let(configs.key?("cache") ? configs["cache"] : false, T::Boolean)
78
81
  @cache_directory = T.let(Pathname.new(configs["cache_directory"] || "tmp/cache/packwerk"), Pathname)
@@ -1,7 +1,7 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "ast/node"
4
+ require "parser/ast/node"
5
5
 
6
6
  module Packwerk
7
7
  class FileProcessor
@@ -53,6 +53,17 @@ module Packwerk
53
53
  ProcessedFile.new(unresolved_references: unresolved_references)
54
54
  rescue Parsers::ParseError => e
55
55
  ProcessedFile.new(offenses: [e.result])
56
+ rescue StandardError => e
57
+ message = <<~MSG
58
+ Packwerk encountered an internal error.
59
+ For now, you can add this file to `packwerk.yml` `exclude` list.
60
+ Please file an issue and include this error message and stacktrace:
61
+
62
+ #{e.message} #{e.backtrace}"
63
+ MSG
64
+
65
+ offense = Parsers::ParseResult.new(file: relative_file, message: message)
66
+ ProcessedFile.new(offenses: [offense])
56
67
  end
57
68
 
58
69
  private
@@ -20,9 +20,9 @@ module Packwerk
20
20
  EOS
21
21
  end
22
22
 
23
- sig { override.params(offense_collection: OffenseCollection, fileset: T::Set[String]).returns(String) }
24
- def show_stale_violations(offense_collection, fileset)
25
- if offense_collection.stale_violations?(fileset)
23
+ sig { override.params(offense_collection: OffenseCollection, file_set: T::Set[String]).returns(String) }
24
+ def show_stale_violations(offense_collection, file_set)
25
+ if offense_collection.stale_violations?(file_set)
26
26
  "There were stale violations found, please run `packwerk update-todo`"
27
27
  else
28
28
  "No stale violations detected"
@@ -30,6 +30,15 @@ module Packwerk
30
30
  finished(execution_time)
31
31
  end
32
32
 
33
+ sig { params(failed: T::Boolean).void }
34
+ def increment_progress(failed = false)
35
+ if failed
36
+ mark_as_failed
37
+ else
38
+ mark_as_inspected
39
+ end
40
+ end
41
+
33
42
  sig { void }
34
43
  def mark_as_inspected
35
44
  @out.print(".")
@@ -44,6 +53,7 @@ module Packwerk
44
53
  def interrupted
45
54
  @out.puts
46
55
  @out.puts("Manually interrupted. Violations caught so far are listed below:")
56
+ @out.puts
47
57
  end
48
58
 
49
59
  private
@@ -52,6 +62,7 @@ module Packwerk
52
62
  def finished(execution_time)
53
63
  @out.puts
54
64
  @out.puts("📦 Finished in #{execution_time.round(2)} seconds")
65
+ @out.puts
55
66
  end
56
67
 
57
68
  sig { void }
@@ -1,5 +1,5 @@
1
1
  # See: Setting up the configuration file
2
- # https://github.com/Shopify/packwerk/blob/main/USAGE.md#setting-up-the-configuration-file
2
+ # https://github.com/Shopify/packwerk/blob/main/USAGE.md#configuring-packwerk
3
3
 
4
4
  # List of patterns for folder paths to include
5
5
  # include:
@@ -11,12 +11,12 @@ module Packwerk
11
11
  sig do
12
12
  params(
13
13
  root_path: String,
14
- package_todo: T::Hash[Packwerk::Package, Packwerk::PackageTodo]
14
+ package_todos: T::Hash[Packwerk::Package, Packwerk::PackageTodo]
15
15
  ).void
16
16
  end
17
- def initialize(root_path, package_todo = {})
17
+ def initialize(root_path, package_todos = {})
18
18
  @root_path = root_path
19
- @package_todo = T.let(package_todo, T::Hash[Packwerk::Package, Packwerk::PackageTodo])
19
+ @package_todos = T.let(package_todos, T::Hash[Packwerk::Package, Packwerk::PackageTodo])
20
20
  @new_violations = T.let([], T::Array[Packwerk::ReferenceOffense])
21
21
  @strict_mode_violations = T.let([], T::Array[Packwerk::ReferenceOffense])
22
22
  @errors = T.let([], T::Array[Packwerk::Offense])
@@ -38,8 +38,12 @@ module Packwerk
38
38
  def listed?(offense)
39
39
  return false unless offense.is_a?(ReferenceOffense)
40
40
 
41
- reference = offense.reference
42
- package_todo_for(reference.package).listed?(reference, violation_type: offense.violation_type)
41
+ already_listed?(offense)
42
+ end
43
+
44
+ sig { params(offenses: T::Array[Offense]).void }
45
+ def add_offenses(offenses)
46
+ offenses.each { |offense| add_offense(offense) }
43
47
  end
44
48
 
45
49
  sig do
@@ -51,16 +55,21 @@ module Packwerk
51
55
  return
52
56
  end
53
57
 
54
- if !already_listed?(offense)
55
- new_violations << offense
56
- elsif strict_mode_violation?(offense)
58
+ already_listed = already_listed?(offense)
59
+
60
+ new_violations << offense unless already_listed
61
+
62
+ if strict_mode_violation?(offense)
63
+ add_to_package_todo(offense) if already_listed
57
64
  strict_mode_violations << offense
65
+ else
66
+ add_to_package_todo(offense)
58
67
  end
59
68
  end
60
69
 
61
70
  sig { params(for_files: T::Set[String]).returns(T::Boolean) }
62
71
  def stale_violations?(for_files)
63
- @package_todo.values.any? do |package_todo|
72
+ @package_todos.values.any? do |package_todo|
64
73
  package_todo.stale_violations?(for_files)
65
74
  end
66
75
  end
@@ -76,10 +85,21 @@ module Packwerk
76
85
  errors + new_violations
77
86
  end
78
87
 
88
+ sig { returns(T::Array[Packwerk::ReferenceOffense]) }
89
+ def unlisted_strict_mode_violations
90
+ strict_mode_violations.reject { |offense| already_listed?(offense) }
91
+ end
92
+
79
93
  private
80
94
 
81
95
  sig { params(offense: ReferenceOffense).returns(T::Boolean) }
82
96
  def already_listed?(offense)
97
+ package_todo_for(offense.reference.package).listed?(offense.reference,
98
+ violation_type: offense.violation_type)
99
+ end
100
+
101
+ sig { params(offense: ReferenceOffense).returns(T::Boolean) }
102
+ def add_to_package_todo(offense)
83
103
  package_todo_for(offense.reference.package).add_entries(offense.reference,
84
104
  offense.violation_type)
85
105
  end
@@ -92,7 +112,7 @@ module Packwerk
92
112
 
93
113
  sig { params(package_set: Packwerk::PackageSet).void }
94
114
  def cleanup_extra_package_todo_files(package_set)
95
- packages_without_todos = (package_set.packages.values - @package_todo.keys)
115
+ packages_without_todos = (package_set.packages.values - @package_todos.keys)
96
116
 
97
117
  packages_without_todos.each do |package|
98
118
  Packwerk::PackageTodo.new(
@@ -104,12 +124,12 @@ module Packwerk
104
124
 
105
125
  sig { void }
106
126
  def dump_package_todo_files
107
- @package_todo.each_value(&:dump)
127
+ @package_todos.each_value(&:dump)
108
128
  end
109
129
 
110
130
  sig { params(package: Packwerk::Package).returns(Packwerk::PackageTodo) }
111
131
  def package_todo_for(package)
112
- @package_todo[package] ||= Packwerk::PackageTodo.new(
132
+ @package_todos[package] ||= Packwerk::PackageTodo.new(
113
133
  package,
114
134
  package_todo_file_for(package),
115
135
  )
@@ -22,14 +22,13 @@ module Packwerk
22
22
 
23
23
  sig { params(base: Class).void }
24
24
  def included(base)
25
- @offenses_formatters ||= T.let(@offenses_formatters, T.nilable(T::Array[Class]))
26
- @offenses_formatters ||= []
27
- @offenses_formatters << base
25
+ offenses_formatters << base
28
26
  end
29
27
 
30
28
  sig { returns(T::Array[OffensesFormatter]) }
31
29
  def all
32
- T.unsafe(@offenses_formatters).map(&:new)
30
+ load_defaults
31
+ T.cast(offenses_formatters.map(&:new), T::Array[OffensesFormatter])
33
32
  end
34
33
 
35
34
  sig { params(identifier: String).returns(OffensesFormatter) }
@@ -39,6 +38,16 @@ module Packwerk
39
38
 
40
39
  private
41
40
 
41
+ sig { void }
42
+ def load_defaults
43
+ require("packwerk/formatters/default_offenses_formatter")
44
+ end
45
+
46
+ sig { returns(T::Array[Class]) }
47
+ def offenses_formatters
48
+ @offenses_formatters ||= T.let([], T.nilable(T::Array[Class]))
49
+ end
50
+
42
51
  sig { params(name: String).returns(OffensesFormatter) }
43
52
  def formatter_by_identifier(name)
44
53
  @formatter_by_identifier ||= T.let(nil, T.nilable(T::Hash[String, T.nilable(OffensesFormatter)]))