packwerk 1.0.0 → 1.1.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/pull_request_template.md +8 -7
  3. data/.github/workflows/ci.yml +1 -1
  4. data/.gitignore +1 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +5 -2
  7. data/README.md +5 -3
  8. data/TROUBLESHOOT.md +1 -1
  9. data/USAGE.md +56 -19
  10. data/exe/packwerk +1 -1
  11. data/lib/packwerk.rb +3 -3
  12. data/lib/packwerk/application_load_paths.rb +68 -0
  13. data/lib/packwerk/application_validator.rb +96 -70
  14. data/lib/packwerk/association_inspector.rb +50 -20
  15. data/lib/packwerk/cache_deprecated_references.rb +55 -0
  16. data/lib/packwerk/checker.rb +23 -0
  17. data/lib/packwerk/checking_deprecated_references.rb +5 -2
  18. data/lib/packwerk/cli.rb +65 -56
  19. data/lib/packwerk/commands/detect_stale_violations_command.rb +60 -0
  20. data/lib/packwerk/commands/offense_progress_marker.rb +24 -0
  21. data/lib/packwerk/commands/result.rb +13 -0
  22. data/lib/packwerk/commands/update_deprecations_command.rb +81 -0
  23. data/lib/packwerk/configuration.rb +6 -34
  24. data/lib/packwerk/const_node_inspector.rb +28 -17
  25. data/lib/packwerk/dependency_checker.rb +16 -5
  26. data/lib/packwerk/deprecated_references.rb +24 -1
  27. data/lib/packwerk/detect_stale_deprecated_references.rb +14 -0
  28. data/lib/packwerk/file_processor.rb +4 -4
  29. data/lib/packwerk/formatters/offenses_formatter.rb +48 -0
  30. data/lib/packwerk/formatters/progress_formatter.rb +6 -2
  31. data/lib/packwerk/generators/application_validation.rb +2 -2
  32. data/lib/packwerk/generators/templates/package.yml +4 -0
  33. data/lib/packwerk/generators/templates/packwerk +2 -2
  34. data/lib/packwerk/generators/templates/packwerk.yml.erb +1 -1
  35. data/lib/packwerk/inflector.rb +17 -8
  36. data/lib/packwerk/node.rb +78 -39
  37. data/lib/packwerk/node_processor.rb +14 -3
  38. data/lib/packwerk/node_processor_factory.rb +39 -0
  39. data/lib/packwerk/offense.rb +4 -6
  40. data/lib/packwerk/output_style.rb +20 -0
  41. data/lib/packwerk/output_styles/coloured.rb +29 -0
  42. data/lib/packwerk/output_styles/plain.rb +26 -0
  43. data/lib/packwerk/package.rb +8 -1
  44. data/lib/packwerk/package_set.rb +13 -5
  45. data/lib/packwerk/parsed_constant_definitions.rb +4 -4
  46. data/lib/packwerk/parsers/erb.rb +4 -0
  47. data/lib/packwerk/parsers/factory.rb +10 -1
  48. data/lib/packwerk/privacy_checker.rb +26 -5
  49. data/lib/packwerk/run_context.rb +70 -46
  50. data/lib/packwerk/sanity_checker.rb +1 -1
  51. data/lib/packwerk/spring_command.rb +1 -1
  52. data/lib/packwerk/updating_deprecated_references.rb +2 -39
  53. data/lib/packwerk/version.rb +1 -1
  54. data/packwerk.gemspec +2 -2
  55. metadata +15 -8
  56. data/lib/packwerk/output_styles.rb +0 -41
  57. data/static/packwerk-check-demo.png +0 -0
  58. data/static/packwerk_check.gif +0 -0
  59. data/static/packwerk_check_violation.gif +0 -0
  60. data/static/packwerk_update.gif +0 -0
  61. data/static/packwerk_validate.gif +0 -0
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "packwerk/constant_name_inspector"
@@ -7,38 +7,68 @@ require "packwerk/node"
7
7
  module Packwerk
8
8
  # Extracts the implicit constant reference from an active record association
9
9
  class AssociationInspector
10
+ extend T::Sig
10
11
  include ConstantNameInspector
11
12
 
12
- RAILS_ASSOCIATIONS = %i(
13
- belongs_to
14
- has_many
15
- has_one
16
- has_and_belongs_to_many
17
- ).to_set
13
+ CustomAssociations = T.type_alias { T.any(T::Array[Symbol], T::Set[Symbol]) }
18
14
 
