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
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+ require "sorbet-runtime"
4
+ require "packwerk/formatters/progress_formatter"
5
+
6
+ module Packwerk
7
+ module OffenseProgressMarker
8
+ extend T::Sig
9
+
10
+ sig do
11
+ params(
12
+ offenses: T::Array[T.nilable(::Packwerk::Offense)],
13
+ progress_formatter: ::Packwerk::Formatters::ProgressFormatter
14
+ ).void
15
+ end
16
+ def mark_progress(offenses:, progress_formatter:)
17
+ if offenses.empty?
18
+ progress_formatter.mark_as_inspected
19
+ else
20
+ progress_formatter.mark_as_failed
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Packwerk
7
+ module Commands
8
+ class Result < T::Struct
9
+ prop :message, String
10
+ prop :status, T::Boolean
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,81 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "benchmark"
6
+
7
+ require "packwerk/commands/offense_progress_marker"
8
+ require "packwerk/commands/result"
9
+ require "packwerk/run_context"
10
+ require "packwerk/updating_deprecated_references"
11
+
12
+ module Packwerk
13
+ module Commands
14
+ class UpdateDeprecationsCommand
15
+ extend T::Sig
16
+ include OffenseProgressMarker
17
+
18
+ sig do
19
+ params(
20
+ files: T::Enumerable[String],
21
+ configuration: Configuration,
22
+ offenses_formatter: Formatters::OffensesFormatter,
23
+ progress_formatter: Formatters::ProgressFormatter
24
+ ).void
25
+ end
26
+ def initialize(files:, configuration:, offenses_formatter:, progress_formatter:)
27
+ @files = files
28
+ @configuration = configuration
29
+ @progress_formatter = progress_formatter
30
+ @offenses_formatter = offenses_formatter
31
+ @updating_deprecated_references = T.let(nil, T.nilable(UpdatingDeprecatedReferences))
32
+ @run_context = T.let(nil, T.nilable(RunContext))
33
+ end
34
+
35
+ sig { returns(Result) }
36
+ def run
37
+ @progress_formatter.started(@files)
38
+
39
+ all_offenses = T.let([], T.untyped)
40
+ execution_time = Benchmark.realtime do
41
+ all_offenses = @files.flat_map do |path|
42
+ run_context.process_file(file: path).tap do |offenses|
43
+ mark_progress(offenses: offenses, progress_formatter: @progress_formatter)
44
+ end
45
+ end
46
+
47
+ updating_deprecated_references.dump_deprecated_references_files
48
+ end
49
+
50
+ @progress_formatter.finished(execution_time)
51
+ calculate_result(all_offenses)
52
+ end
53
+
54
+ private
55
+
56
+ sig { returns(RunContext) }
57
+ def run_context
58
+ @run_context ||= RunContext.from_configuration(
59
+ @configuration,
60
+ reference_lister: updating_deprecated_references
61
+ )
62
+ end
63
+
64
+ sig { returns(UpdatingDeprecatedReferences) }
65
+ def updating_deprecated_references
66
+ @updating_deprecated_references ||= UpdatingDeprecatedReferences.new(@configuration.root_path)
67
+ end
68
+
69
+ sig { params(all_offenses: T::Array[T.nilable(::Packwerk::Offense)]).returns(Result) }
70
+ def calculate_result(all_offenses)
71
+ result_status = all_offenses.empty?
72
+ message = <<~EOS
73
+ #{@offenses_formatter.show_offenses(all_offenses)}
74
+ ✅ `deprecated_references.yml` has been updated.
75
+ EOS
76
+
77
+ Result.new(message: message, status: result_status)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -22,13 +22,16 @@ 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
 
29
32
  DEFAULT_CONFIG_PATH = "packwerk.yml"
30
33
  DEFAULT_INCLUDE_GLOBS = ["**/*.{rb,rake,erb}"]
31
- DEFAULT_EXCLUDE_GLOBS = ["{bin,node_modules,script,tmp}/**/*"]
34
+ DEFAULT_EXCLUDE_GLOBS = ["{bin,node_modules,script,tmp,vendor}/**/*"]
32
35
 
