packwerk 1.3.0 → 2.0.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -0
  3. data/Gemfile.lock +11 -8
  4. data/README.md +2 -3
  5. data/TROUBLESHOOT.md +3 -3
  6. data/UPGRADING.md +54 -0
  7. data/USAGE.md +27 -53
  8. data/exe/packwerk +7 -1
  9. data/lib/packwerk/application_load_paths.rb +1 -0
  10. data/lib/packwerk/application_validator.rb +3 -54
  11. data/lib/packwerk/association_inspector.rb +1 -1
  12. data/lib/packwerk/cli.rb +37 -20
  13. data/lib/packwerk/configuration.rb +34 -4
  14. data/lib/packwerk/const_node_inspector.rb +3 -2
  15. data/lib/packwerk/constant_discovery.rb +1 -1
  16. data/lib/packwerk/constant_name_inspector.rb +1 -1
  17. data/lib/packwerk/deprecated_references.rb +19 -7
  18. data/lib/packwerk/file_processor.rb +39 -14
  19. data/lib/packwerk/files_for_processing.rb +15 -4
  20. data/lib/packwerk/formatters/offenses_formatter.rb +1 -1
  21. data/lib/packwerk/formatters/progress_formatter.rb +1 -1
  22. data/lib/packwerk/generators/configuration_file.rb +4 -19
  23. data/lib/packwerk/generators/templates/package.yml +1 -1
  24. data/lib/packwerk/generators/templates/packwerk.yml.erb +0 -6
  25. data/lib/packwerk/graph.rb +2 -0
  26. data/lib/packwerk/node.rb +1 -0
  27. data/lib/packwerk/node_processor.rb +10 -22
  28. data/lib/packwerk/node_processor_factory.rb +0 -2
  29. data/lib/packwerk/node_visitor.rb +4 -2
  30. data/lib/packwerk/package.rb +25 -4
  31. data/lib/packwerk/package_set.rb +43 -8
  32. data/lib/packwerk/parsed_constant_definitions.rb +1 -0
  33. data/lib/packwerk/reference.rb +2 -1
  34. data/lib/packwerk/reference_checking/checkers/checker.rb +21 -0
  35. data/lib/packwerk/reference_checking/checkers/dependency_checker.rb +31 -0
  36. data/lib/packwerk/reference_checking/checkers/privacy_checker.rb +58 -0
  37. data/lib/packwerk/reference_checking/reference_checker.rb +33 -0
  38. data/lib/packwerk/reference_extractor.rb +2 -2
  39. data/lib/packwerk/reference_offense.rb +1 -0
  40. data/lib/packwerk/run_context.rb +10 -7
  41. data/lib/packwerk/sanity_checker.rb +1 -1
  42. data/lib/packwerk/spring_command.rb +28 -0
  43. data/lib/packwerk/version.rb +1 -1
  44. data/lib/packwerk/violation_type.rb +1 -1
  45. data/lib/packwerk.rb +17 -12
  46. data/packwerk.gemspec +3 -1
  47. data/service.yml +0 -2
  48. data/sorbet/rbi/gems/psych@3.3.2.rbi +24 -0
  49. metadata +25 -11
  50. data/lib/packwerk/checker.rb +0 -17
  51. data/lib/packwerk/dependency_checker.rb +0 -26
  52. data/lib/packwerk/generators/inflections_file.rb +0 -43
  53. data/lib/packwerk/generators/templates/inflections.yml +0 -6
  54. data/lib/packwerk/inflections/custom.rb +0 -33
  55. data/lib/packwerk/inflections/default.rb +0 -73
  56. data/lib/packwerk/inflector.rb +0 -48
  57. data/lib/packwerk/privacy_checker.rb +0 -53
@@ -4,7 +4,7 @@
4
4
  require "ast"
5
5
 
6
6
  module Packwerk
7
- # An interface describing some object that can extract a constant name from an AST node
7
+ # An interface describing an object that can extract a constant name from an AST node.
8
8
  module ConstantNameInspector