19
- def initialize(inflector: Inflector.new, custom_associations: Set.new)
15
+ RAILS_ASSOCIATIONS = T.let(
16
+ %i(
17
+ belongs_to
18
+ has_many
19
+ has_one
20
+ has_and_belongs_to_many
21
+ ).to_set,
22
+ CustomAssociations
23
+ )
24
+
25
+ sig { params(inflector: Inflector, custom_associations: CustomAssociations).void }
26
+ def initialize(inflector:, custom_associations: Set.new)
20
27
  @inflector = inflector
21
- @associations = RAILS_ASSOCIATIONS + custom_associations
28
+ @associations = T.let(RAILS_ASSOCIATIONS + custom_associations, CustomAssociations)
22
29
  end
23
30
 
31
+ sig do
32
+ override
33
+ .params(node: AST::Node, ancestors: T::Array[AST::Node])
34
+ .returns(T.nilable(String))
35
+ end
24
36
  def constant_name_from_node(node, ancestors:)
25
- return unless Node.type(node) == Node::METHOD_CALL
26
-
27
- method_name = Node.method_name(node)
28
- return nil unless @associations.include?(method_name)
37
+ return unless Node.method_call?(node)
38
+ return unless association?(node)
29
39
 
30
40
  arguments = Node.method_arguments(node)
31
- association_name = Node.literal_value(arguments[0]) if Node.type(arguments[0]) == Node::SYMBOL
32
- return nil unless association_name
41
+ return unless (association_name = association_name(arguments))
33
42
 
34
- association_options = arguments.detect { |n| Node.type(n) == Node::HASH }
35
- class_name_node = Node.value_from_hash(association_options, :class_name) if association_options
36
-
37
- if class_name_node
38
- Node.literal_value(class_name_node) if Node.type(class_name_node) == Node::STRING
43
+ if (class_name_node = custom_class_name(arguments))
44
+ return unless Node.string?(class_name_node)
45
+ Node.literal_value(class_name_node)
39
46
  else
40
47
  @inflector.classify(association_name.to_s)
41
48
  end
42
49
  end
50
+
51
+ private
52
+
53
+ sig { params(node: AST::Node).returns(T::Boolean) }
54
+ def association?(node)
55
+ method_name = Node.method_name(node)
56
+ @associations.include?(method_name)
57
+ end
58
+
59
+ sig { params(arguments: T::Array[AST::Node]).returns(T.nilable(AST::Node)) }
60
+ def custom_class_name(arguments)
61
+ association_options = arguments.detect { |n| Node.hash?(n) }
62
+ return unless association_options
63
+
64
+ Node.value_from_hash(association_options, :class_name)
65
+ end
66
+
67
+ sig { params(arguments: T::Array[AST::Node]).returns(T.any(T.nilable(Symbol), T.nilable(String))) }
68
+ def association_name(arguments)
69
+ return unless Node.symbol?(arguments[0])
70
+
71
+ Node.literal_value(arguments[0])
72
+ end
43
73
  end
44
74
  end
@@ -0,0 +1,55 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "packwerk/deprecated_references"
7
+ require "packwerk/reference"
8
+ require "packwerk/reference_lister"
9
+ require "packwerk/violation_type"
10
+
11
+ module Packwerk
12
+ class CacheDeprecatedReferences
13
+ extend T::Sig
14
+ extend T::Helpers
15
+ include ReferenceLister
16
+ abstract!
17
+
18
+ sig do
19
+ params(
20
+ root_path: String,
21
+ deprecated_references: T::Hash[Packwerk::Package, Packwerk::DeprecatedReferences]
22
+ ).void
23
+ end
24
+ def initialize(root_path, deprecated_references = {})
25
+ @root_path = root_path
26
+ @deprecated_references = T.let(deprecated_references, T::Hash[Packwerk::Package, Packwerk::DeprecatedReferences])
27
+ end
28
+
29
+ sig do
30
+ params(reference: Packwerk::Reference, violation_type: ViolationType)
31
+ .returns(T::Boolean)
32
+ .override
33
+ end
34
+ def listed?(reference, violation_type:)
35
+ deprecated_references = deprecated_references_for(reference.source_package)
36
+ deprecated_references.add_entries(reference, violation_type.serialize)
37
+ true
38
+ end
39
+
40
+ private
41
+
42
+ sig { params(package: Packwerk::Package).returns(Packwerk::DeprecatedReferences) }
43
+ def deprecated_references_for(package)
44
+ @deprecated_references[package] ||= Packwerk::DeprecatedReferences.new(
45
+ package,
46
+ deprecated_references_file_for(package),
47
+ )
48
+ end
49
+
50
+ sig { params(package: Packwerk::Package).returns(String) }
51
+ def deprecated_references_file_for(package)
52
+ File.join(@root_path, package.name, "deprecated_references.yml")
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,23 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "packwerk/reference_lister"
6
+
7
+ module Packwerk
8
+ module Checker
9
+ extend T::Sig
10
+ extend T::Helpers
11
+
12
+ interface!
13
+
14
+ sig { returns(ViolationType).abstract }
15
+ def violation_type; end
16
+
17
+ sig { params(reference: Reference, reference_lister: ReferenceLister).returns(T::Boolean).abstract }
18
+ def invalid_reference?(reference, reference_lister); end
19
+
20
+ sig { params(reference: Reference).returns(String).abstract }
21
+ def message_for(reference); end
22
+ end
23
+ end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
@@ -10,9 +10,10 @@ module Packwerk
10
10
  extend T::Sig
