packwerk 3.0.0 → 3.2.2

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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/exe/packwerk +4 -1
  4. data/lib/packwerk/application_validator.rb +3 -0
  5. data/lib/packwerk/association_inspector.rb +17 -4
  6. data/lib/packwerk/checker.rb +16 -7
  7. data/lib/packwerk/cli.rb +14 -177
  8. data/lib/packwerk/commands/base_command.rb +69 -0
  9. data/lib/packwerk/commands/check_command.rb +62 -0
  10. data/lib/packwerk/commands/help_command.rb +33 -0
  11. data/lib/packwerk/commands/init_command.rb +42 -0
  12. data/lib/packwerk/commands/lazy_loaded_entry.rb +37 -0
  13. data/lib/packwerk/commands/update_todo_command.rb +60 -0
  14. data/lib/packwerk/commands/uses_parse_run.rb +92 -0
  15. data/lib/packwerk/commands/validate_command.rb +46 -0
  16. data/lib/packwerk/commands/version_command.rb +18 -0
  17. data/lib/packwerk/commands.rb +54 -0
  18. data/lib/packwerk/configuration.rb +8 -1
  19. data/lib/packwerk/const_node_inspector.rb +2 -2
  20. data/lib/packwerk/constant_name_inspector.rb +2 -2
  21. data/lib/packwerk/file_processor.rb +12 -1
  22. data/lib/packwerk/formatters/default_offenses_formatter.rb +3 -3
  23. data/lib/packwerk/formatters/progress_formatter.rb +11 -0
  24. data/lib/packwerk/generators/templates/package.yml +2 -2
  25. data/lib/packwerk/generators/templates/packwerk.yml.erb +1 -1
  26. data/lib/packwerk/offense_collection.rb +32 -12
  27. data/lib/packwerk/offenses_formatter.rb +14 -5
  28. data/lib/packwerk/package.rb +1 -1
  29. data/lib/packwerk/package_todo.rb +86 -69
  30. data/lib/packwerk/parse_run.rb +42 -82
  31. data/lib/packwerk/parsers/factory.rb +3 -3
  32. data/lib/packwerk/parsers/ruby.rb +9 -2
  33. data/lib/packwerk/reference_checking/checkers/dependency_checker.rb +3 -3
  34. data/lib/packwerk/reference_extractor.rb +29 -1
  35. data/lib/packwerk/reference_offense.rb +1 -1
  36. data/lib/packwerk/run_context.rb +15 -4
  37. data/lib/packwerk/spring_command.rb +0 -2
  38. data/lib/packwerk/validator.rb +19 -5
  39. data/lib/packwerk/version.rb +1 -1
  40. data/lib/packwerk.rb +5 -28
  41. data/sorbet/config +1 -0
  42. data/sorbet/rbi/gems/actionpack@7.0.3.1.rbi +3280 -3450
  43. data/sorbet/rbi/gems/actionview@7.0.3.1.rbi +2322 -1782
  44. data/sorbet/rbi/gems/activesupport@7.0.3.1.rbi +2654 -3268
  45. data/sorbet/rbi/gems/ast@2.4.2.rbi +535 -6
  46. data/sorbet/rbi/gems/better_html@2.0.1.rbi +529 -0
  47. data/sorbet/rbi/gems/builder@3.2.4.rbi +4 -4
  48. data/sorbet/rbi/gems/byebug@11.1.3.rbi +32 -4
  49. data/sorbet/rbi/gems/concurrent-ruby@1.1.10.rbi +1750 -1840
  50. data/sorbet/rbi/gems/constant_resolver@0.2.0.rbi +15 -15
  51. data/sorbet/rbi/gems/crass@1.0.6.rbi +489 -5
  52. data/sorbet/rbi/gems/erubi@1.11.0.rbi +24 -21
  53. data/sorbet/rbi/gems/i18n@1.12.0.rbi +395 -395
  54. data/sorbet/rbi/gems/json@2.6.2.rbi +70 -77
  55. data/sorbet/rbi/gems/language_server-protocol@3.16.0.3.rbi +1 -1
  56. data/sorbet/rbi/gems/loofah@2.18.0.rbi +134 -134
  57. data/sorbet/rbi/gems/m@1.6.0.rbi +60 -60
  58. data/sorbet/rbi/gems/method_source@1.1.0.rbi +303 -0
  59. data/sorbet/rbi/gems/minitest-focus@1.3.1.rbi +22 -28
  60. data/sorbet/rbi/gems/minitest@5.16.2.rbi +384 -396
  61. data/sorbet/rbi/gems/mocha@1.14.0.rbi +589 -589
  62. data/sorbet/rbi/gems/netrc@0.11.0.rbi +37 -32
  63. data/sorbet/rbi/gems/{nokogiri@1.13.8.rbi → nokogiri@1.15.3.rbi} +1869 -1030
  64. data/sorbet/rbi/gems/{parallel@1.22.1.rbi → parallel@1.24.0.rbi} +85 -82
  65. data/sorbet/rbi/gems/parser@3.3.1.0.rbi +7320 -0
  66. data/sorbet/rbi/gems/prettier_print@0.1.0.rbi +1 -1
  67. data/sorbet/rbi/gems/prism@0.27.0.rbi +36983 -0
  68. data/sorbet/rbi/gems/{racc@1.6.0.rbi → racc@1.7.1.rbi} +42 -33
  69. data/sorbet/rbi/gems/rack-test@2.0.2.rbi +148 -338
  70. data/sorbet/rbi/gems/rack@2.2.4.rbi +1079 -1130
  71. data/sorbet/rbi/gems/rails-dom-testing@2.0.3.rbi +354 -22
  72. data/sorbet/rbi/gems/rails-html-sanitizer@1.4.3.rbi +113 -259
  73. data/sorbet/rbi/gems/railties@7.0.3.1.rbi +642 -638
  74. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +109 -99
  75. data/sorbet/rbi/gems/rake@13.0.6.rbi +714 -599
  76. data/sorbet/rbi/gems/{rbi@0.0.15.rbi → rbi@0.1.12.rbi} +865 -801
  77. data/sorbet/rbi/gems/regexp_parser@2.5.0.rbi +853 -870
  78. data/sorbet/rbi/gems/rexml@3.2.5.rbi +480 -477
  79. data/sorbet/rbi/gems/rubocop-ast@1.21.0.rbi +1621 -1622
  80. data/sorbet/rbi/gems/rubocop-performance@1.14.3.rbi +507 -526
  81. data/sorbet/rbi/gems/rubocop-shopify@2.9.0.rbi +1 -1
  82. data/sorbet/rbi/gems/rubocop-sorbet@0.6.11.rbi +186 -203
  83. data/sorbet/rbi/gems/rubocop@1.34.1.rbi +8126 -8367
  84. data/sorbet/rbi/gems/{ruby-lsp@0.2.1.rbi → ruby-lsp@0.2.3.rbi} +2 -2
  85. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1235 -4
  86. data/sorbet/rbi/gems/smart_properties@1.17.0.rbi +90 -90
  87. data/sorbet/rbi/gems/spoom@1.3.2.rbi +4420 -0
  88. data/sorbet/rbi/gems/spring@4.0.0.rbi +104 -104
  89. data/sorbet/rbi/gems/syntax_tree@3.3.0.rbi +1 -1
  90. data/sorbet/rbi/gems/{tapioca@0.9.2.rbi → tapioca@0.13.3.rbi} +1596 -1253
  91. data/sorbet/rbi/gems/{thor@1.2.1.rbi → thor@1.3.1.rbi} +1047 -652
  92. data/sorbet/rbi/gems/tzinfo@2.0.5.rbi +531 -513
  93. data/sorbet/rbi/gems/unicode-display_width@2.2.0.rbi +13 -13
  94. data/sorbet/rbi/gems/{yard-sorbet@0.6.1.rbi → yard-sorbet@0.8.1.rbi} +132 -92
  95. data/sorbet/rbi/gems/{yard@0.9.28.rbi → yard@0.9.36.rbi} +3158 -3067
  96. data/sorbet/rbi/gems/zeitwerk@2.6.4.rbi +149 -145
  97. metadata +36 -84
  98. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  99. data/.github/pull_request_template.md +0 -28
  100. data/.github/workflows/ci.yml +0 -65
  101. data/.github/workflows/cla.yml +0 -22
  102. data/.gitignore +0 -13
  103. data/.rubocop.yml +0 -75
  104. data/.ruby-version +0 -1
  105. data/CODEOWNERS +0 -1
  106. data/CODE_OF_CONDUCT.md +0 -76
  107. data/CONTRIBUTING.md +0 -17
  108. data/Gemfile +0 -27
  109. data/Gemfile.lock +0 -201
  110. data/RESOLVING_VIOLATIONS.md +0 -81
  111. data/Rakefile +0 -13
  112. data/TROUBLESHOOT.md +0 -45
  113. data/UPGRADING.md +0 -54
  114. data/USAGE.md +0 -367
  115. data/bin/console +0 -15
  116. data/bin/m +0 -29
  117. data/bin/rake +0 -29
  118. data/bin/rubocop +0 -29
  119. data/bin/setup +0 -8
  120. data/bin/srb +0 -29
  121. data/bin/tapioca +0 -29
  122. data/dev.yml +0 -32
  123. data/docs/cohesion.png +0 -0
  124. data/gemfiles/Gemfile-rails-6-0 +0 -22
  125. data/gemfiles/Gemfile-rails-6-1 +0 -22
  126. data/lib/packwerk/cli/result.rb +0 -11
  127. data/packwerk.gemspec +0 -58
  128. data/shipit.rubygems.yml +0 -5
  129. data/sorbet/rbi/gems/actioncable@7.0.3.1.rbi +0 -2754
  130. data/sorbet/rbi/gems/actionmailbox@7.0.3.1.rbi +0 -1496
  131. data/sorbet/rbi/gems/actionmailer@7.0.3.1.rbi +0 -2362
  132. data/sorbet/rbi/gems/actiontext@7.0.3.1.rbi +0 -1569
  133. data/sorbet/rbi/gems/activejob@7.0.3.1.rbi +0 -2553
  134. data/sorbet/rbi/gems/activemodel@7.0.3.1.rbi +0 -5999
  135. data/sorbet/rbi/gems/activerecord@7.0.3.1.rbi +0 -37832
  136. data/sorbet/rbi/gems/activestorage@7.0.3.1.rbi +0 -2321
  137. data/sorbet/rbi/gems/better_html@1.0.16.rbi +0 -317
  138. data/sorbet/rbi/gems/coderay@1.1.3.rbi +0 -8
  139. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +0 -1079
  140. data/sorbet/rbi/gems/digest@3.1.0.rbi +0 -189
  141. data/sorbet/rbi/gems/globalid@1.0.0.rbi +0 -572
  142. data/sorbet/rbi/gems/mail@2.7.1.rbi +0 -2490
  143. data/sorbet/rbi/gems/marcel@1.0.2.rbi +0 -220
  144. data/sorbet/rbi/gems/method_source@1.0.0.rbi +0 -76
  145. data/sorbet/rbi/gems/mini_mime@1.1.2.rbi +0 -170
  146. data/sorbet/rbi/gems/net-imap@0.2.3.rbi +0 -2147
  147. data/sorbet/rbi/gems/net-pop@0.1.1.rbi +0 -926
  148. data/sorbet/rbi/gems/net-protocol@0.1.3.rbi +0 -11
  149. data/sorbet/rbi/gems/net-smtp@0.3.1.rbi +0 -1108
  150. data/sorbet/rbi/gems/nio4r@2.5.8.rbi +0 -292
  151. data/sorbet/rbi/gems/parser@3.1.2.1.rbi +0 -9029
  152. data/sorbet/rbi/gems/pry@0.14.1.rbi +0 -8
  153. data/sorbet/rbi/gems/rails@7.0.3.1.rbi +0 -8
  154. data/sorbet/rbi/gems/spoom@1.1.11.rbi +0 -2181
  155. data/sorbet/rbi/gems/strscan@3.0.4.rbi +0 -8
  156. data/sorbet/rbi/gems/timeout@0.3.0.rbi +0 -142
  157. data/sorbet/rbi/gems/unparser@0.6.5.rbi +0 -4529
  158. data/sorbet/rbi/gems/webrick@1.7.0.rbi +0 -2582
  159. data/sorbet/rbi/gems/websocket-driver@0.7.5.rbi +0 -993
  160. data/sorbet/rbi/gems/websocket-extensions@0.1.5.rbi +0 -71
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11206a06e6ac99c1fb8ef4cdefbe334133317d0599fce6480d4dfe10f694c747
4
- data.tar.gz: 606778530b8a872d331e5f331b9c6e14cb37ff6e0a5e13247bb0aebe1ca9e0cc
3
+ metadata.gz: 9172cfed0b1190af4ece801d73138cdaba7cd9aaad8fdb90537b2d6a03d597bb
4
+ data.tar.gz: 946267b723dff18a00a2948d4f9d5c8766018ae815b50fa542ba450bdc29c718
5
5
  SHA512:
6
- metadata.gz: 66166a9c683483fda99341adc79c807c40f8190c349c8650b8646810e06d11f9b4728e2e938ef0abf640967d0764d78b66e956aba3bdaef830c4a15253711542
7
- data.tar.gz: c1cce5df16246f1561cd0954abca4685e21adcaf52d3f6d164acd776f9466a0ad276a65b9a2f5cc9e358b058fcdc2cef265c2b3948043f9f3e35b1d4798ec5e9
6
+ metadata.gz: a7ec7b0f03a55f713b24d9aec7e6cadfb7138087685ee698f7a35c64209638168d91ee7de44a174543271b4252173007d64a16b0bd5f00348dd0554a76c34bb1
7
+ data.tar.gz: ccc7785fcef8f061923c72f122dbf7f7c9ebab1e7c4cc9d2e6c2a8a643ac0da8f7594c17c2823b2bcc7845dd255d886a6b85eabe0ce68176a8b1ff57097c7278
data/README.md CHANGED
@@ -58,10 +58,12 @@ Read [USAGE.md](USAGE.md) for usage once Packwerk is installed on your project.
58
58
  Various third parties have built tooling on top of packwerk. Here's a selection of some that might prove useful:
59
59
 
60
60
  - https://github.com/bellroy/graphwerk draws a graph of your package dependencies