9
9
  extend T::Sig
10
10
  extend T::Helpers
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "yaml"
@@ -7,11 +7,15 @@ module Packwerk
7
7
  class DeprecatedReferences
8
8
  extend T::Sig
9
9
 
10
+ ENTRIES_TYPE = T.type_alias do
11
+ T::Hash[String, T.untyped]
12
+ end
13
+
10
14
  sig { params(package: Packwerk::Package, filepath: String).void }
11
15
  def initialize(package, filepath)
12
16
  @package = package
13
17
  @filepath = filepath
14
- @new_entries = {}
18
+ @new_entries = T.let({}, ENTRIES_TYPE)
15
19
  end
16
20
 
17
21
  sig do
@@ -53,7 +57,7 @@ module Packwerk
53
57
  if entries_for_file["violations"].all? { |type| new_entries_violation_types.include?(type) }
54
58
  stale_violations =
55
59
  entries_for_file["files"] - Array(@new_entries.dig(package, constant_name, "files"))
56
- stale_violations.present?
60
+ stale_violations.any?
57
61
  else
58
62
  return true
59
63
  end
@@ -73,7 +77,7 @@ module Packwerk
73
77
  #
74
78
  # You can regenerate this file using the following command:
75
79
  #
76
- # packwerk update-deprecations #{@package.name}
80
+ # bin/packwerk update-deprecations #{@package.name}
77
81
  MESSAGE
78
82
  File.open(@filepath, "w") do |f|
79
83
  f.write(message)
@@ -84,7 +88,7 @@ module Packwerk
84
88
 
85
89
  private
86
90
 
87
- sig { returns(Hash) }
91
+ sig { returns(ENTRIES_TYPE) }
88
92
  def prepare_entries_for_dump
89
93
  @new_entries.each do |package_name, package_violations|
90
94
  package_violations.each do |_, entries_for_file|
@@ -97,13 +101,21 @@ module Packwerk
97
101
  @new_entries = @new_entries.sort.to_h
98
102
  end
99
103
 
100
- sig { returns(Hash) }
104
+ sig { returns(ENTRIES_TYPE) }
101
105
  def deprecated_references
106
+ @deprecated_references ||= T.let(@deprecated_references, T.nilable(ENTRIES_TYPE))
102
107
  @deprecated_references ||= if File.exist?(@filepath)
103
- YAML.load_file(@filepath) || {}
108
+ load_yaml(@filepath)
104
109
  else
105
110
  {}
106
111
  end
107
112
  end
113
+
114
+ sig { params(filepath: String).returns(ENTRIES_TYPE) }
115
+ def load_yaml(filepath)
116
+ YAML.load_file(filepath) || {}
117
+ rescue Psych::Exception
118
+ {}
119
+ end
108
120
  end
109
121
  end
@@ -1,10 +1,12 @@
1
- # typed: false
1
+ # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "ast/node"
5
5
 
6
6
  module Packwerk
7
7
  class FileProcessor
8
+ extend T::Sig
9
+
8
10
  class UnknownFileTypeResult < Offense
9
11
  def initialize(file:)
10
12
  super(file: file, message: "unknown file type")
@@ -16,24 +18,47 @@ module Packwerk
16
18
  @parser_factory = parser_factory || Packwerk::Parsers::Factory.instance
17
19
  end
18
20
 
21
+ sig do
22
+ params(file_path: String).returns(
23
+ T::Array[
24
+ T.any(
25
+ Packwerk::Reference,
26
+ Packwerk::Offense,
27
+ )
28
+ ]
29
+ )
30
+ end
19
31
  def call(file_path)
20
- parser = @parser_factory.for_path(file_path)
21
- return [UnknownFileTypeResult.new(file: file_path)] if parser.nil?
32
+ return [UnknownFileTypeResult.new(file: file_path)] if parser_for(file_path).nil?
22
33
 
