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,20 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module OutputStyle
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ interface!
10
+
11
+ sig { abstract.returns(String) }
12
+ def reset; end
13
+
14
+ sig { abstract.returns(String) }
15
+ def filename; end
16
+
17
+ sig { abstract.returns(String) }
18
+ def error; end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module OutputStyles
6
+ # See https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit for ANSI escape colour codes
7
+ class Coloured
8
+ extend T::Sig
9
+ include OutputStyle
10
+
11
+ sig { override.returns(String) }
12
+ def reset
13
+ "\033[m"
14
+ end
15
+
16
+ sig { override.returns(String) }
17
+ def filename
18
+ # 36 is foreground cyan
19
+ "\033[36m"
20
+ end
21
+
22
+ sig { override.returns(String) }
23
+ def error
24
+ # 31 is foreground red
25
+ "\033[31m"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module OutputStyles
6
+ class Plain
7
+ extend T::Sig
8
+ include OutputStyle
9
+
10
+ sig { override.returns(String) }
11
+ def reset
12
+ ""
13
+ end
14
+
15
+ sig { override.returns(String) }
16
+ def filename
17
+ ""
18
+ end
19
+
20
+ sig { override.returns(String) }
21
+ def error
22
+ ""
23
+ end
24
+ end
25
+ end
26
+ end
@@ -33,13 +33,20 @@ module Packwerk
33
33
  end
34
34
 
35
35
  def public_path
36
- @public_path ||= File.join(@name, "app/public/")
36
+ @public_path ||= File.join(@name, user_defined_public_path || "app/public/")
37
37
  end
38
38
 
39
39
  def public_path?(path)
40
40
  path.start_with?(public_path)
41
41
  end
42
42
 
43
+ def user_defined_public_path
44
+ return unless @config["public_path"]
45
+ return @config["public_path"] if @config["public_path"].end_with?("/")
46
+
47
+ @config["public_path"] + "/"
48
+ end
49
+
43
50
  def <=>(other)
44
51
  return nil unless other.is_a?(self.class)
45
52
  name <=> other.name
@@ -25,13 +25,20 @@ module Packwerk
25
25
  new(packages)
26
26
  end
27
27
 
28
- private
29
-
30
28
  def package_paths(root_path, package_pathspec)
31
- Dir.glob(File.join(root_path, package_pathspec, PACKAGE_CONFIG_FILENAME))
32
- .map! { |path| Pathname.new(path) }
29
+ bundle_path_match = Bundler.bundle_path.join("**").to_s
30
+
31
+ glob_patterns = Array(package_pathspec).map do |pathspec|
32
+ File.join(root_path, pathspec, PACKAGE_CONFIG_FILENAME)
33
+ end
34
+
35
+ Dir.glob(glob_patterns)
36
+ .map { |path| Pathname.new(path).cleanpath }
37
+ .reject { |path| path.realpath.fnmatch(bundle_path_match) }
33
38
  end
34
39
 
40
+ private
41
+
35
42
  def create_root_package_if_none_in(packages)
36
43
  return if packages.any?(&:root?)
37
44
  packages << Package.new(name: Package::ROOT_PACKAGE_NAME, config: nil)
@@ -53,7 +60,8 @@ module Packwerk
53
60
  end
54
61
 
55
62
  def package_from_path(file_path)
56
- @packages.values.find { |package| package.package_path?(file_path) }
63
+ path_string = file_path.to_s
64
+ @packages.values.find { |package| package.package_path?(path_string) }
57
65
  end
58
66
  end
59
67
  end
@@ -28,8 +28,8 @@ module Packwerk
28
28
 
29
29
  fully_qualified_constant_name = "::#{constant_name}"
30
30
 
31
- possible_namespaces = namespace_path.reduce([""]) do |acc, current|
32
- acc << acc.last + "::" + current
31
+ possible_namespaces = namespace_path.each_with_object([""]) do |current, acc|
32
+ acc << "#{acc.last}::#{current}" if acc.last && current
33
33
  end
34
34
 
35
35
  possible_namespaces.map { |namespace| namespace + fully_qualified_constant_name }
