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
@@ -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)]))