61
- - https://github.com/Gusto/packwerk-vscode integrates packwerk into Visual Studio Code so you can see violations right in your editor
62
- - https://github.com/Gusto/stimpack sets up Rails autoloading, as well as `rspec` and `FactoryBot` integration, for packages arranged in a flat list. Stimpack is quite convenient, but for autoloading we recommend to use `Rails::Engine`s instead.
61
+ - https://github.com/rubyatscale/packwerk-vscode integrates packwerk into Visual Studio Code so you can see violations right in your editor
62
+ - https://github.com/vinted/packwerk-intellij integrates packwerk into RubyMine so you can see violations right in your editor
63
+ - https://github.com/rubyatscale/packs-rails sets up Rails autoloading, as well as `rspec` and `FactoryBot` integration, for packages arranged in a flat list. packs-rails is quite convenient, but for autoloading we recommend to use `Rails::Engine`s instead.
63
64
  - https://github.com/rubyatscale/danger-packwerk integrates packwerk with [danger.systems](https://danger.systems) to provide packwerk feedback as Github inline PR comments
64
65
  - https://github.com/rubyatscale/packwerk-extensions contains extensions for packwerk, including a checker for packwerk that allows you to enforce public API boundaries. This was originally extracted from `packwerk` itself.
66
+ - https://github.com/alexevanczuk/packs is a Rust implementation of packwerk that has experimental support for non-Rails, non-Zeitwerk applications.
65
67
 