23
- node = File.open(file_path, "r", external_encoding: Encoding::UTF_8) do |file|
24
- parser.call(io: file, file_path: file_path)
25
- rescue Parsers::ParseError => e
26
- return [e.result]
27
- end
34
+ node = parse_into_ast(file_path)
35
+ return [] unless node
36
+
37
+ references_from_ast(node, file_path)
38
+ rescue Parsers::ParseError => e
39
+ [e.result]
40
+ end
41
+
42
+ private
28
43
 
29
- result = []
30
- if node
31
- node_processor = @node_processor_factory.for(filename: file_path, node: node)
32
- node_visitor = Packwerk::NodeVisitor.new(node_processor: node_processor)
44
+ def references_from_ast(node, file_path)
45
+ references = []
33
46
 
34
- node_visitor.visit(node, ancestors: [], result: result)
47
+ node_processor = @node_processor_factory.for(filename: file_path, node: node)
48
+ node_visitor = Packwerk::NodeVisitor.new(node_processor: node_processor)
49
+ node_visitor.visit(node, ancestors: [], result: references)
50
+
51
+ references
52
+ end
53
+
54
+ def parse_into_ast(file_path)
55
+ File.open(file_path, "r", nil, external_encoding: Encoding::UTF_8) do |file|
56
+ parser_for(file_path).call(io: file, file_path: file_path)
35
57
  end
36
- result
58
+ end
59
+
60
+ def parser_for(file_path)
61
+ @parser_factory.for_path(file_path)
37
62
  end
38
63
  end
39
64
  end
@@ -4,14 +4,15 @@
4
4
  module Packwerk
5
5
  class FilesForProcessing
6
6
  class << self
7
- def fetch(paths:, configuration:)
8
- new(paths, configuration).files
7
+ def fetch(paths:, configuration:, ignore_nested_packages: false)
8
+ new(paths, configuration, ignore_nested_packages).files
9
9
  end
10
10
  end
11
11
 
12
- def initialize(paths, configuration)
12
+ def initialize(paths, configuration, ignore_nested_packages)
13
13
  @paths = paths
14
14
  @configuration = configuration
15
+ @ignore_nested_packages = ignore_nested_packages
15
16
  end
16
17
 
17
18
  def files
@@ -43,11 +44,21 @@ module Packwerk
43
44
  File.expand_path(glob, @configuration.root_path)
44
45
  end
45
46
 
46
- Dir.glob([File.join(path, "**", "*")]).select do |file_path|
47
+ files = Dir.glob([File.join(path, "**", "*")]).select do |file_path|
47
48
  absolute_includes.any? do |pattern|
48
49
  File.fnmatch?(pattern, file_path, File::FNM_EXTGLOB)
49
50
  end
50
51
  end
52
+
53
+ if @ignore_nested_packages
54
+ nested_packages_paths = Dir.glob(File.join(path, "*", "**", "package.yml"))
55
+ nested_packages_globs = nested_packages_paths.map { |npp| npp.gsub("package.yml", "**/*") }
56
+ nested_packages_globs.each do |glob|
57
+ files -= Dir.glob(glob)
58
+ end
59
+ end
60
+
61
+ files
51
62
  end
52
63
 
53
64
  def configured_included_files
@@ -44,7 +44,7 @@ module Packwerk
44
44
 