11
11
  include ReferenceLister
12
12
 
13
+ sig { params(root_path: String).void }
13
14
  def initialize(root_path)
14
15
  @root_path = root_path
15
- @deprecated_references = {}
16
+ @deprecated_references = T.let({}, T::Hash[Packwerk::Package, Packwerk::DeprecatedReferences])
16
17
  end
17
18
 
18
19
  sig do
@@ -26,6 +27,7 @@ module Packwerk
26
27
 
27
28
  private
28
29
 
30
+ sig { params(source_package: Packwerk::Package).returns(Packwerk::DeprecatedReferences) }
29
31
  def deprecated_references_for(source_package)
30
32
  @deprecated_references[source_package] ||= Packwerk::DeprecatedReferences.new(
31
33
  source_package,
@@ -33,6 +35,7 @@ module Packwerk
33
35
  )
34
36
  end
35
37
 
38
+ sig { params(package: Packwerk::Package).returns(String) }
36
39
  def deprecated_references_file_for(package)
37
40
  File.join(@root_path, package.name, "deprecated_references.yml")
38
41
  end
@@ -3,25 +3,45 @@
3
3
  require "benchmark"
4
4
  require "sorbet-runtime"
5
5
 
6
+ require "packwerk/application_load_paths"
6
7
  require "packwerk/application_validator"
7
8
  require "packwerk/configuration"
8
9
  require "packwerk/files_for_processing"
10
+ require "packwerk/formatters/offenses_formatter"
9
11
  require "packwerk/formatters/progress_formatter"
10
12
  require "packwerk/inflector"
11
- require "packwerk/output_styles"
13
+ require "packwerk/output_style"
14
+ require "packwerk/output_styles/plain"
12
15
  require "packwerk/run_context"
13
16
  require "packwerk/updating_deprecated_references"
17
+ require "packwerk/checking_deprecated_references"
18
+ require "packwerk/commands/detect_stale_violations_command"
19
+ require "packwerk/commands/update_deprecations_command"
20
+ require "packwerk/commands/offense_progress_marker"
14
21
 
15
22
  module Packwerk
16
23
  class Cli
17
24
  extend T::Sig
18
-
19
- def initialize(run_context: nil, configuration: nil, out: $stdout, err_out: $stderr, style: OutputStyles::Plain)
25
+ include OffenseProgressMarker
26
+
27
+ sig do
28
+ params(
29
+ run_context: T.nilable(Packwerk::RunContext),
30
+ configuration: T.nilable(Configuration),
31
+ out: T.any(StringIO, IO),
32
+ err_out: T.any(StringIO, IO),
33
+ style: Packwerk::OutputStyle
34
+ ).void
35
+ end
36
+ def initialize(run_context: nil, configuration: nil, out: $stdout, err_out: $stderr, style: OutputStyles::Plain.new)
20
37
  @out = out
21
38
  @err_out = err_out
22
39
  @style = style
23
40
  @configuration = configuration || Configuration.from_path
24
- @run_context = run_context || Packwerk::RunContext.from_configuration(@configuration)
41
+ @run_context = run_context || Packwerk::RunContext.from_configuration(
42
+ @configuration,
43
+ reference_lister: ::Packwerk::CheckingDeprecatedReferences.new(@configuration.root_path),
44
+ )
25
45
  @progress_formatter = Formatters::ProgressFormatter.new(@out, style: style)