66
68
  ## Development
67
69
 
data/exe/packwerk CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "packwerk/disable_sorbet"
4
+ unless defined?(Spring)
5
+ require "packwerk/disable_sorbet"
6
+ end
7
+
5
8
  require "packwerk"
6
9
 
7
10
  # Needs to be run in test environment in order to have test helper paths available in the autoload paths
@@ -11,6 +11,9 @@ module Packwerk
11
11
  class ApplicationValidator
12
12
  include Validator
13
13
  extend T::Sig
14
+ extend ActiveSupport::Autoload
15
+
16
+ autoload :Helpers
14
17
 
15
18
  sig { params(package_set: PackageSet, configuration: Configuration).returns(Validator::Result) }
16
19
  def check_all(package_set, configuration)
@@ -19,19 +19,27 @@ module Packwerk
19
19
  CustomAssociations
20
20
  )
21
21
 
22
- sig { params(inflector: T.class_of(ActiveSupport::Inflector), custom_associations: CustomAssociations).void }
23
- def initialize(inflector:, custom_associations: Set.new)
22
+ sig do
23
+ params(
24
+ inflector: T.class_of(ActiveSupport::Inflector),
25
+ custom_associations: CustomAssociations,
26
+ excluded_files: T::Set[String]
27
+ ).void
28
+ end
29
+ def initialize(inflector:, custom_associations: Set.new, excluded_files: Set.new)
24
30
  @inflector = inflector