33
36
  attr_reader(
34
37
  :include, :exclude, :root_path, :package_paths, :custom_associations, :load_paths, :inflections_file,
@@ -42,41 +45,10 @@ 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"] || all_application_autoload_paths
48
+ @load_paths = configs["load_paths"] || []
46
49
  @inflections_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
47
50
 
48
51
  @config_path = config_path
49
52
  end
50
-
51
- def all_application_autoload_paths
52
- return [] unless defined?(::Rails)
53
-
54
- all_paths = Rails.application.railties
55
- .select { |railtie| railtie.is_a?(Rails::Engine) }
56
- .push(Rails.application)
57
- .flat_map do |engine|
58
- (engine.config.autoload_paths + engine.config.eager_load_paths + engine.config.autoload_once_paths).uniq
59
- end
60
-
61
- all_paths = all_paths.map do |path_string|
62
- # ignore paths outside of the Rails root
63
- path = Pathname.new(path_string)
64
- if path.exist? && path.realpath.fnmatch(Rails.root.join("**").to_s)
65
- path.relative_path_from(Rails.root).to_s
66
- end
67
- end
68
-
69
- all_paths.compact.tap do |paths|
70
- if paths.empty?
71
- raise <<~EOS
72
- No autoload paths have been set up in your Rails app. This is likely a bug, and
73
- packwerk is unlikely to work correctly without any autoload paths.
74
-
75
- You can follow the Rails guides on setting up load paths, or manually configure
76
- them in `packwerk.yml` with `load_paths`.
77
- EOS
78
- end
79
- end
80
- end
81
53
  end
82
54
  end
@@ -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"
@@ -6,23 +6,21 @@ require "packwerk/constant_name_inspector"
6
6
  module Packwerk
7
7
  # Extracts a constant name from an AST node of type :const
8
8
  class ConstNodeInspector
9
+ extend T::Sig
9
10
  include ConstantNameInspector
10
11
 
12
+ sig do
13
+ override
14
+ .params(node: AST::Node, ancestors: T::Array[AST::Node])
15
+ .returns(T.nilable(String))
16
+ end
11
17
  def constant_name_from_node(node, ancestors:)
12
- return nil unless Node.type(node) == Node::CONSTANT
13
-
14
- # Only process the root `const` node for namespaced constant references. For example, in the
15
- # reference `Spam::Eggs::Thing`, we only process the const node associated with `Spam`.
18
+ return nil unless Node.constant?(node)
16
19
  parent = ancestors.first
17
- return nil if parent && Node.type(parent) == Node::CONSTANT
20
+ return nil unless root_constant?(parent)
18
21
 
19
- if constant_in_module_or_class_definition?(node, parent: parent)
20
- # We're defining a class with this name, in which case the constant is implicitly fully qualified by its
21
- # enclosing namespace
22
- name = Node.parent_module_name(ancestors: ancestors)
23
- name ||= Node.enclosing_namespace_path(node, ancestors: ancestors).push(Node.constant_name(node)).join("::")
24
-
25
- "::" + name
22
+ if parent && constant_in_module_or_class_definition?(node, parent: parent)
23
+ fully_qualify_constant(ancestors)
26
24
  else
27
25
  begin
28
26
  Node.constant_name(node)
@@ -34,11 +32,24 @@ module Packwerk
34
32
 
35
33
  private
36
34
 
35
+ # Only process the root `const` node for namespaced constant references. For example, in the
36
+ # reference `Spam::Eggs::Thing`, we only process the const node associated with `Spam`.
37
+ sig { params(parent: T.nilable(AST::Node)).returns(T::Boolean) }
38
+ def root_constant?(parent)
39
+ !(parent && Node.constant?(parent))
40
+ end
41
+
42
+ sig { params(node: AST::Node, parent: AST::Node).returns(T.nilable(T::Boolean)) }
37
43
  def constant_in_module_or_class_definition?(node, parent:)
38
- if parent
39
- parent_name = Node.module_name_from_definition(parent)
40
- parent_name && parent_name == Node.constant_name(node)
41
- end
44
+ parent_name = Node.module_name_from_definition(parent)
45
+ parent_name && parent_name == Node.constant_name(node)
46
+ end
47
+
48
+ sig { params(ancestors: T::Array[AST::Node]).returns(String) }
49
+ def fully_qualify_constant(ancestors)
50
+ # We're defining a class with this name, in which case the constant is implicitly fully qualified by its
51
+ # enclosing namespace
52
+ "::" + Node.parent_module_name(ancestors: ancestors)
42
53
  end
43
54
  end
44
55
  end
@@ -1,22 +1,33 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "packwerk/violation_type"
5
+ require "packwerk/checker"
5
6
 
6
7
  module Packwerk
7
8
  class DependencyChecker
9
+ extend T::Sig
10
+ include Checker
11
+
12
+ sig { override.returns(ViolationType) }
8
13
  def violation_type
9
14
  ViolationType::Dependency
10
15
  end
11
16
 
17
+ sig do
18
+ override
19
+ .params(reference: Packwerk::Reference, reference_lister: Packwerk::ReferenceLister)
20
+ .returns(T::Boolean)
21
+ end
12
22
  def invalid_reference?(reference, reference_lister)
13
- return unless reference.source_package
14
- return unless reference.source_package.enforce_dependencies?
15
- return if reference.source_package.dependency?(reference.constant.package)
16
- return if reference_lister.listed?(reference, violation_type: violation_type)
23
+ return false unless reference.source_package
24
+ return false unless reference.source_package.enforce_dependencies?
25
+ return false if reference.source_package.dependency?(reference.constant.package)
26
+ return false if reference_lister.listed?(reference, violation_type: violation_type)
17
27
  true
18
28
  end
19
29
 
30
+ sig { override.params(reference: Packwerk::Reference).returns(String) }
20
31
  def message_for(reference)
21
32
  "Dependency violation: #{reference.constant.name} belongs to '#{reference.constant.package}', but " \
22
33
  "'#{reference.source_package}' does not specify a dependency on " \
@@ -13,6 +13,7 @@ module Packwerk
13
13
  extend T::Sig
14
14
  include ReferenceLister
15
15
 
16
+ sig { params(package: Packwerk::Package, filepath: String).void }
16
17
  def initialize(package, filepath)
17
18
  @package = package
18
19
  @filepath = filepath
@@ -34,6 +35,7 @@ module Packwerk
34
35
  violated_constants_found.fetch("violations", []).include?(violation_type.serialize)
35
36
  end
36
37
 
38
+ sig { params(reference: Packwerk::Reference, violation_type: String).void }
37
39
  def add_entries(reference, violation_type)
38
40
  package_violations = @new_entries.fetch(reference.constant.package.name, {})
39
41
  entries_for_file = package_violations[reference.constant.name] ||= {}
@@ -47,6 +49,25 @@ module Packwerk
47
49
  @new_entries[reference.constant.package.name] = package_violations
48
50
  end
49
51
 
52
+ sig { returns(T::Boolean) }
53
+ def stale_violations?
54
+ prepare_entries_for_dump
55
+ deprecated_references.any? do |package, package_violations|
56
+ package_violations.any? do |constant_name, entries_for_file|
57
+ new_entries_violation_types = @new_entries.dig(package, constant_name, "violations")
58
+ return true if new_entries_violation_types.nil?
59
+ if entries_for_file["violations"].all? { |type| new_entries_violation_types.include?(type) }
60
+ stale_violations =
61
+ entries_for_file["files"] - Array(@new_entries.dig(package, constant_name, "files"))
62
+ stale_violations.present?
63
+ else
64
+ return true
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ sig { void }
50
71
  def dump
51
72
  if @new_entries.empty?
52
73
  File.delete(@filepath) if File.exist?(@filepath)
@@ -58,7 +79,7 @@ module Packwerk
58
79
  #
59
80
  # You can regenerate this file using the following command:
60
81
  #
61
- # bundle exec packwerk update #{@package.name}
82
+ # bundle exec packwerk update-deprecations #{@package.name}
62
83
  MESSAGE
63
84
  File.open(@filepath, "w") do |f|
64
85
  f.write(message)
@@ -69,6 +90,7 @@ module Packwerk
69
90
 
70
91
  private
71
92
 
93
+ sig { returns(Hash) }
72
94
  def prepare_entries_for_dump
73
95
  @new_entries.each do |package_name, package_violations|
74
96
  package_violations.each do |_, entries_for_file|
@@ -81,6 +103,7 @@ module Packwerk
81
103
  @new_entries = @new_entries.sort.to_h
82
104
  end
83
105
 
106
+ sig { returns(Hash) }
84
107
  def deprecated_references
85
108
  @deprecated_references ||= if File.exist?(@filepath)
86
109
  YAML.load_file(@filepath) || {}
@@ -0,0 +1,14 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "packwerk/cache_deprecated_references"
5
+
6
+ module Packwerk
7
+ class DetectStaleDeprecatedReferences < CacheDeprecatedReferences
8
+ extend T::Sig
9
+ sig { returns(T::Boolean) }
10
+ def stale_violations?
11
+ @deprecated_references.values.any?(&:stale_violations?)
12
+ end
13
+ end
14
+ end
@@ -15,8 +15,8 @@ module Packwerk
15
15
  end
16
16
  end
17
17
 
18
- def initialize(run_context:, parser_factory: nil)
19
- @run_context = run_context
18
+ def initialize(node_processor_factory:, parser_factory: nil)
19
+ @node_processor_factory = node_processor_factory
20
20
  @parser_factory = parser_factory || Packwerk::Parsers::Factory.instance
21
21
  end
22
22
 
@@ -32,8 +32,8 @@ module Packwerk
32
32
 
33
33
  result = []
34
34
  if node
35
- @node_processor = @run_context.node_processor_for(filename: file_path, ast_node: node)
36
- node_visitor = Packwerk::NodeVisitor.new(node_processor: @node_processor)
35
+ node_processor = @node_processor_factory.for(filename: file_path, node: node)
36
+ node_visitor = Packwerk::NodeVisitor.new(node_processor: node_processor)
37
37
 
38
38
  node_visitor.visit(node, ancestors: [], result: result)
39
39
  end
@@ -0,0 +1,48 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "benchmark"
5
+ require "sorbet-runtime"
6
+
7
+ require "packwerk/inflector"
8
+ require "packwerk/output_style"
9
+ require "packwerk/output_styles/plain"
10
+
11
+ module Packwerk
12
+ module Formatters
13
+ class OffensesFormatter
14
+ extend T::Sig
15
+
16
+ sig { params(style: OutputStyle).void }
17
+ def initialize(style: OutputStyles::Plain.new)
18
+ @style = style
19
+ end
20
+
21
+ sig { params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
22
+ def show_offenses(offenses)
23
+ return "No offenses detected 🎉" if offenses.empty?
24
+
25
+ <<~EOS
26
+ #{offenses_list(offenses)}
27
+ #{offenses_summary(offenses)}
28
+ EOS
29
+ end
30
+
31
+ private
32
+
33
+ sig { params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
34
+ def offenses_list(offenses)
35
+ offenses
36
+ .compact
37
+ .map { |offense| offense.to_s(@style) }
38
+ .join("\n")
39
+ end
40
+
41
+ sig { params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
42
+ def offenses_summary(offenses)
43
+ offenses_string = Inflector.default.pluralize("offense", offenses.length)
44
+ "#{offenses.length} #{offenses_string} detected"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -4,12 +4,16 @@
4
4
  require "benchmark"
5
5
 
6
6
  require "packwerk/inflector"
7
- require "packwerk/output_styles"
7
+ require "packwerk/output_style"
8
+ require "packwerk/output_styles/plain"
8
9
 
9
10
  module Packwerk
10
11
  module Formatters
11
12
  class ProgressFormatter
12
- def initialize(out, style: OutputStyles::Plain)
13
+ extend T::Sig
14
+
15
+ sig { params(out: T.any(StringIO, IO), style: OutputStyle).void }
16
+ def initialize(out, style: OutputStyles::Plain.new)
13
17
  @out = out
14
18
  @style = style
15
19
  end