26
46
  end
27
47
 
@@ -41,8 +61,12 @@ module Packwerk
41
61
  generate_configs
42
62
  when "check"
43
63
  check(args)
64
+ when "detect-stale-violations"
65
+ detect_stale_violations(args)
44
66
  when "update"
45
67
  update(args)
68
+ when "update-deprecations"
69
+ update_deprecations(args)
46
70
  when "validate"
47
71
  validate(args)
48
72
  when nil, "help"
@@ -52,7 +76,8 @@ module Packwerk
52
76
  Subcommands:
53
77
  init - set up packwerk
54
78
  check - run all checks
55
- update - update deprecated references
79
+ update - update deprecated references (deprecated, use update-deprecations instead)
80
+ update-deprecations - update deprecated references
56
81
  validate - verify integrity of packwerk and package configuration
57
82
  help - display help information about packwerk
58
83
  USAGE
@@ -89,7 +114,7 @@ module Packwerk
89
114
 
90
115
  def generate_configs
91
116
  configuration_file = Packwerk::Generators::ConfigurationFile.generate(
92
- load_paths: @configuration.load_paths,
117
+ load_paths: Packwerk::ApplicationLoadPaths.extract_relevant_paths,
93
118
  root: @configuration.root_path,
94
119
  out: @out
95
120
  )
@@ -117,31 +142,21 @@ module Packwerk
117
142
  end
118
143
 
119
144
  def update(paths)
120
- updating_deprecated_references = ::Packwerk::UpdatingDeprecatedReferences.new(@configuration.root_path)
121
- @run_context = Packwerk::RunContext.from_configuration(
122
- @configuration,
123
- reference_lister: updating_deprecated_references
124
- )
125
-
126
- files = fetch_files_to_process(paths)
127
-
128
- @progress_formatter.started(files)
129
-
130
- all_offenses = T.let([], T.untyped)
131
- execution_time = Benchmark.realtime do
132
- all_offenses = files.flat_map do |path|
133
- @run_context.file_processor.call(path).tap { |offenses| mark_progress(offenses) }
134
- end
135
-
136
- updating_deprecated_references.dump_deprecated_references_files
137
- end
138
-
139
- @out.puts # put a new line after the progress dots
140
- show_offenses(all_offenses)
141
- @progress_formatter.finished(execution_time)
142
- @out.puts("✅ `deprecated_references.yml` has been updated.")
145
+ warn("`packwerk update` is deprecated in favor of `packwerk update-deprecations`.")
146
+ update_deprecations(paths)
147
+ end
143
148
 
144
- all_offenses.empty?
149
+ def update_deprecations(paths)
150
+ update_deprecations = Commands::UpdateDeprecationsCommand.new(
151
+ files: fetch_files_to_process(paths),
152
+ configuration: @configuration,
153
+ offenses_formatter: offenses_formatter,
154
+ progress_formatter: @progress_formatter
155
+ )
156
+ result = update_deprecations.run
157
+ @out.puts
158
+ @out.puts(result.message)
159
+ result.status
145
160
  end
146
161
 
147
162
  def check(paths)
@@ -152,8 +167,8 @@ module Packwerk
152
167
  all_offenses = T.let([], T.untyped)
153
168
  execution_time = Benchmark.realtime do
154
169
  files.each do |path|
155
- @run_context.file_processor.call(path).tap do |offenses|
156
- mark_progress(offenses)
170
+ @run_context.process_file(file: path).tap do |offenses|
171
+ mark_progress(offenses: offenses, progress_formatter: @progress_formatter)
157
172
  all_offenses.concat(offenses)
158
173
  end
159
174
  end
@@ -162,13 +177,25 @@ module Packwerk
162
177
  @out.puts("Manually interrupted. Violations caught so far are listed below:")
163
178
  end
164
179
 
165
- @out.puts # put a new line after the progress dots
166
- show_offenses(all_offenses)
167
180
  @progress_formatter.finished(execution_time)
181
+ @out.puts
182
+ @out.puts(offenses_formatter.show_offenses(all_offenses))
168
183
 
169
184
  all_offenses.empty?
170
185
  end
171
186
 