@@ -38,12 +38,12 @@ module Packwerk
38
38
  private
39
39
 
40
40
  def collect_local_definitions_from_root(node, current_namespace_path = [])
41
- if Node.type(node) == Node::CONSTANT_ASSIGNMENT
41
+ if Node.constant_assignment?(node)
42
42
  add_definition(Node.constant_name(node), current_namespace_path, Node.name_location(node))
43
43
  elsif Node.module_name_from_definition(node)
44
44
  # handle compact constant nesting (e.g. "module Sales::Order")
45
45
  tempnode = node
46
- while (tempnode = Node.each_child(tempnode).find { |n| Node.type(n) == Node::CONSTANT })
46
+ while (tempnode = Node.each_child(tempnode).find { |n| Node.constant?(n) })
47
47
  add_definition(Node.constant_name(tempnode), current_namespace_path, Node.name_location(tempnode))
48
48
  end
49
49
 
@@ -19,6 +19,10 @@ module Packwerk
19
19
  def call(io:, file_path: "<unknown>")
20
20
  buffer = Parser::Source::Buffer.new(file_path)
21
21
  buffer.source = io.read
22
+ parse_buffer(buffer, file_path: file_path)
23
+ end
24
+
25
+ def parse_buffer(buffer, file_path:)
22
26
  parser = @parser_class.new(buffer, template_language: :html)
23
27
  to_ruby_ast(parser.ast, file_path)
24
28
  rescue EncodingError => e
@@ -26,9 +26,18 @@ module Packwerk
26
26
  when RUBY_REGEX
27
27
  @ruby_parser ||= Ruby.new
28
28
  when ERB_REGEX
29
- @erb_parser ||= Erb.new
29
+ @erb_parser ||= erb_parser_class.new
30
30
  end
31
31
  end
32
+
33
+ def erb_parser_class
34
+ @erb_parser_class ||= Erb
35
+ end
36
+
37
+ def erb_parser_class=(klass)
38
+ @erb_parser_class = klass
39
+ @erb_parser = nil
40
+ end
32
41
  end
33
42
  end
34
43
  end
@@ -1,28 +1,39 @@
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 PrivacyChecker
9
+ extend T::Sig
10
+ include Checker
11
+
12
+ sig { override.returns(Packwerk::ViolationType) }
8
13
  def violation_type
9
14
  ViolationType::Privacy
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 if reference.constant.public?
23
+ return false if reference.constant.public?
14
24
 
15
25
  privacy_option = reference.constant.package.enforce_privacy
16
- return if enforcement_disabled?(privacy_option)
26
+ return false if enforcement_disabled?(privacy_option)
17
27
 
18
- return unless privacy_option == true ||
28
+ return false unless privacy_option == true ||
19
29
  explicitly_private_constant?(reference.constant, explicitly_private_constants: privacy_option)
20
30
 
21
- return if reference_lister.listed?(reference, violation_type: violation_type)
31
+ return false if reference_lister.listed?(reference, violation_type: violation_type)
22
32
 
23
33
  true
24
34
  end
25
35
 
36
+ sig { override.params(reference: Packwerk::Reference).returns(String) }
26
37
  def message_for(reference)
27
38
  source_desc = reference.source_package ? "'#{reference.source_package}'" : "here"
28
39
  "Privacy violation: '#{reference.constant.name}' is private to '#{reference.constant.package}' but " \
@@ -32,12 +43,22 @@ module Packwerk
32
43
 
33
44
  private
34
45
 
46
+ sig do
47
+ params(
48
+ constant: ConstantDiscovery::ConstantContext,
49
+ explicitly_private_constants: T::Array[String]
50
+ ).returns(T::Boolean)
51
+ end
35
52
  def explicitly_private_constant?(constant, explicitly_private_constants:)
36
53
  explicitly_private_constants.include?(constant.name) ||
37
54
  # nested constants
38
55
  explicitly_private_constants.any? { |epc| constant.name.start_with?(epc + "::") }
39
56
  end
40
57
 
