packwerk 1.1.0 → 1.2.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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +14 -5
  3. data/.ruby-version +1 -1
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +129 -111
  6. data/README.md +8 -1
  7. data/USAGE.md +17 -16
  8. data/dev.yml +1 -1
  9. data/exe/packwerk +1 -1
  10. data/gemfiles/Gemfile-rails-6-0 +22 -0
  11. data/lib/packwerk/application_load_paths.rb +3 -2
  12. data/lib/packwerk/application_validator.rb +83 -67
  13. data/lib/packwerk/association_inspector.rb +23 -11
  14. data/lib/packwerk/checker.rb +4 -7
  15. data/lib/packwerk/cli.rb +32 -100
  16. data/lib/packwerk/configuration.rb +10 -2
  17. data/lib/packwerk/const_node_inspector.rb +13 -14
  18. data/lib/packwerk/constant_discovery.rb +2 -0
  19. data/lib/packwerk/constant_name_inspector.rb +0 -1
  20. data/lib/packwerk/dependency_checker.rb +12 -17
  21. data/lib/packwerk/deprecated_references.rb +7 -9
  22. data/lib/packwerk/file_processor.rb +0 -4
  23. data/lib/packwerk/formatters/offenses_formatter.rb +43 -0
  24. data/lib/packwerk/formatters/progress_formatter.rb +9 -4
  25. data/lib/packwerk/generators/configuration_file.rb +0 -1
  26. data/lib/packwerk/inflector.rb +0 -2
  27. data/lib/packwerk/node.rb +9 -2
  28. data/lib/packwerk/node_processor.rb +15 -32
  29. data/lib/packwerk/node_processor_factory.rb +0 -5
  30. data/lib/packwerk/node_visitor.rb +1 -4
  31. data/lib/packwerk/offense.rb +2 -8
  32. data/lib/packwerk/offense_collection.rb +84 -0
  33. data/lib/packwerk/offenses_formatter.rb +15 -0
  34. data/lib/packwerk/output_style.rb +20 -0
  35. data/lib/packwerk/output_styles/coloured.rb +29 -0
  36. data/lib/packwerk/output_styles/plain.rb +26 -0
  37. data/lib/packwerk/package.rb +8 -0
  38. data/lib/packwerk/package_set.rb +2 -3
  39. data/lib/packwerk/parse_run.rb +104 -0
  40. data/lib/packwerk/parsed_constant_definitions.rb +2 -4
  41. data/lib/packwerk/parsers/erb.rb +0 -2
  42. data/lib/packwerk/parsers/factory.rb +1 -3
  43. data/lib/packwerk/parsers.rb +0 -2
  44. data/lib/packwerk/privacy_checker.rb +22 -17
  45. data/lib/packwerk/reference_extractor.rb +0 -8
  46. data/lib/packwerk/reference_offense.rb +49 -0
  47. data/lib/packwerk/result.rb +9 -0
  48. data/lib/packwerk/run_context.rb +4 -21
  49. data/lib/packwerk/sanity_checker.rb +1 -3
  50. data/lib/packwerk/spring_command.rb +1 -1
  51. data/lib/packwerk/version.rb +1 -1
  52. data/lib/packwerk/violation_type.rb +0 -2
  53. data/lib/packwerk.rb +73 -34
  54. data/library.yml +1 -1
  55. data/packwerk.gemspec +1 -0
  56. data/service.yml +1 -4
  57. data/shipit.rubygems.yml +5 -1
  58. data/sorbet/rbi/gems/{actioncable@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actioncable@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +56 -36
  59. data/sorbet/rbi/gems/{actionmailbox@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actionmailbox@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +25 -28
  60. data/sorbet/rbi/gems/{actionmailer@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actionmailer@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +43 -24
  61. data/sorbet/rbi/gems/{actionpack@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actionpack@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +382 -284
  62. data/sorbet/rbi/gems/{actiontext@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actiontext@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +76 -40
  63. data/sorbet/rbi/gems/{actionview@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actionview@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +206 -195
  64. data/sorbet/rbi/gems/{activejob@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activejob@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +64 -75
  65. data/sorbet/rbi/gems/{activemodel@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activemodel@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +103 -56
  66. data/sorbet/rbi/gems/{activerecord@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activerecord@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +1250 -898
  67. data/sorbet/rbi/gems/{activestorage@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activestorage@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +92 -120
  68. data/sorbet/rbi/gems/{activesupport@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activesupport@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +292 -193
  69. data/sorbet/rbi/gems/{ast@2.4.1.rbi → ast@2.4.2.rbi} +2 -1
  70. data/sorbet/rbi/gems/{better_html@1.0.15.rbi → better_html@1.0.16.rbi} +2 -2
  71. data/sorbet/rbi/gems/{concurrent-ruby@1.1.6.rbi → concurrent-ruby@1.1.8.rbi} +12 -9
  72. data/sorbet/rbi/gems/{erubi@1.9.0.rbi → erubi@1.10.0.rbi} +3 -1
  73. data/sorbet/rbi/gems/{i18n@1.8.2.rbi → i18n@1.8.10.rbi} +19 -52
  74. data/sorbet/rbi/gems/{loofah@2.5.0.rbi → loofah@2.9.0.rbi} +3 -1
  75. data/sorbet/rbi/gems/marcel@1.0.0.rbi +70 -0
  76. data/sorbet/rbi/gems/{mini_mime@1.0.2.rbi → mini_mime@1.0.3.rbi} +6 -6
  77. data/sorbet/rbi/gems/{mini_portile2@2.4.0.rbi → minitest-focus@1.2.1.rbi} +2 -2
  78. data/sorbet/rbi/gems/{minitest@5.14.0.rbi → minitest@5.14.4.rbi} +31 -29
  79. data/sorbet/rbi/gems/{mocha@1.11.2.rbi → mocha@1.12.0.rbi} +25 -36
  80. data/sorbet/rbi/gems/{nio4r@2.5.2.rbi → nio4r@2.5.7.rbi} +21 -20
  81. data/sorbet/rbi/gems/{nokogiri@1.10.9.rbi → nokogiri@1.11.2.rbi} +193 -154
  82. data/sorbet/rbi/gems/parallel@1.20.1.rbi +117 -0
  83. data/sorbet/rbi/gems/parlour@6.0.0.rbi +1272 -0
  84. data/sorbet/rbi/gems/{parser@2.7.1.4.rbi → parser@3.0.0.0.rbi} +287 -174
  85. data/sorbet/rbi/gems/{pry@0.13.1.rbi → pry@0.14.0.rbi} +1 -1
  86. data/sorbet/rbi/gems/racc@1.5.2.rbi +57 -0
  87. data/sorbet/rbi/gems/{rack@2.2.2.rbi → rack@2.2.3.rbi} +23 -35
  88. data/sorbet/rbi/gems/{rails@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → rails@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +1 -1
  89. data/sorbet/rbi/gems/{railties@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → railties@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +132 -121
  90. data/sorbet/rbi/gems/{rake@13.0.1.rbi → rake@13.0.3.rbi} +16 -20
  91. data/sorbet/rbi/gems/{parallel@1.19.1.rbi → regexp_parser@2.1.1.rbi} +2 -2
  92. data/sorbet/rbi/gems/rubocop-ast@1.4.1.rbi +8 -0
  93. data/sorbet/rbi/gems/{rubocop-performance@1.5.2.rbi → rubocop-performance@1.10.2.rbi} +1 -1
  94. data/sorbet/rbi/gems/{rubocop-shopify@1.0.2.rbi → rubocop-shopify@2.0.1.rbi} +1 -1
  95. data/sorbet/rbi/gems/{rubocop-sorbet@0.3.7.rbi → rubocop-sorbet@0.6.1.rbi} +1 -1
  96. data/sorbet/rbi/gems/{rubocop@0.82.0.rbi → rubocop@1.12.0.rbi} +1 -1
  97. data/sorbet/rbi/gems/{ruby-progressbar@1.10.1.rbi → ruby-progressbar@1.11.0.rbi} +1 -1
  98. data/sorbet/rbi/gems/spoom@1.1.0.rbi +1061 -0
  99. data/sorbet/rbi/gems/{spring@2.1.0.rbi → spring@2.1.1.rbi} +7 -7
  100. data/sorbet/rbi/gems/{sprockets-rails@3.2.1.rbi → sprockets-rails@3.2.2.rbi} +88 -68
  101. data/sorbet/rbi/gems/{sprockets@4.0.0.rbi → sprockets@4.0.2.rbi} +8 -7
  102. data/sorbet/rbi/gems/{tapioca@0.4.5.rbi → tapioca@0.4.19.rbi} +109 -24
  103. data/sorbet/rbi/gems/{thor@1.0.1.rbi → thor@1.1.0.rbi} +16 -15
  104. data/sorbet/rbi/gems/{tzinfo@2.0.2.rbi → tzinfo@2.0.4.rbi} +21 -2
  105. data/sorbet/rbi/gems/{unicode-display_width@1.7.0.rbi → unicode-display_width@2.0.0.rbi} +1 -1
  106. data/sorbet/rbi/gems/{websocket-driver@0.7.1.rbi → websocket-driver@0.7.3.rbi} +29 -29
  107. data/sorbet/rbi/gems/{websocket-extensions@0.1.4.rbi → websocket-extensions@0.1.5.rbi} +2 -2
  108. data/sorbet/rbi/gems/zeitwerk@2.4.2.rbi +177 -0
  109. data/sorbet/tapioca/require.rb +1 -0
  110. metadata +78 -61
  111. data/lib/packwerk/cache_deprecated_references.rb +0 -47
  112. data/lib/packwerk/checking_deprecated_references.rb +0 -40
  113. data/lib/packwerk/commands/detect_stale_violations_command.rb +0 -63
  114. data/lib/packwerk/commands/offense_progress_marker.rb +0 -24
  115. data/lib/packwerk/detect_stale_deprecated_references.rb +0 -14
  116. data/lib/packwerk/output_styles.rb +0 -41
  117. data/lib/packwerk/reference_lister.rb +0 -23
  118. data/lib/packwerk/updating_deprecated_references.rb +0 -14
  119. data/sorbet/rbi/gems/jaro_winkler@1.5.4.rbi +0 -8
  120. data/sorbet/rbi/gems/marcel@0.3.3.rbi +0 -30
  121. data/sorbet/rbi/gems/mimemagic@0.3.5.rbi +0 -47
  122. data/sorbet/rbi/gems/parlour@4.0.1.rbi +0 -561
  123. data/sorbet/rbi/gems/spoom@1.0.4.rbi +0 -418
  124. data/sorbet/rbi/gems/zeitwerk@2.3.0.rbi +0 -8
@@ -6,11 +6,6 @@ require "constant_resolver"
6
6
  require "pathname"
7
7
  require "yaml"
8
8
 
9
- require "packwerk/package_set"
10
- require "packwerk/graph"
11
- require "packwerk/inflector"
12
- require "packwerk/application_load_paths"
13
-
14
9
  module Packwerk
15
10
  class ApplicationValidator
16
11
  def initialize(config_file_path:, configuration:)
@@ -35,16 +30,7 @@ module Packwerk
35
30
  check_root_package_exists,
36
31
  ]
37
32
 
38
- results.reject!(&:ok?)
39
-
40
- if results.empty?
41
- Result.new(true)
42
- else
43
- Result.new(
44
- false,
45
- results.map(&:error_value).join("\n===\n")
46
- )
47
- end
33
+ merge_results(results)
48
34
  end
49
35
 
50
36
  def check_autoload_path_cache
@@ -65,51 +51,37 @@ module Packwerk
65
51
  def check_package_manifests_for_privacy
66
52
  privacy_settings = package_manifests_settings_for("enforce_privacy")
67
53
 
68
- autoload_paths = @configuration.load_paths
69
-
70
54
  resolver = ConstantResolver.new(
71
55
  root_path: @configuration.root_path,
72
- load_paths: autoload_paths
56
+ load_paths: @configuration.load_paths
73
57
  )
74
58
 
75
- errors = []
59
+ results = []
76
60
 
77
- privacy_settings.each do |filepath, setting|
61
+ privacy_settings.each do |config_file_path, setting|
78
62
  next unless setting.is_a?(Array)
63
+ constants = setting
79
64
 
80
- setting.each do |constant|
81
- # make sure the constant can be loaded
82
- constant.constantize # rubocop:disable Sorbet/ConstantsFromStrings
83
- context = resolver.resolve(constant)
84
-
85
- unless context
86
- errors << "#{constant}, listed in #{filepath.inspect}, could not be resolved"
87
- next
88
- end
89
-
90
- expected_filename = constant.underscore + ".rb"
65
+ assert_constants_can_be_loaded(constants)
91
66
 
92
- # We don't support all custom inflections yet, so we may accidentally resolve constants to the
93
- # file that defines their parent namespace. This restriction makes sure that we don't.
94
- next if context.location.end_with?(expected_filename)
67
+ constant_locations = constants.map { |c| [c, resolver.resolve(c)&.location] }
95
68
 
96
- errors << "Explicitly private constants need to have their own files.\n"\
97
- "#{constant}, listed in #{filepath.inspect}, was resolved to #{context.location.inspect}.\n"\
98
- "It should be in something like #{expected_filename.inspect}"
69
+ constant_locations.each do |name, location|
70
+ results << if location
71
+ check_private_constant_location(name, location, config_file_path)
72
+ else
73
+ private_constant_unresolvable(name, config_file_path)
74
+ end
99
75
  end
100
76
  end
101
77
 
102
- if errors.empty?
103
- Result.new(true)
104
- else
105
- Result.new(false, errors.join("\n---\n"))
106
- end
78
+ merge_results(results, separator: "\n---\n")
107
79
  end
108
80
 
109
81
  def check_package_manifest_syntax
110
82
  errors = []
111
83
 
112
- package_manifests(package_glob).each do |f|
84
+ package_manifests.each do |f|
113
85
  hash = YAML.load_file(f)
114
86
  next unless hash
115
87
 
@@ -124,26 +96,26 @@ module Packwerk
124
96
 
125
97
  if hash.key?("enforce_privacy")
126
98
  unless [TrueClass, FalseClass, Array].include?(hash["enforce_privacy"].class)
127
- errors << "Invalid 'enforce_privacy' option in #{f.inspect}: #{hash['enforce_privacy'].inspect}"
99
+ errors << "Invalid 'enforce_privacy' option in #{f.inspect}: #{hash["enforce_privacy"].inspect}"
128
100
  end
129
101
  end
130
102
 
131
103
  if hash.key?("enforce_dependencies")
132
104
  unless [TrueClass, FalseClass].include?(hash["enforce_dependencies"].class)
133
- errors << "Invalid 'enforce_dependencies' option in #{f.inspect}: #{hash['enforce_dependencies'].inspect}"
105
+ errors << "Invalid 'enforce_dependencies' option in #{f.inspect}: #{hash["enforce_dependencies"].inspect}"
134
106
  end
135
107
  end
136
108
 
137
109
  if hash.key?("public_path")
138
110
  unless hash["public_path"].is_a?(String)
139
- errors << "'public_path' option must be a string in #{f.inspect}: #{hash['public_path'].inspect}"
111
+ errors << "'public_path' option must be a string in #{f.inspect}: #{hash["public_path"].inspect}"
140
112
  end
141
113
  end
142
114
 
143
115
  next unless hash.key?("dependencies")
144
116
  next if hash["dependencies"].is_a?(Array)
145
117
 
146
- errors << "Invalid 'dependencies' option in #{f.inspect}: #{hash['dependencies'].inspect}"
118
+ errors << "Invalid 'dependencies' option in #{f.inspect}: #{hash["dependencies"].inspect}"
147
119
  end
148
120
 
149
121
  if errors.empty?
@@ -193,24 +165,16 @@ module Packwerk
193
165
  end
194
166
  end
195
167
 
196
- errors = results.reject(&:ok?)
197
-
198
- if errors.empty?
199
- Result.new(true)
200
- else
201
- Result.new(
202
- false,
203
- "Inflections specified in #{inflections_file} don't line up with application!\n" +
204
- errors.map(&:error_value).join("\n")
205
- )
206
- end
168
+ merge_results(
169
+ results,
170
+ separator: "\n",
171
+ errors_headline: "Inflections specified in #{inflections_file} don't line up with application!\n"
172
+ )
207
173
  end
208
174
 
209
175
  def check_acyclic_graph
210
- packages = Packwerk::PackageSet.load_all_from(@configuration.root_path)
211
-
212
- edges = packages.flat_map do |package|
213
- package.dependencies.map { |dependency| [package, packages.fetch(dependency)] }
176
+ edges = package_set.flat_map do |package|
177
+ package.dependencies.map { |dependency| [package, package_set.fetch(dependency)] }
214
178
  end
215
179
  dependency_graph = Packwerk::Graph.new(*edges)
216
180
 
@@ -225,7 +189,7 @@ module Packwerk
225
189
  cycle_strings = dependency_graph.cycles.map do |cycle|
226
190
  cycle_strings = cycle.map(&:to_s)
227
191
  cycle_strings << cycle.first.to_s
228
- "\t- #{cycle_strings.join(' → ')}"
192
+ "\t- #{cycle_strings.join(" → ")}"
229
193
  end
230
194
 
231
195
  if dependency_graph.acyclic?
@@ -316,8 +280,7 @@ module Packwerk
316
280
  private
317
281
 
318
282
  def package_manifests_settings_for(setting)
319
- package_manifests(package_glob)
320
- .map { |f| [f, (YAML.load_file(File.join(f)) || {})[setting]] }
283
+ package_manifests.map { |f| [f, (YAML.load_file(File.join(f)) || {})[setting]] }
321
284
  end
322
285
 
323
286
  def format_yaml_strings(list)
@@ -328,13 +291,17 @@ module Packwerk
328
291
  @configuration.package_paths || "**"
329
292
  end
330
293
 
331
- def package_manifests(glob_pattern)
294
+ def package_manifests(glob_pattern = package_glob)
332
295
  PackageSet.package_paths(@configuration.root_path, glob_pattern)
333
296
  .map { |f| File.realpath(f) }
334
297
  end
335
298
 
336
299
  def relative_paths(paths)
337
- paths.map { |path| Pathname.new(path).relative_path_from(@configuration.root_path) }
300
+ paths.map { |path| relative_path(path) }
301
+ end
302
+
303
+ def relative_path(path)
304
+ Pathname.new(path).relative_path_from(@configuration.root_path)
338
305
  end
339
306
 
340
307
  def invalid_package_path?(path)
@@ -344,5 +311,54 @@ module Packwerk
344
311
  package_path = File.join(@configuration.root_path, path, Packwerk::PackageSet::PACKAGE_CONFIG_FILENAME)
345
312
  !File.file?(package_path)
346
313
  end
314
+
315
+ def assert_constants_can_be_loaded(constants)
316
+ constants.each(&:constantize)
317
+ nil
318
+ end
319
+
320
+ def private_constant_unresolvable(name, config_file_path)
321
+ explicit_filepath = (name.start_with?("::") ? name[2..-1] : name).underscore + ".rb"
322
+
323
+ Result.new(
324
+ false,
325
+ "'#{name}', listed in #{config_file_path}, could not be resolved.\n"\
326
+ "This is probably because it is an autovivified namespace - a namespace module that doesn't have a\n"\
327
+ "file explicitly defining it. Packwerk currently doesn't support declaring autovivified namespaces as\n"\
328
+ "private. Add a #{explicit_filepath} file to explicitly define the constant."
329
+ )
330
+ end
331
+
332
+ def check_private_constant_location(name, location, config_file_path)
333
+ declared_package = package_set.package_from_path(relative_path(config_file_path))
334
+ constant_package = package_set.package_from_path(location)
335
+
336
+ if constant_package == declared_package
337
+ Result.new(true)
338
+ else
339
+ Result.new(
340
+ false,
341
+ "'#{name}' is declared as private in the '#{declared_package}' package but appears to be "\
342
+ "defined\nin the '#{constant_package}' package. Packwerk resolved it to #{location}."
343
+ )
344
+ end
345
+ end
346
+
347
+ def package_set
348
+ @package_set ||= Packwerk::PackageSet.load_all_from(@configuration.root_path, package_pathspec: package_glob)
349
+ end
350
+
351
+ def merge_results(results, separator: "\n===\n", errors_headline: "")
352
+ results.reject!(&:ok?)
353
+
354
+ if results.empty?
355
+ Result.new(true)
356
+ else
357
+ Result.new(
358
+ false,
359
+ errors_headline + results.map(&:error_value).join(separator)
360
+ )
361
+ end
362
+ end
347
363
  end
348
364
  end
@@ -1,26 +1,35 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "packwerk/constant_name_inspector"
5
- require "packwerk/node"
6
-
7
4
  module Packwerk
8
5
  # Extracts the implicit constant reference from an active record association
9
6
  class AssociationInspector
7
+ extend T::Sig
10
8
  include ConstantNameInspector
11
9
 
12
- RAILS_ASSOCIATIONS = %i(
13
- belongs_to
14
- has_many
15
- has_one
16
- has_and_belongs_to_many
17
- ).to_set
10
+ CustomAssociations = T.type_alias { T.any(T::Array[Symbol], T::Set[Symbol]) }
11
+
12
+ RAILS_ASSOCIATIONS = T.let(
13
+ %i(
14
+ belongs_to
15
+ has_many
16
+ has_one
17
+ has_and_belongs_to_many
18
+ ).to_set,
19
+ CustomAssociations
20
+ )
18
21
 
22
+ sig { params(inflector: Inflector, custom_associations: CustomAssociations).void }
19
23
  def initialize(inflector:, custom_associations: Set.new)
20
24
  @inflector = inflector
21
- @associations = RAILS_ASSOCIATIONS + custom_associations
25
+ @associations = T.let(RAILS_ASSOCIATIONS + custom_associations, CustomAssociations)
22
26
  end
23
27
 
28
+ sig do
29
+ override
30
+ .params(node: AST::Node, ancestors: T::Array[AST::Node])
31
+ .returns(T.nilable(String))
32
+ end
24
33
  def constant_name_from_node(node, ancestors:)
25
34
  return unless Node.method_call?(node)
26
35
  return unless association?(node)
@@ -38,11 +47,13 @@ module Packwerk
38
47
 
39
48
  private
40
49
 
50
+ sig { params(node: AST::Node).returns(T::Boolean) }
41
51
  def association?(node)
42
52
  method_name = Node.method_name(node)
43
53
  @associations.include?(method_name)
44
54
  end
45
55
 
56
+ sig { params(arguments: T::Array[AST::Node]).returns(T.nilable(AST::Node)) }
46
57
  def custom_class_name(arguments)
47
58
  association_options = arguments.detect { |n| Node.hash?(n) }
48
59
  return unless association_options
@@ -50,6 +61,7 @@ module Packwerk
50
61
  Node.value_from_hash(association_options, :class_name)
51
62
  end
52
63
 
64
+ sig { params(arguments: T::Array[AST::Node]).returns(T.any(T.nilable(Symbol), T.nilable(String))) }
53
65
  def association_name(arguments)
54
66
  return unless Node.symbol?(arguments[0])
55
67
 
@@ -1,9 +1,6 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require "sorbet-runtime"
5
- require "packwerk/reference_lister"
6
-
7
4
  module Packwerk
8
5
  module Checker
9
6
  extend T::Sig
@@ -11,10 +8,10 @@ module Packwerk
11
8
 
12
9
  interface!
13
10
 
14
- sig { params(reference: Reference, reference_lister: ReferenceLister).returns(T::Boolean).abstract }
15
- def invalid_reference?(reference, reference_lister); end
11
+ sig { returns(ViolationType).abstract }
12
+ def violation_type; end
16
13
 
17
- sig { params(reference: Reference).returns(String).abstract }
18
- def message_for(reference); end
14
+ sig { params(reference: Reference).returns(T::Boolean).abstract }
15
+ def invalid_reference?(reference); end
19
16
  end
20
17
  end
data/lib/packwerk/cli.rb CHANGED
@@ -1,35 +1,32 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
- require "benchmark"
4
- require "sorbet-runtime"
5
-
6
- require "packwerk/application_validator"
7
- require "packwerk/configuration"
8
- require "packwerk/files_for_processing"
9
- require "packwerk/formatters/progress_formatter"
10
- require "packwerk/inflector"
11
- require "packwerk/output_styles"
12
- require "packwerk/run_context"
13
- require "packwerk/updating_deprecated_references"
14
- require "packwerk/checking_deprecated_references"
15
- require "packwerk/commands/detect_stale_violations_command"
16
- require "packwerk/commands/offense_progress_marker"
17
3
 
18
4
  module Packwerk
19
5
  class Cli
20
6
  extend T::Sig
21
- include OffenseProgressMarker
22
7
 
23
- def initialize(run_context: nil, configuration: nil, out: $stdout, err_out: $stderr, style: OutputStyles::Plain)
8
+ sig do
9
+ params(
10
+ configuration: T.nilable(Configuration),
11
+ out: T.any(StringIO, IO),
12
+ err_out: T.any(StringIO, IO),
13
+ style: Packwerk::OutputStyle,
14
+ offenses_formatter: T.nilable(Packwerk::OffensesFormatter)
15
+ ).void
16
+ end
17
+ def initialize(
18
+ configuration: nil,
19
+ out: $stdout,
20
+ err_out: $stderr,
21
+ style: OutputStyles::Plain.new,
22
+ offenses_formatter: nil
23
+ )
24
24
  @out = out
25
25
  @err_out = err_out
26
26
  @style = style
27
27
  @configuration = configuration || Configuration.from_path
28
- @run_context = run_context || Packwerk::RunContext.from_configuration(
29
- @configuration,
30
- reference_lister: ::Packwerk::CheckingDeprecatedReferences.new(@configuration.root_path),
31
- )
32
28
  @progress_formatter = Formatters::ProgressFormatter.new(@out, style: style)
29
+ @offenses_formatter = offenses_formatter || Formatters::OffensesFormatter.new(style: @style)
33
30
  end
34
31
 
35
32
  sig { params(args: T::Array[String]).returns(T.noreturn) }
@@ -47,13 +44,13 @@ module Packwerk
47
44
  when "generate_configs"
48
45
  generate_configs
49
46
  when "check"
50
- check(args)
47
+ output_result(parse_run(args).check)
51
48
  when "detect-stale-violations"
52
- detect_stale_violations(args)
49
+ output_result(parse_run(args).detect_stale_violations)
53
50
  when "update"
54
51
  update(args)
55
52
  when "update-deprecations"
56
- update_deprecations(args)
53
+ output_result(parse_run(args).update_deprecations)
57
54
  when "validate"
58
55
  validate(args)
59
56
  when nil, "help"
@@ -101,7 +98,7 @@ module Packwerk
101
98
 
102
99
  def generate_configs
103
100
  configuration_file = Packwerk::Generators::ConfigurationFile.generate(
104
- load_paths: @configuration.load_paths,
101
+ load_paths: Packwerk::ApplicationLoadPaths.extract_relevant_paths,
105
102
  root: @configuration.root_path,
106
103
  out: @out
107
104
  )
@@ -130,71 +127,10 @@ module Packwerk
130
127
 
131
128
  def update(paths)
132
129
  warn("`packwerk update` is deprecated in favor of `packwerk update-deprecations`.")
133
- update_deprecations(paths)
134
- end
135
-
136
- def update_deprecations(paths)
137
- updating_deprecated_references = ::Packwerk::UpdatingDeprecatedReferences.new(@configuration.root_path)
138
- @run_context = Packwerk::RunContext.from_configuration(
139
- @configuration,
140
- reference_lister: updating_deprecated_references
141
- )
142
-
143
- files = fetch_files_to_process(paths)
144
-
145
- @progress_formatter.started(files)
146
-
147
- all_offenses = T.let([], T.untyped)
148
- execution_time = Benchmark.realtime do
149
- all_offenses = files.flat_map do |path|
150
- @run_context.process_file(file: path).tap do |offenses|
151
- mark_progress(offenses: offenses, progress_formatter: @progress_formatter)
152
- end
153
- end
154
-
155
- updating_deprecated_references.dump_deprecated_references_files
156
- end
157
-
158
- @out.puts # put a new line after the progress dots
159
- show_offenses(all_offenses)
160
- @progress_formatter.finished(execution_time)
161
- @out.puts("✅ `deprecated_references.yml` has been updated.")
162
-
163
- all_offenses.empty?
164
- end
165
-
166
- def check(paths)
167
- files = fetch_files_to_process(paths)
168
-
169
- @progress_formatter.started(files)
170
-
171
- all_offenses = T.let([], T.untyped)
172
- execution_time = Benchmark.realtime do
173
- files.each do |path|
174
- @run_context.process_file(file: path).tap do |offenses|
175
- mark_progress(offenses: offenses, progress_formatter: @progress_formatter)
176
- all_offenses.concat(offenses)
177
- end
178
- end
179
- rescue Interrupt
180
- @out.puts
181
- @out.puts("Manually interrupted. Violations caught so far are listed below:")
182
- end
183
-
184
- @out.puts # put a new line after the progress dots
185
- show_offenses(all_offenses)
186
- @progress_formatter.finished(execution_time)
187
-
188
- all_offenses.empty?
130
+ output_result(parse_run(paths).update_deprecations)
189
131
  end
190
132
 
191
- def detect_stale_violations(paths)
192
- detect_stale_violations = DetectStaleViolationsCommand.new(
193
- files: fetch_files_to_process(paths),
194
- configuration: @configuration,
195
- progress_formatter: @progress_formatter
196
- )
197
- result = detect_stale_violations.run
133
+ def output_result(result)
198
134
  @out.puts
199
135
  @out.puts(result.message)
200
136
  result.status
@@ -225,19 +161,6 @@ module Packwerk
225
161
  end
226
162
  end
227
163
 
228
- def show_offenses(offenses)
229
- if offenses.empty?
230
- @out.puts("No offenses detected 🎉")
231
- else
232
- offenses.each do |offense|
233
- @out.puts(offense.to_s(@style))
234
- end
235
-
236
- offenses_string = Inflector.default.pluralize("offense", offenses.length)
237
- @out.puts("#{offenses.length} #{offenses_string} detected")
238
- end
239
- end
240
-
241
164
  def list_validation_errors(result)
242
165
  @out.puts
243
166
  if result.ok?
@@ -256,5 +179,14 @@ module Packwerk
256
179
  false
257
180
  end
258
181
  end
182
+
183
+ def parse_run(paths)
184
+ ParseRun.new(
185
+ files: fetch_files_to_process(paths),
186
+ configuration: @configuration,
187
+ progress_formatter: @progress_formatter,
188
+ offenses_formatter: @offenses_formatter
189
+ )
190
+ end
259
191
  end
260
192
  end
@@ -22,7 +22,10 @@ module Packwerk
22
22
  private
23
23
 
24
24
  def from_packwerk_config(path)
25
- new(YAML.load_file(path), config_path: path)
25
+ new(
26
+ YAML.load_file(path) || {},
27
+ config_path: path
28
+ )
26
29
  end
27
30
  end
28
31
 
@@ -42,10 +45,15 @@ module Packwerk
42
45
  @root_path = File.expand_path(root)
43
46
  @package_paths = configs["package_paths"] || "**/"
44
47
  @custom_associations = configs["custom_associations"] || []
45
- @load_paths = configs["load_paths"]
48
+ @load_paths = configs["load_paths"] || []
46
49
  @inflections_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
50
+ @parallel = configs.key?("parallel") ? configs["parallel"] : true
47
51
 
48
52
  @config_path = config_path
49
53
  end
54
+
55
+ def parallel?
56
+ @parallel
57
+ end
50
58
  end
51
59
  end
@@ -1,20 +1,24 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "packwerk/constant_name_inspector"
5
-
6
4
  module Packwerk
7
5
  # Extracts a constant name from an AST node of type :const
8
6
  class ConstNodeInspector
7
+ extend T::Sig
9
8
  include ConstantNameInspector
10
9
 
10
+ sig do
11
+ override
12
+ .params(node: AST::Node, ancestors: T::Array[AST::Node])
13
+ .returns(T.nilable(String))
14
+ end
11
15
  def constant_name_from_node(node, ancestors:)
12
16
  return nil unless Node.constant?(node)
13
17
  parent = ancestors.first
14
18
  return nil unless root_constant?(parent)
15
19
 
16
20
  if parent && constant_in_module_or_class_definition?(node, parent: parent)
17
- fully_qualify_constant(node, ancestors: ancestors)
21
+ fully_qualify_constant(ancestors)
18
22
  else
19
23
  begin
20
24
  Node.constant_name(node)
@@ -28,27 +32,22 @@ module Packwerk
28
32
 
29
33
  # Only process the root `const` node for namespaced constant references. For example, in the
30
34
  # reference `Spam::Eggs::Thing`, we only process the const node associated with `Spam`.
35
+ sig { params(parent: T.nilable(AST::Node)).returns(T::Boolean) }
31
36
  def root_constant?(parent)
32
37
  !(parent && Node.constant?(parent))
33
38
  end
34
39
 
40
+ sig { params(node: AST::Node, parent: AST::Node).returns(T.nilable(T::Boolean)) }
35
41
  def constant_in_module_or_class_definition?(node, parent:)
36
42
  parent_name = Node.module_name_from_definition(parent)
37
43
  parent_name && parent_name == Node.constant_name(node)
38
44
  end
39
45
 
40
- def fully_qualify_constant(node, ancestors:)
46
+ sig { params(ancestors: T::Array[AST::Node]).returns(String) }
47
+ def fully_qualify_constant(ancestors)
41
48
  # We're defining a class with this name, in which case the constant is implicitly fully qualified by its
42
49
  # enclosing namespace
43
- name = Node.parent_module_name(ancestors: ancestors)
44
- name ||= generate_qualified_constant(node, ancestors)
45
- "::" + name
46
- end
47
-
48
- def generate_qualified_constant(node, ancestors:)
49
- namespace_path = Node.enclosing_namespace_path(node, ancestors: ancestors)
50
- constant_name = Node.constant_name(node)
51
- namespace_path.push(constant_name).join("::")
50
+ "::" + Node.parent_module_name(ancestors: ancestors)
52
51
  end
53
52
  end
54
53
  end
@@ -1,6 +1,8 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "constant_resolver"
5
+
4
6
  module Packwerk
5
7
  # Get information about (partially qualified) constants without loading the application code.
6
8
  # Information gathered: Fully qualified name, path to file containing the definition, package,
@@ -2,7 +2,6 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "ast"
5
- require "sorbet-runtime"
6
5
 
7
6
  module Packwerk
8
7
  # An interface describing some object that can extract a constant name from an AST node
@@ -1,31 +1,26 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "packwerk/violation_type"
5
- require "packwerk/checker"
6
-
7
4
  module Packwerk
8
5
  class DependencyChecker
6
+ extend T::Sig
9
7
  include Checker
10
8
 
9
+ sig { override.returns(ViolationType) }
11
10
  def violation_type
12
11
  ViolationType::Dependency
13
12
  end
14
13
 
15
- def invalid_reference?(reference, reference_lister)
16
- return unless reference.source_package
17
- return unless reference.source_package.enforce_dependencies?
18
- return if reference.source_package.dependency?(reference.constant.package)
19
- return if reference_lister.listed?(reference, violation_type: violation_type)
20
- true
14
+ sig do
15
+ override
16
+ .params(reference: Packwerk::Reference)
17
+ .returns(T::Boolean)
21
18
  end
22
-
23
- def message_for(reference)
24
- "Dependency violation: #{reference.constant.name} belongs to '#{reference.constant.package}', but " \
25
- "'#{reference.source_package}' does not specify a dependency on " \
26
- "'#{reference.constant.package}'.\n" \
27
- "Are we missing an abstraction?\n" \
28
- "Is the code making the reference, and the referenced constant, in the right packages?\n"
19
+ def invalid_reference?(reference)
20
+ return false unless reference.source_package
21
+ return false unless reference.source_package.enforce_dependencies?
22
+ return false if reference.source_package.dependency?(reference.constant.package)
23
+ true
29
24
  end
30
25
  end
31
26
  end