45
45
  sig { params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
46
46
  def offenses_summary(offenses)
47
- offenses_string = Inflector.default.pluralize("offense", offenses.length)
47
+ offenses_string = "offense".pluralize(offenses.length)
48
48
  "#{offenses.length} #{offenses_string} detected"
49
49
  end
50
50
  end
@@ -16,7 +16,7 @@ module Packwerk
16
16
 
17
17
  def started(target_files)
18
18
  files_size = target_files.size
19
- files_string = Inflector.default.pluralize("file", files_size)
19
+ files_string = "file".pluralize(files_size)
20
20
  @out.puts("📦 Packwerk is inspecting #{files_size} #{files_string}")
21
21
  end
22
22
 
@@ -11,18 +11,15 @@ module Packwerk
11
11
  CONFIGURATION_TEMPLATE_FILE_PATH = "templates/packwerk.yml.erb"
12
12
 
13
13
  class << self
14
- def generate(load_paths:, root:, out:)
15
- new(load_paths: load_paths, root: root, out: out).generate
14
+ def generate(root:, out:)
15
+ new(root: root, out: out).generate
16
16
  end
17
17
  end
18
18
 
19
- sig { params(load_paths: T::Array[String], root: String, out: T.any(StringIO, IO)).void }
20
- def initialize(load_paths:, root:, out: $stdout)
21
- @load_paths = load_paths
19
+ sig { params(root: String, out: T.any(StringIO, IO)).void }
20
+ def initialize(root:, out: $stdout)
22
21
  @root = root
23
22
  @out = out
24
-
25
- set_template_variables
26
23
  end
27
24
 
28
25
  sig { returns(T::Boolean) }
@@ -43,18 +40,6 @@ module Packwerk
43
40
 
44
41
  private
45
42
 
46
- def set_template_variables
47
- @load_paths_formatted = if @load_paths.empty?
48
- "# load_paths:\n# - 'app/models'\n"
49
- else
50
- @load_paths.map { |path| "- #{path}\n" }.join
51
- end
52
-
53
- @load_paths_comment = unless @load_paths.empty?
54
- "# These load paths were auto generated by Packwerk.\nload_paths:\n"
55
- end
56
- end
57
-
58
43
  def render
59
44
  ERB.new(template, trim_mode: "-").result(binding)
60
45
  end
@@ -1,5 +1,5 @@
1
1
  # This file represents the root package of the application
2
- # Please validate the configuration using `bin/packwerk validate` (for Rails applications) or running the auto generated
2
+ # Please validate the configuration using `packwerk validate` (for Rails applications) or running the auto generated
3
3
  # test case (for non-Rails projects). You can then use `packwerk check` to check your code.
4
4
 
5
5
  # Turn on dependency checks for this package
@@ -12,12 +12,6 @@
12
12
  # Patterns to find package configuration files
13
13
  # package_paths: "**/"
14
14
 
15
- # List of application load paths
16
- <%= @load_paths_comment -%>
17
- <%= @load_paths_formatted %>
18
15
  # List of custom associations, if any
19
16
  # custom_associations:
20
17
  # - "cache_belongs_to"
21
-
22
- # Location of inflections file
23
- # inflections_file: "config/inflections.yml"
@@ -2,7 +2,9 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
+ # A general implementation of a graph data structure with the ability to check for - and list - cycles.
5
6
  class Graph
7
+ # @param [Array<Array>] edges The edges of the graph; An edge being represented as an Array of two nodes.
6
8
  def initialize(*edges)
7
9
  @edges = edges.uniq
8
10
  @cycles = Set.new
data/lib/packwerk/node.rb CHANGED
@@ -5,6 +5,7 @@ require "parser"
5
5
  require "parser/ast/node"
6
6
 
7
7
  module Packwerk
8
+ # Convenience methods for working with AST nodes.
8
9
  module Node
9
10
  class TypeError < ArgumentError; end
10
11
  Location = Struct.new(:line, :column)
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
+ # Processes a single node in an abstract syntax tree (AST) using the provided checkers.
5
6
  class NodeProcessor
6
7
  extend T::Sig
7
8
 
@@ -9,35 +10,22 @@ module Packwerk
9
10
  params(
10
11
  reference_extractor: ReferenceExtractor,
11
12
  filename: String,
12
- checkers: T::Array[Checker]
13
13
  ).void
14
14
  end
15
- def initialize(reference_extractor:, filename:, checkers:)
15
+ def initialize(reference_extractor:, filename:)
16
16
  @reference_extractor = reference_extractor
17
17
  @filename = filename
18
- @checkers = checkers
19
18
  end
20
19
 
21
- sig { params(node: Parser::AST::Node, ancestors: T::Array[Parser::AST::Node]).returns(T::Array[Offense]) }
22
- def call(node, ancestors)
23
- return [] unless Node.method_call?(node) || Node.constant?(node)
24
- reference = @reference_extractor.reference_from_node(node, ancestors: ancestors, file_path: @filename)
25
- check_reference(reference, node)
20
+ sig do
21
+ params(
22
+ node: Parser::AST::Node,
23
+ ancestors: T::Array[Parser::AST::Node]
24
+ ).returns(T.nilable(Packwerk::Reference))
26
25
  end
27
-
28
- private
29
-
30
- def check_reference(reference, node)
31
- return [] unless reference
32
- @checkers.each_with_object([]) do |checker, violations|
33
- next unless checker.invalid_reference?(reference)
34
- offense = Packwerk::ReferenceOffense.new(
35
- location: Node.location(node),
36
- reference: reference,
37
- violation_type: checker.violation_type
38
- )
39
- violations << offense
40
- end
26
+ def call(node, ancestors)
27
+ return unless Node.method_call?(node) || Node.constant?(node)
28
+ @reference_extractor.reference_from_node(node, ancestors: ancestors, file_path: @filename)
41
29
  end
42
30
  end
43
31
  end
@@ -8,14 +8,12 @@ module Packwerk
8
8
  const :root_path, String
9
9
  const :context_provider, Packwerk::ConstantDiscovery
10
10
  const :constant_name_inspectors, T::Array[ConstantNameInspector]
11
- const :checkers, T::Array[Checker]
12
11
 
13
12
  sig { params(filename: String, node: AST::Node).returns(NodeProcessor) }
14
13
  def for(filename:, node:)
15
14
  ::Packwerk::NodeProcessor.new(
16
15
  reference_extractor: reference_extractor(node: node),
17
16
  filename: filename,
18
- checkers: checkers,
19
17
  )
20
18
  end
21
19
 
@@ -1,14 +1,16 @@
1
- # typed: false
1
+ # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
+ # Visits all nodes of an AST, processing them using a given node processor.
5
6
  class NodeVisitor
6
7
  def initialize(node_processor:)
7
8
  @node_processor = node_processor
8
9
  end
9
10
 
10
11
  def visit(node, ancestors:, result:)
11
- result.concat(@node_processor.call(node, ancestors))
12
+ reference = @node_processor.call(node, ancestors)
13
+ result << reference if reference
12
14
 
13
15
  child_ancestors = [node] + ancestors
14
16
  Node.each_child(node) do |child|
@@ -1,38 +1,52 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
+ # The basic unit of modularity for packwerk; a folder that has been declared to define a package.
6
+ # The package contains all constants defined in files in this folder and all subfolders that are not packages
7
+ # themselves.
5
8
  class Package
9
+ extend T::Sig
6
10
  include Comparable
7
11
 
8
12
  ROOT_PACKAGE_NAME = "."
9
13
 
10
- attr_reader :name, :dependencies
14
+ sig { returns(String) }
15
+ attr_reader :name
16
+ sig { returns(T::Array[String]) }
17
+ attr_reader :dependencies
11
18
 
19
+ sig { params(name: String, config: T.nilable(T.any(T::Hash[T.untyped, T.untyped], FalseClass))).void }
12
20
  def initialize(name:, config:)
13
21
  @name = name
14
- @config = config || {}
15
- @dependencies = Array(@config["dependencies"]).freeze
22
+ @config = T.let(config || {}, T::Hash[T.untyped, T.untyped])
23
+ @dependencies = T.let(Array(@config["dependencies"]).freeze, T::Array[String])
16
24
  end
17
25
 
26
+ sig { returns(T.nilable(T.any(T::Boolean, T::Array[String]))) }
18
27
  def enforce_privacy
19
28
  @config["enforce_privacy"]
20
29
  end
21
30
 
31
+ sig { returns(T::Boolean) }
22
32
  def enforce_dependencies?
23
33
  @config["enforce_dependencies"] == true
24
34
  end
25
35
 
36
+ sig { params(package: Package).returns(T::Boolean) }
26
37
  def dependency?(package)
27
38
  @dependencies.include?(package.name)
28
39
  end
29
40
 
41
+ sig { params(path: String).returns(T::Boolean) }
30
42
  def package_path?(path)
31
43
  return true if root?
32
44
  path.start_with?(@name)
33
45
  end
34
46
 
47
+ sig { returns(String) }
35
48
  def public_path
49
+ @public_path = T.let(@public_path, T.nilable(String))
36
50
  @public_path ||= begin
37
51
  unprefixed_public_path = user_defined_public_path || "app/public/"
38
52
 
@@ -44,10 +58,12 @@ module Packwerk
44
58
  end
45
59
  end
46
60
 
61
+ sig { params(path: String).returns(T::Boolean) }
47
62
  def public_path?(path)
48
63
  path.start_with?(public_path)
49
64
  end
50
65
 
66
+ sig { returns(T.nilable(String)) }
51
67
  def user_defined_public_path
52
68
  return unless @config["public_path"]
53
69
  return @config["public_path"] if @config["public_path"].end_with?("/")
@@ -55,23 +71,28 @@ module Packwerk
55
71
  @config["public_path"] + "/"
56
72
  end
57
73
 
74
+ sig { params(other: T.untyped).returns(T.nilable(Integer)) }
58
75
  def <=>(other)
59
76
  return nil unless other.is_a?(self.class)
60
77
  name <=> other.name
61
78
  end
62
79
 
80
+ sig { params(other: T.untyped).returns(T::Boolean) }
63
81
  def eql?(other)
64
82
  self == other
65
83
  end
66
84
 
85
+ sig { returns(Integer) }
67
86
  def hash
68
87
  name.hash
69
88
  end
70
89
 
90
+ sig { returns(String) }
71
91
  def to_s
72
92
  name
73
93
  end
74
94
 
95
+ sig { returns(T::Boolean) }
75
96
  def root?
76
97
  @name == ROOT_PACKAGE_NAME
77
98
  end
@@ -1,15 +1,25 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "pathname"
5
5
 
6
6
  module Packwerk
7
+ PathSpec = T.type_alias { T.any(String, T::Array[String]) }
8
+
9
+ # A set of {Packwerk::Package}s as well as methods to parse packages from the filesystem.
7
10
  class PackageSet
11
+ extend T::Sig
12
+ extend T::Generic
8
13
  include Enumerable
9
14
 
15
+ Elem = type_member(fixed: Package)
16
+
10
17
  PACKAGE_CONFIG_FILENAME = "package.yml"
11
18
 
12
19
  class << self
20
+ extend T::Sig
21
+
22
+ sig { params(root_path: String, package_pathspec: T.nilable(PathSpec)).returns(PackageSet) }
13
23
  def load_all_from(root_path, package_pathspec: nil)
14
24
  package_paths = package_paths(root_path, package_pathspec || "**")
15
25
 
@@ -23,8 +33,17 @@ module Packwerk
23
33
  new(packages)
24
34
  end
25
35
 
26
- def package_paths(root_path, package_pathspec)
27
- bundle_path_match = Bundler.bundle_path.join("**").to_s
36
+ sig do
37
+ params(
38
+ root_path: String,
39
+ package_pathspec: PathSpec,
40
+ exclude_pathspec: T.nilable(PathSpec)
41
+ ).returns(T::Array[Pathname])
42
+ end
43
+ def package_paths(root_path, package_pathspec, exclude_pathspec = [])
44
+ exclude_pathspec = Array(exclude_pathspec).dup
45
+ .push(Bundler.bundle_path.join("**").to_s)
46
+ .map { |glob| File.expand_path(glob) }
28
47
 
29
48
  glob_patterns = Array(package_pathspec).map do |pathspec|
30
49
  File.join(root_path, pathspec, PACKAGE_CONFIG_FILENAME)
@@ -32,34 +51,50 @@ module Packwerk
32
51
 
33
52
  Dir.glob(glob_patterns)
34
53
  .map { |path| Pathname.new(path).cleanpath }
35
- .reject { |path| path.realpath.fnmatch(bundle_path_match) }
54
+ .reject { |path| exclude_path?(exclude_pathspec, path) }
36
55
  end
37
56
 
38
57
  private
39
58
 
59
+ sig { params(packages: T::Array[Package]).void }
40
60
  def create_root_package_if_none_in(packages)
41
61
  return if packages.any?(&:root?)
42
62
  packages << Package.new(name: Package::ROOT_PACKAGE_NAME, config: nil)
43
63
  end
64
+
65
+ sig { params(globs: T::Array[String], path: Pathname).returns(T::Boolean) }
66
+ def exclude_path?(globs, path)
67
+ globs.any? do |glob|
68
+ path.realpath.fnmatch(glob, File::FNM_EXTGLOB)
69
+ end
70
+ end
44
71
  end
45
72
 
73
+ sig { returns(T::Hash[String, Package]) }
74
+ attr_reader :packages
75
+
76
+ sig { params(packages: T::Array[Package]).void }
46
77
  def initialize(packages)
47
78
  # We want to match more specific paths first
48
79
  sorted_packages = packages.sort_by { |package| -package.name.length }
49
- @packages = sorted_packages.each_with_object({}) { |package, hash| hash[package.name] = package }
80
+ packages = sorted_packages.each_with_object({}) { |package, hash| hash[package.name] = package }
81
+ @packages = T.let(packages, T::Hash[String, Package])
50
82
  end
51
83
 
84
+ sig { override.params(blk: T.proc.params(arg0: Package).returns(T.untyped)).returns(T.untyped) }
52
85
  def each(&blk)
53
- @packages.values.each(&blk)
86
+ packages.values.each(&blk)
54
87
  end
55
88
 
89
+ sig { params(name: String).returns(T.nilable(Package)) }
56
90
  def fetch(name)
57
- @packages[name]
91
+ packages[name]
58
92
  end
59
93
 
94
+ sig { params(file_path: T.any(Pathname, String)).returns(T.nilable(Package)) }
60
95
  def package_from_path(file_path)
61
96
  path_string = file_path.to_s
62
- @packages.values.find { |package| package.package_path?(path_string) }
97
+ packages.values.find { |package| package.package_path?(path_string) }
63
98
  end
64
99
  end
65
100
  end
@@ -4,6 +4,7 @@
4
4
  require "ast/node"
5
5
 
6
6
  module Packwerk
7
+ # A collection of constant definitions parsed from an Abstract Syntax Tree (AST).
7
8
  class ParsedConstantDefinitions
8
9
  def initialize(root_node:)
9
10
  @local_definitions = {}
@@ -2,5 +2,6 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
- Reference = Struct.new(:source_package, :relative_path, :constant)
5
+ # A reference from a file in one package to a constant that may be defined in a different package.
6
+ Reference = Struct.new(:source_package, :relative_path, :constant, :source_location)
6
7
  end
@@ -0,0 +1,21 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module ReferenceChecking
6
+ module Checkers
7
+ module Checker
8
+ extend T::Sig
9
+ extend T::Helpers
10
+
11
+ interface!
12
+
13
+ sig { returns(ViolationType).abstract }
14
+ def violation_type; end
15
+
16
+ sig { params(reference: Reference).returns(T::Boolean).abstract }
17
+ def invalid_reference?(reference); end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module ReferenceChecking
6
+ module Checkers
7
+ # Checks whether a given reference conforms to the configured graph of dependencies.
8
+ class DependencyChecker
9
+ extend T::Sig
10
+ include Checker
11
+
12
+ sig { override.returns(ViolationType) }
13
+ def violation_type
14
+ ViolationType::Dependency
15
+ end
16
+
17
+ sig do
18
+ override
19
+ .params(reference: Packwerk::Reference)
20
+ .returns(T::Boolean)
21
+ end
22
+ def invalid_reference?(reference)
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
+ true
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end