187
+ def detect_stale_violations(paths)
188
+ detect_stale_violations = Commands::DetectStaleViolationsCommand.new(
189
+ files: fetch_files_to_process(paths),
190
+ configuration: @configuration,
191
+ progress_formatter: @progress_formatter
192
+ )
193
+ result = detect_stale_violations.run
194
+ @out.puts
195
+ @out.puts(result.message)
196
+ result.status
197
+ end
198
+
172
199
  def fetch_files_to_process(paths)
173
200
  files = FilesForProcessing.fetch(paths: paths, configuration: @configuration)
174
201
  abort("No files found or given. "\
@@ -176,14 +203,6 @@ module Packwerk
176
203
  files
177
204
  end
178
205
 
179
- def mark_progress(offenses)
180
- if offenses.empty?
181
- @progress_formatter.mark_as_inspected
182
- else
183
- @progress_formatter.mark_as_failed
184
- end
185
- end
186
-
187
206
  def validate(_paths)
188
207
  warn("`packwerk validate` should be run within the application. "\
189
208
  "Generate the bin script using `packwerk init` and"\
@@ -192,7 +211,6 @@ module Packwerk
192
211
  @progress_formatter.started_validation do
193
212
  checker = Packwerk::ApplicationValidator.new(
194
213
  config_file_path: @configuration.config_path,
195
- application_load_paths: @configuration.all_application_autoload_paths,
196
214
  configuration: @configuration
197
215
  )
198
216
  result = checker.check_all
@@ -203,19 +221,6 @@ module Packwerk
203
221
  end
204
222
  end
205
223
 
206
- def show_offenses(offenses)
207
- if offenses.empty?
208
- @out.puts("No offenses detected 🎉")
209
- else
210
- offenses.each do |offense|
211
- @out.puts(offense.to_s(@style))
212
- end
213
-
214
- offenses_string = Inflector.default.pluralize("offense", offenses.length)
215
- @out.puts("#{offenses.length} #{offenses_string} detected")
216
- end
217
- end
218
-
219
224
  def list_validation_errors(result)
220
225
  @out.puts
221
226
  if result.ok?
@@ -234,5 +239,9 @@ module Packwerk
234
239
  false
235
240
  end
236
241
  end
242
+
243
+ def offenses_formatter
244
+ @offenses_formatter ||= Formatters::OffensesFormatter.new(style: @style)
245
+ end
237
246
  end
238
247
  end
@@ -0,0 +1,60 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+ require "sorbet-runtime"
4
+ require "benchmark"
5
+ require "packwerk/run_context"
6
+ require "packwerk/detect_stale_deprecated_references"
7
+ require "packwerk/commands/offense_progress_marker"
8
+ require "packwerk/commands/result"
9
+
10
+ module Packwerk
11
+ module Commands
12
+ class DetectStaleViolationsCommand
13
+ extend T::Sig
14
+ include OffenseProgressMarker
15
+ def initialize(files:, configuration:, run_context: nil, progress_formatter: nil, reference_lister: nil)
16
+ @configuration = configuration
17
+ @run_context = run_context
18
+ @reference_lister = reference_lister
19
+ @progress_formatter = progress_formatter
20
+ @files = files
21
+ end
22
+
23
+ sig { returns(Result) }
24
+ def run
25
+ @progress_formatter.started(@files)
26
+
27
+ execution_time = Benchmark.realtime do
28
+ @files.flat_map do |path|
29
+ run_context.process_file(file: path).tap do |offenses|
30
+ mark_progress(offenses: offenses, progress_formatter: @progress_formatter)
31
+ end
32
+ end
33
+ end
34
+
35
+ @progress_formatter.finished(execution_time)
36
+ calculate_result
37
+ end
38
+
39
+ private
40
+
41
+ def run_context
42
+ @run_context ||= Packwerk::RunContext.from_configuration(@configuration, reference_lister: reference_lister)
43
+ end
44
+
45
+ def reference_lister
46
+ @reference_lister ||= ::Packwerk::DetectStaleDeprecatedReferences.new(@configuration.root_path)
47
+ end
48
+
49
+ sig { returns(Result) }
50
+ def calculate_result
51
+ result_status = !reference_lister.stale_violations?
52
+ message = "There were stale violations found, please run `packwerk update-deprecations`"
53
+ if result_status
54
+ message = "No stale violations detected"
55
+ end
56
+ Result.new(message: message, status: result_status)
57
+ end
58
+ end
59
+ end
60
+ end