packwerk 1.1.0 → 1.3.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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +17 -8
  3. data/.ruby-version +1 -1
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +129 -111
  6. data/README.md +10 -3
  7. data/TROUBLESHOOT.md +2 -2
  8. data/USAGE.md +30 -30
  9. data/bin/m +29 -0
  10. data/bin/rake +29 -0
  11. data/bin/rubocop +29 -0
  12. data/bin/srb +29 -0
  13. data/bin/tapioca +29 -0
  14. data/dev.yml +7 -7
  15. data/exe/packwerk +1 -1
  16. data/gemfiles/Gemfile-rails-6-0 +22 -0
  17. data/lib/packwerk.rb +72 -34
  18. data/lib/packwerk/application_load_paths.rb +21 -10
  19. data/lib/packwerk/application_validator.rb +104 -84
  20. data/lib/packwerk/association_inspector.rb +23 -11
  21. data/lib/packwerk/checker.rb +4 -7
  22. data/lib/packwerk/cli.rb +36 -129
  23. data/lib/packwerk/configuration.rb +10 -2
  24. data/lib/packwerk/const_node_inspector.rb +13 -14
  25. data/lib/packwerk/constant_discovery.rb +2 -0
  26. data/lib/packwerk/constant_name_inspector.rb +0 -1
  27. data/lib/packwerk/dependency_checker.rb +12 -17
  28. data/lib/packwerk/deprecated_references.rb +8 -10
  29. data/lib/packwerk/file_processor.rb +0 -4
  30. data/lib/packwerk/formatters/offenses_formatter.rb +52 -0
  31. data/lib/packwerk/formatters/progress_formatter.rb +9 -4
  32. data/lib/packwerk/generators/configuration_file.rb +0 -1
  33. data/lib/packwerk/inflector.rb +0 -2
  34. data/lib/packwerk/node.rb +9 -2
  35. data/lib/packwerk/node_processor.rb +15 -32
  36. data/lib/packwerk/node_processor_factory.rb +0 -5
  37. data/lib/packwerk/node_visitor.rb +1 -4
  38. data/lib/packwerk/offense.rb +2 -8
  39. data/lib/packwerk/offense_collection.rb +84 -0
  40. data/lib/packwerk/offenses_formatter.rb +19 -0
  41. data/lib/packwerk/output_style.rb +20 -0
  42. data/lib/packwerk/output_styles/coloured.rb +29 -0
  43. data/lib/packwerk/output_styles/plain.rb +26 -0
  44. data/lib/packwerk/package.rb +17 -1
  45. data/lib/packwerk/package_set.rb +2 -3
  46. data/lib/packwerk/parse_run.rb +106 -0
  47. data/lib/packwerk/parsed_constant_definitions.rb +2 -4
  48. data/lib/packwerk/parsers.rb +0 -2
  49. data/lib/packwerk/parsers/erb.rb +0 -2
  50. data/lib/packwerk/parsers/factory.rb +1 -3
  51. data/lib/packwerk/privacy_checker.rb +22 -17
  52. data/lib/packwerk/reference_extractor.rb +0 -8
  53. data/lib/packwerk/reference_offense.rb +49 -0
  54. data/lib/packwerk/result.rb +9 -0
  55. data/lib/packwerk/run_context.rb +4 -21
  56. data/lib/packwerk/sanity_checker.rb +1 -3
  57. data/lib/packwerk/version.rb +1 -1
  58. data/lib/packwerk/violation_type.rb +0 -2
  59. data/library.yml +1 -1
  60. data/packwerk.gemspec +1 -0
  61. data/service.yml +1 -4
  62. data/shipit.rubygems.yml +5 -1
  63. data/sorbet/rbi/gems/{actioncable@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actioncable@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +56 -36
  64. data/sorbet/rbi/gems/{actionmailbox@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actionmailbox@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +25 -28
  65. data/sorbet/rbi/gems/{actionmailer@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actionmailer@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +43 -24
  66. data/sorbet/rbi/gems/{actionpack@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actionpack@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +382 -284
  67. data/sorbet/rbi/gems/{actiontext@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actiontext@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +76 -40
  68. data/sorbet/rbi/gems/{actionview@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actionview@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +206 -195
  69. data/sorbet/rbi/gems/{activejob@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activejob@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +64 -75
  70. data/sorbet/rbi/gems/{activemodel@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activemodel@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +103 -56
  71. data/sorbet/rbi/gems/{activerecord@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activerecord@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +1250 -898
  72. data/sorbet/rbi/gems/{activestorage@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activestorage@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +92 -120
  73. data/sorbet/rbi/gems/{activesupport@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activesupport@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +292 -193
  74. data/sorbet/rbi/gems/{ast@2.4.1.rbi → ast@2.4.2.rbi} +2 -1
  75. data/sorbet/rbi/gems/{better_html@1.0.15.rbi → better_html@1.0.16.rbi} +2 -2
  76. data/sorbet/rbi/gems/{concurrent-ruby@1.1.6.rbi → concurrent-ruby@1.1.8.rbi} +12 -9
  77. data/sorbet/rbi/gems/{erubi@1.9.0.rbi → erubi@1.10.0.rbi} +3 -1
  78. data/sorbet/rbi/gems/{i18n@1.8.2.rbi → i18n@1.8.10.rbi} +19 -52
  79. data/sorbet/rbi/gems/{loofah@2.5.0.rbi → loofah@2.9.0.rbi} +3 -1
  80. data/sorbet/rbi/gems/marcel@1.0.0.rbi +70 -0
  81. data/sorbet/rbi/gems/{mini_mime@1.0.2.rbi → mini_mime@1.0.3.rbi} +6 -6
  82. data/sorbet/rbi/gems/{mini_portile2@2.4.0.rbi → minitest-focus@1.2.1.rbi} +2 -2
  83. data/sorbet/rbi/gems/{minitest@5.14.0.rbi → minitest@5.14.4.rbi} +31 -29
  84. data/sorbet/rbi/gems/{mocha@1.11.2.rbi → mocha@1.12.0.rbi} +25 -36
  85. data/sorbet/rbi/gems/{nio4r@2.5.2.rbi → nio4r@2.5.7.rbi} +21 -20
  86. data/sorbet/rbi/gems/{nokogiri@1.10.9.rbi → nokogiri@1.11.2.rbi} +193 -154
  87. data/sorbet/rbi/gems/parallel@1.20.1.rbi +117 -0
  88. data/sorbet/rbi/gems/parlour@6.0.0.rbi +1272 -0
  89. data/sorbet/rbi/gems/{parser@2.7.1.4.rbi → parser@3.0.0.0.rbi} +287 -174
  90. data/sorbet/rbi/gems/{pry@0.13.1.rbi → pry@0.14.0.rbi} +1 -1
  91. data/sorbet/rbi/gems/racc@1.5.2.rbi +57 -0
  92. data/sorbet/rbi/gems/{rack@2.2.2.rbi → rack@2.2.3.rbi} +23 -35
  93. data/sorbet/rbi/gems/{rails@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → rails@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +1 -1
  94. data/sorbet/rbi/gems/{railties@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → railties@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +132 -121
  95. data/sorbet/rbi/gems/{rake@13.0.1.rbi → rake@13.0.3.rbi} +16 -20
  96. data/sorbet/rbi/gems/{parallel@1.19.1.rbi → regexp_parser@2.1.1.rbi} +2 -2
  97. data/sorbet/rbi/gems/rubocop-ast@1.4.1.rbi +8 -0
  98. data/sorbet/rbi/gems/{rubocop-performance@1.5.2.rbi → rubocop-performance@1.10.2.rbi} +1 -1
  99. data/sorbet/rbi/gems/{rubocop-shopify@1.0.2.rbi → rubocop-shopify@2.0.1.rbi} +1 -1
  100. data/sorbet/rbi/gems/{rubocop-sorbet@0.3.7.rbi → rubocop-sorbet@0.6.1.rbi} +1 -1
  101. data/sorbet/rbi/gems/{rubocop@0.82.0.rbi → rubocop@1.12.0.rbi} +1 -1
  102. data/sorbet/rbi/gems/{ruby-progressbar@1.10.1.rbi → ruby-progressbar@1.11.0.rbi} +1 -1
  103. data/sorbet/rbi/gems/spoom@1.1.0.rbi +1061 -0
  104. data/sorbet/rbi/gems/{spring@2.1.0.rbi → spring@2.1.1.rbi} +7 -7
  105. data/sorbet/rbi/gems/{sprockets-rails@3.2.1.rbi → sprockets-rails@3.2.2.rbi} +88 -68
  106. data/sorbet/rbi/gems/{sprockets@4.0.0.rbi → sprockets@4.0.2.rbi} +8 -7
  107. data/sorbet/rbi/gems/{tapioca@0.4.5.rbi → tapioca@0.4.19.rbi} +109 -24
  108. data/sorbet/rbi/gems/{thor@1.0.1.rbi → thor@1.1.0.rbi} +16 -15
  109. data/sorbet/rbi/gems/{tzinfo@2.0.2.rbi → tzinfo@2.0.4.rbi} +21 -2
  110. data/sorbet/rbi/gems/{unicode-display_width@1.7.0.rbi → unicode-display_width@2.0.0.rbi} +1 -1
  111. data/sorbet/rbi/gems/{websocket-driver@0.7.1.rbi → websocket-driver@0.7.3.rbi} +29 -29
  112. data/sorbet/rbi/gems/{websocket-extensions@0.1.4.rbi → websocket-extensions@0.1.5.rbi} +2 -2
  113. data/sorbet/rbi/gems/zeitwerk@2.4.2.rbi +177 -0
  114. data/sorbet/tapioca/require.rb +1 -0
  115. metadata +83 -65
  116. data/lib/packwerk/cache_deprecated_references.rb +0 -47
  117. data/lib/packwerk/checking_deprecated_references.rb +0 -40
  118. data/lib/packwerk/commands/detect_stale_violations_command.rb +0 -63
  119. data/lib/packwerk/commands/offense_progress_marker.rb +0 -24
  120. data/lib/packwerk/detect_stale_deprecated_references.rb +0 -14
  121. data/lib/packwerk/generators/application_validation.rb +0 -62
  122. data/lib/packwerk/generators/templates/packwerk +0 -23
  123. data/lib/packwerk/generators/templates/packwerk_validator_test.rb +0 -11
  124. data/lib/packwerk/output_styles.rb +0 -41
  125. data/lib/packwerk/reference_lister.rb +0 -23
  126. data/lib/packwerk/spring_command.rb +0 -28
  127. data/lib/packwerk/updating_deprecated_references.rb +0 -14
  128. data/sorbet/rbi/gems/jaro_winkler@1.5.4.rbi +0 -8
  129. data/sorbet/rbi/gems/marcel@0.3.3.rbi +0 -30
  130. data/sorbet/rbi/gems/mimemagic@0.3.5.rbi +0 -47
  131. data/sorbet/rbi/gems/parlour@4.0.1.rbi +0 -561
  132. data/sorbet/rbi/gems/spoom@1.0.4.rbi +0 -418
  133. data/sorbet/rbi/gems/zeitwerk@2.3.0.rbi +0 -8
@@ -8,28 +8,24 @@ module Packwerk
8
8
  class << self
9
9
  extend T::Sig
10
10
 
11
- sig { returns(T::Array[String]) }
12
- def extract_relevant_paths
13
- assert_application_booted
11
+ sig { params(root: String, environment: String).returns(T::Array[String]) }
12
+ def extract_relevant_paths(root, environment)
13
+ require_application(root, environment)
14
14
  all_paths = extract_application_autoload_paths
15
15
  relevant_paths = filter_relevant_paths(all_paths)
16
16
  assert_load_paths_present(relevant_paths)
17
17
  relative_path_strings(relevant_paths)
18
18
  end
19
19
 
20
- sig { void }
21
- def assert_application_booted
22
- raise "The application needs to be booted to extract load paths" unless defined?(::Rails)
23
- end
24
-
25
20
  sig { returns(T::Array[String]) }
26
21
  def extract_application_autoload_paths
27
22
  Rails.application.railties
28
23
  .select { |railtie| railtie.is_a?(Rails::Engine) }
29
24
  .push(Rails.application)
30
25
  .flat_map do |engine|
31
- (engine.config.autoload_paths + engine.config.eager_load_paths + engine.config.autoload_once_paths).uniq
32
- end
26
+ paths = (engine.config.autoload_paths + engine.config.eager_load_paths + engine.config.autoload_once_paths)
27
+ paths.map(&:to_s).uniq
28
+ end
33
29
  end
34
30
 
35
31
  sig do
@@ -53,6 +49,21 @@ module Packwerk
53
49
  .uniq
54
50
  end
55
51
 
52
+ private
53
+
54
+ sig { params(root: String, environment: String).void }
55
+ def require_application(root, environment)
56
+ environment_file = "#{root}/config/environment"
57
+
58
+ if File.file?("#{environment_file}.rb")
59
+ ENV["RAILS_ENV"] ||= environment
60
+
61
+ require environment_file
62
+ else
63
+ raise "A Rails application could not be found in #{root}"
64
+ end
65
+ end
66
+
56
67
  sig { params(paths: T::Array[T.untyped]).void }
57
68
  def assert_load_paths_present(paths)
58
69
  if paths.empty?
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "active_support/inflector/inflections"
@@ -6,18 +6,14 @@ 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
- def initialize(config_file_path:, configuration:)
11
+ def initialize(config_file_path:, configuration:, environment:)
17
12
  @config_file_path = config_file_path
18
13
  @configuration = configuration
14
+ @environment = environment
19
15
 
20
- @application_load_paths = ApplicationLoadPaths.extract_relevant_paths
16
+ @application_load_paths = ApplicationLoadPaths.extract_relevant_paths(configuration.root_path, environment)
21
17
  end
22
18
 
23
19
  Result = Struct.new(:ok?, :error_value)
@@ -35,16 +31,7 @@ module Packwerk
35
31
  check_root_package_exists,
36
32
  ]
37
33
 
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
34
+ merge_results(results)
48
35
  end
49
36
 
50
37
  def check_autoload_path_cache
@@ -65,51 +52,37 @@ module Packwerk
65
52
  def check_package_manifests_for_privacy
66
53
  privacy_settings = package_manifests_settings_for("enforce_privacy")
67
54
 
68
- autoload_paths = @configuration.load_paths
69
-
70
55
  resolver = ConstantResolver.new(
71
56
  root_path: @configuration.root_path,
72
- load_paths: autoload_paths
57
+ load_paths: @configuration.load_paths
73
58
  )
74
59
 
75
- errors = []
60
+ results = []
76
61
 
77
- privacy_settings.each do |filepath, setting|
62
+ privacy_settings.each do |config_file_path, setting|
78
63
  next unless setting.is_a?(Array)
64
+ constants = setting
79
65
 
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)
66
+ assert_constants_can_be_loaded(constants)
84
67
 
85
- unless context
86
- errors << "#{constant}, listed in #{filepath.inspect}, could not be resolved"
87
- next
88
- end
68
+ constant_locations = constants.map { |c| [c, resolver.resolve(c)&.location] }
89
69
 
90
- expected_filename = constant.underscore + ".rb"
91
-
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)
95
-
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}"
70
+ constant_locations.each do |name, location|
71
+ results << if location
72
+ check_private_constant_location(name, location, config_file_path)
73
+ else
74
+ private_constant_unresolvable(name, config_file_path)
75
+ end
99
76
  end
100
77
  end
101
78
 
102
- if errors.empty?
103
- Result.new(true)
104
- else
105
- Result.new(false, errors.join("\n---\n"))
106
- end
79
+ merge_results(results, separator: "\n---\n")
107
80
  end
108
81
 
109
82
  def check_package_manifest_syntax
110
83
  errors = []
111
84
 
112
- package_manifests(package_glob).each do |f|
85
+ package_manifests.each do |f|
113
86
  hash = YAML.load_file(f)
114
87
  next unless hash
115
88
 
@@ -124,26 +97,26 @@ module Packwerk
124
97
 
125
98
  if hash.key?("enforce_privacy")
126
99
  unless [TrueClass, FalseClass, Array].include?(hash["enforce_privacy"].class)
127
- errors << "Invalid 'enforce_privacy' option in #{f.inspect}: #{hash['enforce_privacy'].inspect}"
100
+ errors << "Invalid 'enforce_privacy' option in #{f.inspect}: #{hash["enforce_privacy"].inspect}"
128
101
  end
129
102
  end
130
103
 
131
104
  if hash.key?("enforce_dependencies")
132
105
  unless [TrueClass, FalseClass].include?(hash["enforce_dependencies"].class)
133
- errors << "Invalid 'enforce_dependencies' option in #{f.inspect}: #{hash['enforce_dependencies'].inspect}"
106
+ errors << "Invalid 'enforce_dependencies' option in #{f.inspect}: #{hash["enforce_dependencies"].inspect}"
134
107
  end
135
108
  end
136
109
 
137
110
  if hash.key?("public_path")
138
111
  unless hash["public_path"].is_a?(String)
139
- errors << "'public_path' option must be a string in #{f.inspect}: #{hash['public_path'].inspect}"
112
+ errors << "'public_path' option must be a string in #{f.inspect}: #{hash["public_path"].inspect}"
140
113
  end
141
114
  end
142
115
 
143
116
  next unless hash.key?("dependencies")
144
117
  next if hash["dependencies"].is_a?(Array)
145
118
 
146
- errors << "Invalid 'dependencies' option in #{f.inspect}: #{hash['dependencies'].inspect}"
119
+ errors << "Invalid 'dependencies' option in #{f.inspect}: #{hash["dependencies"].inspect}"
147
120
  end
148
121
 
149
122
  if errors.empty?
@@ -193,40 +166,20 @@ module Packwerk
193
166
  end
194
167
  end
195
168
 
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
169
+ merge_results(
170
+ results,
171
+ separator: "\n",
172
+ errors_headline: "Inflections specified in #{inflections_file} don't line up with application!\n"
173
+ )
207
174
  end
208
175
 
209
176
  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)] }
214
- end
215
- dependency_graph = Packwerk::Graph.new(*edges)
216
-
217
- # Convert the cycle
218
- #
219
- # [a, b, c]
220
- #
221
- # to the string
222
- #
223
- # a -> b -> c -> a
224
- #
225
- cycle_strings = dependency_graph.cycles.map do |cycle|
226
- cycle_strings = cycle.map(&:to_s)
227
- cycle_strings << cycle.first.to_s
228
- "\t- #{cycle_strings.join(' → ')}"
177
+ edges = package_set.flat_map do |package|
178
+ package.dependencies.map { |dependency| [package, package_set.fetch(dependency)] }
229
179
  end
180
+ dependency_graph = Packwerk::Graph.new(*T.unsafe(edges))
181
+
182
+ cycle_strings = build_cycle_strings(dependency_graph.cycles)
230
183
 
231
184
  if dependency_graph.acyclic?
232
185
  Result.new(true)
@@ -315,9 +268,23 @@ module Packwerk
315
268
 
316
269
  private
317
270
 
271
+ # Convert the cycles:
272
+ #
273
+ # [[a, b, c], [b, c]]
274
+ #
275
+ # to the string:
276
+ #
277
+ # ["a -> b -> c -> a", "b -> c -> b"]
278
+ def build_cycle_strings(cycles)
279
+ cycles.map do |cycle|
280
+ cycle_strings = cycle.map(&:to_s)
281
+ cycle_strings << cycle.first.to_s
282
+ "\t- #{cycle_strings.join(" → ")}"
283
+ end
284
+ end
285
+
318
286
  def package_manifests_settings_for(setting)
319
- package_manifests(package_glob)
320
- .map { |f| [f, (YAML.load_file(File.join(f)) || {})[setting]] }
287
+ package_manifests.map { |f| [f, (YAML.load_file(File.join(f)) || {})[setting]] }
321
288
  end
322
289
 
323
290
  def format_yaml_strings(list)
@@ -328,13 +295,17 @@ module Packwerk
328
295
  @configuration.package_paths || "**"
329
296
  end
330
297
 
331
- def package_manifests(glob_pattern)
298
+ def package_manifests(glob_pattern = package_glob)
332
299
  PackageSet.package_paths(@configuration.root_path, glob_pattern)
333
300
  .map { |f| File.realpath(f) }
334
301
  end
335
302
 
336
303
  def relative_paths(paths)
337
- paths.map { |path| Pathname.new(path).relative_path_from(@configuration.root_path) }
304
+ paths.map { |path| relative_path(path) }
305
+ end
306
+
307
+ def relative_path(path)
308
+ Pathname.new(path).relative_path_from(@configuration.root_path)
338
309
  end
339
310
 
340
311
  def invalid_package_path?(path)
@@ -344,5 +315,54 @@ module Packwerk
344
315
  package_path = File.join(@configuration.root_path, path, Packwerk::PackageSet::PACKAGE_CONFIG_FILENAME)
345
316
  !File.file?(package_path)
346
317
  end
318
+
319
+ def assert_constants_can_be_loaded(constants)
320
+ constants.each(&:constantize)
321
+ nil
322
+ end
323
+
324
+ def private_constant_unresolvable(name, config_file_path)
325
+ explicit_filepath = (name.start_with?("::") ? name[2..-1] : name).underscore + ".rb"
326
+
327
+ Result.new(
328
+ false,
329
+ "'#{name}', listed in #{config_file_path}, could not be resolved.\n"\
330
+ "This is probably because it is an autovivified namespace - a namespace module that doesn't have a\n"\
331
+ "file explicitly defining it. Packwerk currently doesn't support declaring autovivified namespaces as\n"\
332
+ "private. Add a #{explicit_filepath} file to explicitly define the constant."
333
+ )
334
+ end
335
+
336
+ def check_private_constant_location(name, location, config_file_path)
337
+ declared_package = package_set.package_from_path(relative_path(config_file_path))
338
+ constant_package = package_set.package_from_path(location)
339
+
340
+ if constant_package == declared_package
341
+ Result.new(true)
342
+ else
343
+ Result.new(
344
+ false,
345
+ "'#{name}' is declared as private in the '#{declared_package}' package but appears to be "\
346
+ "defined\nin the '#{constant_package}' package. Packwerk resolved it to #{location}."
347
+ )
348
+ end
349
+ end
350
+
351
+ def package_set
352
+ @package_set ||= Packwerk::PackageSet.load_all_from(@configuration.root_path, package_pathspec: package_glob)
353
+ end
354
+
355
+ def merge_results(results, separator: "\n===\n", errors_headline: "")
356
+ results.reject!(&:ok?)
357
+
358
+ if results.empty?
359
+ Result.new(true)
360
+ else
361
+ Result.new(
362
+ false,
363
+ errors_headline + results.map(&:error_value).join(separator)
364
+ )
365
+ end
366
+ end
347
367
  end
348
368
  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,35 @@
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
+ environment: String,
14
+ style: Packwerk::OutputStyle,
15
+ offenses_formatter: T.nilable(Packwerk::OffensesFormatter)
16
+ ).void
17
+ end
18
+ def initialize(
19
+ configuration: nil,
20
+ out: $stdout,
21
+ err_out: $stderr,
22
+ environment: "test",
23
+ style: OutputStyles::Plain.new,
24
+ offenses_formatter: nil
25
+ )
24
26
  @out = out
25
27
  @err_out = err_out
28
+ @environment = environment
26
29
  @style = style
27
30
  @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
31
  @progress_formatter = Formatters::ProgressFormatter.new(@out, style: style)
32
+ @offenses_formatter = offenses_formatter || Formatters::OffensesFormatter.new(style: @style)
33
33
  end
34
34
 
35
35
  sig { params(args: T::Array[String]).returns(T.noreturn) }
@@ -47,13 +47,13 @@ module Packwerk
47
47
  when "generate_configs"
48
48
  generate_configs
49
49
  when "check"
50
- check(args)
50
+ output_result(parse_run(args).check)
51
51
  when "detect-stale-violations"
52
- detect_stale_violations(args)
52
+ output_result(parse_run(args).detect_stale_violations)
53
53
  when "update"
54
54
  update(args)
55
55
  when "update-deprecations"
56
- update_deprecations(args)
56
+ output_result(parse_run(args).update_deprecations)
57
57
  when "validate"
58
58
  validate(args)
59
59
  when nil, "help"
@@ -80,28 +80,12 @@ module Packwerk
80
80
  def init
81
81
  @out.puts("📦 Initializing Packwerk...")
82
82
 
83
- application_validation = Packwerk::Generators::ApplicationValidation.generate(
84
- for_rails_app: rails_app?,
85
- root: @configuration.root_path,
86
- out: @out
87
- )
88
-
89
- if application_validation
90
- if rails_app?
91
- # To run in the same space as the Rails process,
92
- # in order to fetch load paths for the configuration generator
93
- exec("bin/packwerk", "generate_configs")
94
- else
95
- generate_configurations = generate_configs
96
- end
97
- end
98
-
99
- application_validation && generate_configurations
83
+ generate_configs
100
84
  end
101
85
 
102
86
  def generate_configs
103
87
  configuration_file = Packwerk::Generators::ConfigurationFile.generate(
104
- load_paths: @configuration.load_paths,
88
+ load_paths: Packwerk::ApplicationLoadPaths.extract_relevant_paths(@configuration.root_path, @environment),
105
89
  root: @configuration.root_path,
106
90
  out: @out
107
91
  )
@@ -130,71 +114,10 @@ module Packwerk
130
114
 
131
115
  def update(paths)
132
116
  warn("`packwerk update` is deprecated in favor of `packwerk update-deprecations`.")
133
- update_deprecations(paths)
117
+ output_result(parse_run(paths).update_deprecations)
134
118
  end
135
119
 
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?
189
- end
190
-
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
120
+ def output_result(result)
198
121
  @out.puts
199
122
  @out.puts(result.message)
200
123
  result.status
@@ -208,14 +131,11 @@ module Packwerk
208
131
  end
209
132
 
210
133
  def validate(_paths)
211
- warn("`packwerk validate` should be run within the application. "\
212
- "Generate the bin script using `packwerk init` and"\
213
- " use `bin/packwerk validate` instead.") unless defined?(::Rails)
214
-
215
134
  @progress_formatter.started_validation do
216
135
  checker = Packwerk::ApplicationValidator.new(
217
136
  config_file_path: @configuration.config_path,
218
- configuration: @configuration
137
+ configuration: @configuration,
138
+ environment: @environment,
219
139
  )
220
140
  result = checker.check_all
221
141
 
@@ -225,19 +145,6 @@ module Packwerk
225
145
  end
226
146
  end
227
147
 
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
148
  def list_validation_errors(result)
242
149
  @out.puts
243
150
  if result.ok?
@@ -248,13 +155,13 @@ module Packwerk
248
155
  end
249
156
  end
250
157
 
251
- sig { returns(T::Boolean) }
252
- def rails_app?
253
- if File.exist?("config/application.rb") && File.exist?("bin/rails")
254
- File.foreach("Gemfile").any? { |line| line.match?(/['"]rails['"]/) }
255
- else
256
- false
257
- end
158
+ def parse_run(paths)
159
+ ParseRun.new(
160
+ files: fetch_files_to_process(paths),
161
+ configuration: @configuration,
162
+ progress_formatter: @progress_formatter,
163
+ offenses_formatter: @offenses_formatter
164
+ )
258
165
  end
259
166
  end
260
167
  end