25
31
  @associations = T.let(RAILS_ASSOCIATIONS + custom_associations, CustomAssociations)
32
+ @excluded_files = T.let(excluded_files, T::Set[String])
26
33
  end
27
34
 
28
35
  sig do
29
36
  override
30
- .params(node: AST::Node, ancestors: T::Array[AST::Node])
37
+ .params(node: AST::Node, ancestors: T::Array[AST::Node], relative_file: String)
31
38
  .returns(T.nilable(String))
32
39
  end
33
- def constant_name_from_node(node, ancestors:)
40
+ def constant_name_from_node(node, ancestors:, relative_file:)
34
41
  return unless NodeHelpers.method_call?(node)
42
+ return if excluded?(relative_file)
35
43
  return unless association?(node)
36
44
 
37
45
  arguments = NodeHelpers.method_arguments(node)
@@ -48,6 +56,11 @@ module Packwerk
48
56
 
49
57
  private
50
58
 
59
+ sig { params(relative_file: String).returns(T::Boolean) }
60
+ def excluded?(relative_file)
61
+ @excluded_files.include?(relative_file)
62
+ end
63
+
51
64
  sig { params(node: AST::Node).returns(T::Boolean) }
52
65
  def association?(node)
53
66
  method_name = NodeHelpers.method_name(node)
@@ -11,16 +11,15 @@ module Packwerk
11
11
  class << self
12
12
  extend T::Sig
13
13
 
14
- sig { params(base: Class).void }
14
+ sig { params(base: T::Class[T.anything]).void }
15
15
  def included(base)
16
- @checkers ||= T.let(@checkers, T.nilable(T::Array[Class]))
17
- @checkers ||= []
18
- @checkers << base
16
+ checkers << base
19
17
  end
20
18
 
21
19
  sig { returns(T::Array[Checker]) }
22
20
  def all
23
- T.unsafe(@checkers).map(&:new)
21
+ load_defaults
22
+ T.cast(checkers.map(&:new), T::Array[Checker])
24
23
  end
25
24
 
26
25
  sig { params(violation_type: String).returns(Checker) }
@@ -30,6 +29,16 @@ module Packwerk
30
29
 
31
30
  private
32
31
 
32
+ sig { void }
33
+ def load_defaults
34
+ require("packwerk/reference_checking/checkers/dependency_checker")
35
+ end
36
+
37
+ sig { returns(T::Array[T::Class[T.anything]]) }
38
+ def checkers
39
+ @checkers ||= T.let([], T.nilable(T::Array[T::Class[T.anything]]))
40
+ end
41
+
33
42
  sig { params(name: String).returns(Checker) }
34
43
  def checker_by_violation_type(name)