58
+ sig do
59
+ params(privacy_option: T.nilable(T.any(T::Boolean, T::Array[String])))
60
+ .returns(T::Boolean)
61
+ end
41
62
  def enforcement_disabled?(privacy_option)
42
63
  [false, nil].include?(privacy_option)
43
64
  end
@@ -1,47 +1,49 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require "active_support/inflector"
5
4
  require "constant_resolver"
6
5
 
7
6
  require "packwerk/association_inspector"
8
- require "packwerk/checking_deprecated_references"
9
7
  require "packwerk/constant_discovery"
10
8
  require "packwerk/const_node_inspector"
11
9
  require "packwerk/dependency_checker"
12
10
  require "packwerk/file_processor"
13
- require "packwerk/node_processor"
11
+ require "packwerk/inflector"
14
12
  require "packwerk/package_set"
15
13
  require "packwerk/privacy_checker"
16
14
  require "packwerk/reference_extractor"
15
+ require "packwerk/node_processor_factory"
17
16
 
18
17
  module Packwerk
19
18
  class RunContext
19
+ extend T::Sig
20
+
20
21
  attr_reader(
21
- :checkers,
22
- :constant_name_inspectors,
23
- :context_provider,
24
22
  :root_path,
25
- :file_processor,
26
- :node_processor_class,
27
- :reference_lister
23
+ :load_paths,
24
+ :package_paths,
25
+ :inflector,
26
+ :custom_associations,
27
+ :checker_classes,
28
28
  )
29
29
 
30
+ attr_accessor :reference_lister
31
+
30
32
  DEFAULT_CHECKERS = [
31
33
  ::Packwerk::DependencyChecker,
32
34
  ::Packwerk::PrivacyChecker,
33
35
  ]
34
36
 
35
37
  class << self
36
- def from_configuration(configuration, reference_lister: nil)
37
- default_reference_lister = reference_lister ||
38
- ::Packwerk::CheckingDeprecatedReferences.new(configuration.root_path)
38
+ def from_configuration(configuration, reference_lister:)
39
+ inflector = ::Packwerk::Inflector.from_file(configuration.inflections_file)
39
40
  new(
40
41
  root_path: configuration.root_path,
41
42
  load_paths: configuration.load_paths,
42
- inflector: ActiveSupport::Inflector,
43
+ package_paths: configuration.package_paths,
44
+ inflector: inflector,
43
45
  custom_associations: configuration.custom_associations,
44
- reference_lister: default_reference_lister,
46
+ reference_lister: reference_lister,
45
47
  )
46
48
  end
47
49
  end
@@ -53,51 +55,73 @@ module Packwerk
53
55
  inflector: nil,
54
56
  custom_associations: [],
55
57
  checker_classes: DEFAULT_CHECKERS,
56
- node_processor_class: NodeProcessor,
57
- reference_lister: nil
58
+ reference_lister:
58
59
  )
59
- @root_path = File.expand_path(root_path)
60
+ @root_path = root_path
61
+ @load_paths = load_paths
62
+ @package_paths = package_paths
63
+ @inflector = inflector
64
+ @custom_associations = custom_associations
65
+ @checker_classes = checker_classes
66
+ @reference_lister = reference_lister
67
+ end
60
68
 
61
- resolver = ConstantResolver.new(
62
- root_path: @root_path,
63
- load_paths: load_paths,
64
- inflector: inflector,
65
- )
69
+ sig { params(file: String).returns(T::Array[T.nilable(::Packwerk::Offense)]) }
70
+ def process_file(file:)
71
+ file_processor.call(file)
72
+ end
73
+
74
+ private
75
+
76
+ sig { returns(FileProcessor) }
77
+ def file_processor
78
+ @file_processor ||= FileProcessor.new(node_processor_factory: node_processor_factory)
79
+ end
66
80
 
67
- package_set = ::Packwerk::PackageSet.load_all_from(@root_path, package_pathspec: package_paths)
81
+ sig { returns(NodeProcessorFactory) }
82
+ def node_processor_factory
83
+ NodeProcessorFactory.new(
84
+ context_provider: context_provider,
85
+ checkers: checkers,
86
+ root_path: root_path,
87
+ constant_name_inspectors: constant_name_inspectors,
88
+ reference_lister: reference_lister
89
+ )
90
+ end
68
91
 
