packwerk 1.3.0 → 2.0.0

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