35
44
  @checker_by_violation_type ||= T.let(Checker.all.to_h do |checker|
@@ -42,8 +51,8 @@ module Packwerk
42
51
  sig { abstract.returns(String) }
43
52
  def violation_type; end
44
53
 
45
- sig { abstract.params(listed_offense: ReferenceOffense).returns(T::Boolean) }
46
- def strict_mode_violation?(listed_offense); end
54
+ sig { abstract.params(offense: ReferenceOffense).returns(T::Boolean) }
55
+ def strict_mode_violation?(offense); end
47
56
 
48
57
  sig { abstract.params(reference: Reference).returns(T::Boolean) }
49
58
  def invalid_reference?(reference); end
data/lib/packwerk/cli.rb CHANGED
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "optparse"
5
-
6
4
  module Packwerk
7
5
  # A command-line interface to Packwerk.
8
6
  class Cli
@@ -46,184 +44,23 @@ module Packwerk
46
44
 
47
45
  sig { params(args: T::Array[String]).returns(T::Boolean) }
48
46
  def execute_command(args)
49
- subcommand = args.shift
50
- case subcommand
51
- when "init"
52
- init
53
- when "check"
54
- output_result(parse_run(args).check)
55
- when "update-todo", "update"
56
- output_result(parse_run(args).update_todo)
57
- when "validate"
58
- validate(args)
59
- when "version"
60
- @out.puts(Packwerk::VERSION)
61
- true
62
- when nil, "help"
63
- usage
64
- else
65
- @err_out.puts(
66
- "'#{subcommand}' is not a packwerk command. See `packwerk help`."
67
- )
68
- false
69
- end
70
- end
71
-
72
- private
73
-
74
- sig { returns(T::Boolean) }
75
- def init
76
- @out.puts("📦 Initializing Packwerk...")
77
-
78
- generate_configs
79
- end
80
-
81
- sig { returns(T::Boolean) }
82
- def generate_configs
83
- configuration_file = Generators::ConfigurationFile.generate(
84
- root: @configuration.root_path,
85
- out: @out
86
- )
87
-
88
- root_package = Generators::RootPackage.generate(root: @configuration.root_path, out: @out)
89
-
90
- success = configuration_file && root_package
91
-
92
- result = if success
93
- <<~EOS
94
-
95
- 🎉 Packwerk is ready to be used. You can start defining packages and run `bin/packwerk check`.
96
- For more information on how to use Packwerk, see: https://github.com/Shopify/packwerk/blob/main/USAGE.md
97
- EOS
98
- else
99
- <<~EOS
100
-
101
- ⚠️ Packwerk is not ready to be used.
102
- Please check output and refer to https://github.com/Shopify/packwerk/blob/main/USAGE.md for more information.
103
- EOS
104
- end
105
-
106
- @out.puts(result)
107
- success
108
- end
109
-
110
- sig { returns(T::Boolean) }
111
- def usage
112
- @err_out.puts(<<~USAGE)
113
- Usage: #{$PROGRAM_NAME} <subcommand>
114
-
115
- Subcommands:
116
- init - set up packwerk
117
- check - run all checks
118
- update-todo - update package_todo.yml files
119
- validate - verify integrity of packwerk and package configuration
120
- version - output packwerk version
121
- help - display help information about packwerk
122
- USAGE
123
- true
124
- end
125
-
126
- sig { params(result: Result).returns(T::Boolean) }
127
- def output_result(result)
128
- @out.puts
129
- @out.puts(result.message)
130
- result.status
131
- end
132
-
133
- sig do
134
- params(
135
- relative_file_paths: T::Array[String],
136
- ignore_nested_packages: T::Boolean
137
- ).returns(FilesForProcessing)
138
- end
139
- def fetch_files_to_process(relative_file_paths, ignore_nested_packages)
140
- files_for_processing = FilesForProcessing.fetch(
141
- relative_file_paths: relative_file_paths,
142
- ignore_nested_packages: ignore_nested_packages,
143
- configuration: @configuration
144
- )
145
- @out.puts(<<~MSG.squish) if files_for_processing.files.empty?
146
- No files found or given.
147
- Specify files or check the include and exclude glob in the config file.
148
- MSG
149
-
150
- files_for_processing
151
- end
152
-
153
- sig { params(_paths: T::Array[String]).returns(T::Boolean) }
154
- def validate(_paths)
155
- result = T.let(nil, T.nilable(Validator::Result))
156
-
157
- @progress_formatter.started_validation do
158
- result = validator.check_all(package_set, @configuration)
159
-
160
- list_validation_errors(result)
161
- end
162
-
163
- T.must(result).ok?
164
- end
165
-
166
- sig { returns(ApplicationValidator) }
167
- def validator
168
- ApplicationValidator.new
169
- end
170
-
171
- sig { returns(PackageSet) }
172
- def package_set
173
- PackageSet.load_all_from(
174
- @configuration.root_path,
175
- package_pathspec: @configuration.package_paths
176
- )
177
- end
178
-
179
- sig { params(result: Validator::Result).void }
180
- def list_validation_errors(result)
181
- @out.puts
182
- if result.ok?
183
- @out.puts("Validation successful 🎉")
47
+ command = args.shift || "help"
48
+ command_class = Commands.for(command)
49
+
50
+ if command_class
51
+ command_class.new(
52
+ args,
53
+ configuration: @configuration,
54
+ out: @out,
55
+ err_out: @err_out,
56
+ progress_formatter: @progress_formatter,
57
+ offenses_formatter: @offenses_formatter,
58
+ ).run
184
59
  else
185
- @out.puts("Validation failed ")
186
- @out.puts
187
- @out.puts(result.error_value)
188
- end
189
- end
190
-
191
- sig { params(args: T::Array[String]).returns(ParseRun) }
192
- def parse_run(args)
193
- relative_file_paths = T.let([], T::Array[String])
194
- ignore_nested_packages = nil
195
- formatter = @offenses_formatter
60
+ @err_out.puts("'#{command}' is not a packwerk command. See `packwerk help`.",)
196
61
 
197
- if args.any? { |arg| arg.include?("--packages") }
198
- OptionParser.new do |parser|
199
- parser.on("--packages=PACKAGESLIST", Array, "package names, comma separated") do |p|
200
- relative_file_paths = p
201
- end
202
- end.parse!(args)
203
- ignore_nested_packages = true
204
- else
205
- relative_file_paths = args
206
- ignore_nested_packages = false
207
- end
208
-
209
- if args.any? { |arg| arg.include?("--offenses-formatter") }
210
- OptionParser.new do |parser|
211
- parser.on("--offenses-formatter=FORMATTER", String,
212
- "identifier of offenses formatter to use") do |formatter_identifier|
213
- formatter = OffensesFormatter.find(formatter_identifier)
214
- end
215
- end.parse!(args)
62
+ false
216
63
  end
217
-
218
- files_for_processing = fetch_files_to_process(relative_file_paths, ignore_nested_packages)
219
-
220
- ParseRun.new(
221
- relative_file_set: files_for_processing.files,
222
- file_set_specified: files_for_processing.files_specified?,
223
- configuration: @configuration,
224
- progress_formatter: @progress_formatter,
225
- offenses_formatter: formatter
226
- )
227
64
  end
228
65
  end
229
66
  end
@@ -0,0 +1,69 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Commands
6
+ class BaseCommand
7
+ extend T::Sig
8
+ extend T::Helpers
9
+ abstract!
10
+
11
+ @description = T.let("", String)
12
+
13
+ class << self
14
+ extend T::Sig
15
+
16
+ sig { params(description: T.nilable(String)).returns(String) }
17
+ def description(description = nil)
18
+ if description
19
+ @description = description
20
+ else
21
+ @description
22
+ end
23
+ end
24
+ end
25
+
26
+ sig do
27
+ params(
28
+ args: T::Array[String],
29
+ configuration: Configuration,
30
+ out: T.any(StringIO, IO),
31
+ err_out: T.any(StringIO, IO),
32
+ progress_formatter: Formatters::ProgressFormatter,
33
+ offenses_formatter: OffensesFormatter,
34
+ ).void
35
+ end
36
+ def initialize(args, configuration:, out:, err_out:, progress_formatter:, offenses_formatter:)
37
+ @args = args
38
+ @configuration = configuration
39
+ @out = out
40
+ @err_out = err_out
41
+ @progress_formatter = progress_formatter
42
+ @offenses_formatter = offenses_formatter
43
+ end
44
+
45
+ sig { abstract.returns(T::Boolean) }
46
+ def run; end
47
+
48
+ private
49
+
50
+ sig { returns(T::Array[String]) }
51
+ attr_reader :args
52
+
53
+ sig { returns(Configuration) }
54
+ attr_reader :configuration
55
+
56
+ sig { returns(T.any(StringIO, IO)) }
57
+ attr_reader :out
58
+
59
+ sig { returns(T.any(StringIO, IO)) }
60
+ attr_reader :err_out
61
+
62
+ sig { returns(Formatters::ProgressFormatter) }
63
+ attr_reader :progress_formatter
64
+
65
+ sig { returns(OffensesFormatter) }
66
+ attr_reader :offenses_formatter
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,62 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Commands
6
+ class CheckCommand < BaseCommand
7
+ extend T::Sig
8
+ include UsesParseRun
9
+
10
+ description "run all checks"
11
+
12
+ sig { override.returns(T::Boolean) }
13
+ def run
14
+ if @files_for_processing.files.empty?
15
+ out.puts(<<~MSG.squish)
16
+ No files found or given.
17
+ Specify files or check the include and exclude glob in the config file.
18
+ MSG
19
+
20
+ true
21
+ end
22
+
23
+ all_offenses = T.let([], T::Array[Offense])
24
+ on_interrupt = T.let(-> { progress_formatter.interrupted }, T.proc.void)
25
+
26
+ progress_formatter.started_inspection(@files_for_processing.files) do
27
+ all_offenses = parse_run.find_offenses(run_context, on_interrupt: on_interrupt) do |offenses|
28
+ failed = offenses.any? { |offense| !offense_collection.listed?(offense) }
29
+ progress_formatter.increment_progress(failed)
30
+ end
31
+ end
32
+ offense_collection.add_offenses(all_offenses)
33
+
34
+ unlisted_strict_mode_violations = offense_collection.unlisted_strict_mode_violations
35
+
36
+ messages = [
37
+ offenses_formatter.show_offenses(offense_collection.outstanding_offenses),
38
+ offenses_formatter.show_stale_violations(offense_collection, @files_for_processing.files),
39
+ offenses_formatter.show_strict_mode_violations(unlisted_strict_mode_violations),
40
+ ]
41
+
42
+ out.puts(messages.select(&:present?).join("\n") + "\n")
43
+
44
+ offense_collection.outstanding_offenses.empty? &&
45
+ !offense_collection.stale_violations?(@files_for_processing.files) &&
46
+ unlisted_strict_mode_violations.empty?
47
+ end
48
+
49
+ private
50
+
51
+ sig { returns(RunContext) }
52
+ def run_context
53
+ @run_context ||= T.let(RunContext.from_configuration(configuration), T.nilable(RunContext))
54
+ end
55
+
56
+ sig { returns(OffenseCollection) }
57
+ def offense_collection
58
+ @offense_collection ||= T.let(OffenseCollection.new(configuration.root_path), T.nilable(OffenseCollection))
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Commands
6
+ class HelpCommand < BaseCommand
7
+ extend T::Sig
8
+
9
+ description "display help information about packwerk"
10
+
11
+ sig { override.returns(T::Boolean) }
12
+ def run
13
+ err_out.puts(<<~USAGE)
14
+ Usage: #{$PROGRAM_NAME} <subcommand>
15
+
16
+ Subcommands:
17
+ #{command_help_lines}
18
+ USAGE
19
+
20
+ true
21
+ end
22
+
23
+ private
24
+
25
+ sig { returns(String) }
26
+ def command_help_lines
27
+ Commands.all.map do |command|
28
+ " #{command.name} - #{command.description}"
29
+ end.join("\n")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Commands
6
+ class InitCommand < BaseCommand
7
+ extend T::Sig
8
+
9
+ description "set up packwerk"
10
+
11
+ sig { override.returns(T::Boolean) }
12
+ def run
13
+ out.puts("📦 Initializing Packwerk...")
14
+
15
+ configuration_file = Generators::ConfigurationFile.generate(
16
+ root: configuration.root_path,
17
+ out: out
18
+ )
19
+
20
+ root_package = Generators::RootPackage.generate(root: configuration.root_path, out: out)
21
+
22
+ success = configuration_file && root_package
23
+
24
+ if success
25
+ out.puts(<<~EOS)
26
+
27
+ 🎉 Packwerk is ready to be used. You can start defining packages and run `bin/packwerk check`.
28
+ For more information on how to use Packwerk, see: https://github.com/Shopify/packwerk/blob/main/USAGE.md
29
+ EOS
30
+ else
31
+ out.puts(<<~EOS)
32
+
33
+ ⚠️ Packwerk is not ready to be used.
34
+ Please check output and refer to https://github.com/Shopify/packwerk/blob/main/USAGE.md for more information.
35
+ EOS
36
+ end
37
+
38
+ success
39
+ end
40
+ end
41
+ end
42
+ end
@@ -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