69
- @context_provider = ::Packwerk::ConstantDiscovery.new(
92
+ sig { returns(ConstantDiscovery) }
93
+ def context_provider
94
+ ::Packwerk::ConstantDiscovery.new(
70
95
  constant_resolver: resolver,
71
96
  packages: package_set
72
97
  )
98
+ end
73
99
 
74
- @reference_lister = reference_lister || ::Packwerk::CheckingDeprecatedReferences.new(@root_path)
100
+ sig { returns(ConstantResolver) }
101
+ def resolver
102
+ ConstantResolver.new(
103
+ root_path: root_path,
104
+ load_paths: load_paths,
105
+ inflector: inflector,
106
+ )
107
+ end
75
108
 
76
- @checkers = checker_classes.map(&:new)
109
+ sig { returns(PackageSet) }
110
+ def package_set
111
+ ::Packwerk::PackageSet.load_all_from(root_path, package_pathspec: package_paths)
112
+ end
77
113
 
78
- @constant_name_inspectors = [
114
+ sig { returns(T::Array[Checker]) }
115
+ def checkers
116
+ checker_classes.map(&:new)
117
+ end
118
+
119
+ sig { returns(T::Array[ConstantNameInspector]) }
120
+ def constant_name_inspectors
121
+ [
79
122
  ::Packwerk::ConstNodeInspector.new,
80
123
  ::Packwerk::AssociationInspector.new(inflector: inflector, custom_associations: custom_associations),
81
124
  ]
82
-
83
- @node_processor_class = node_processor_class
84
- @file_processor = FileProcessor.new(run_context: self)
85
- end
86
-
87
- def node_processor_for(filename:, ast_node:)
88
- reference_extractor = ::Packwerk::ReferenceExtractor.new(
89
- context_provider: context_provider,
90
- constant_name_inspectors: constant_name_inspectors,
91
- root_node: ast_node,
92
- root_path: root_path,
93
- )
94
-
95
- node_processor_class.new(
96
- reference_extractor: reference_extractor,
97
- reference_lister: @reference_lister,
98
- filename: filename,
99
- checkers: checkers,
100
- )
101
125
  end
102
126
  end
103
127
  end
@@ -1,4 +1,4 @@
1
- # typed: ignore
1
+ # typed: false
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "packwerk/application_validator"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- # typed: ignore
2
+ # typed: false
3
3
 
4
4
  require "spring/commands"
5
5
 
@@ -1,51 +1,14 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
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"
4
+ require "packwerk/cache_deprecated_references"
10
5
 
11
6
  module Packwerk
12
- class UpdatingDeprecatedReferences
13
- extend T::Sig
14
- include ReferenceLister
15
-
16
- def initialize(root_path, deprecated_references = {})
17
- @root_path = root_path
18
- @deprecated_references = deprecated_references
19
- end
20
-
21
- sig do
22
- params(reference: Packwerk::Reference, violation_type: ViolationType)
23
- .returns(T::Boolean)
24
- .override
25
- end
26
- def listed?(reference, violation_type:)
27
- deprecated_references = deprecated_references_for(reference.source_package)
28
- deprecated_references.add_entries(reference, violation_type.serialize)
29
- true
30
- end
31
-
7
+ class UpdatingDeprecatedReferences < CacheDeprecatedReferences
32
8
  def dump_deprecated_references_files
33
9
  @deprecated_references.each do |_, deprecated_references_file|
34
10
  deprecated_references_file.dump
35
11
  end
36
12
  end
37
-
38
- private
39
-
40
- def deprecated_references_for(package)
41
- @deprecated_references[package] ||= Packwerk::DeprecatedReferences.new(
42
- package,
43
- deprecated_references_file_for(package),
44
- )
45
- end
46
-
47
- def deprecated_references_file_for(package)
48
- File.join(@root_path, package.name, "deprecated_references.yml")
49
- end
50
13
  end
51
14
  end