packwerk 1.0.0 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
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