packwerk 1.3.1 → 2.1.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -0
  3. data/Gemfile.lock +12 -9
  4. data/README.md +9 -3
  5. data/TROUBLESHOOT.md +3 -3
  6. data/UPGRADING.md +54 -0
  7. data/USAGE.md +32 -51
  8. data/exe/packwerk +7 -1
  9. data/lib/packwerk/application_load_paths.rb +1 -0
  10. data/lib/packwerk/application_validator.rb +17 -59
  11. data/lib/packwerk/association_inspector.rb +1 -1
  12. data/lib/packwerk/cache.rb +168 -0
  13. data/lib/packwerk/cli.rb +37 -20
  14. data/lib/packwerk/configuration.rb +40 -5
  15. data/lib/packwerk/const_node_inspector.rb +3 -2
  16. data/lib/packwerk/constant_discovery.rb +4 -4
  17. data/lib/packwerk/constant_name_inspector.rb +1 -1
  18. data/lib/packwerk/deprecated_references.rb +18 -6
  19. data/lib/packwerk/file_processor.rb +53 -14
  20. data/lib/packwerk/files_for_processing.rb +15 -4
  21. data/lib/packwerk/formatters/offenses_formatter.rb +1 -1
  22. data/lib/packwerk/formatters/progress_formatter.rb +1 -1
  23. data/lib/packwerk/generators/configuration_file.rb +4 -19
  24. data/lib/packwerk/generators/templates/package.yml +1 -1
  25. data/lib/packwerk/generators/templates/packwerk.yml.erb +5 -5
  26. data/lib/packwerk/graph.rb +2 -0
  27. data/lib/packwerk/node.rb +2 -0
  28. data/lib/packwerk/node_processor.rb +10 -22
  29. data/lib/packwerk/node_processor_factory.rb +0 -3
  30. data/lib/packwerk/node_visitor.rb +7 -2
  31. data/lib/packwerk/package.rb +25 -4
  32. data/lib/packwerk/package_set.rb +44 -8
  33. data/lib/packwerk/parsed_constant_definitions.rb +5 -4
  34. data/lib/packwerk/reference.rb +2 -1
  35. data/lib/packwerk/reference_checking/checkers/checker.rb +21 -0
  36. data/lib/packwerk/reference_checking/checkers/dependency_checker.rb +31 -0
  37. data/lib/packwerk/reference_checking/checkers/privacy_checker.rb +58 -0
  38. data/lib/packwerk/reference_checking/reference_checker.rb +33 -0
  39. data/lib/packwerk/reference_extractor.rb +66 -19
  40. data/lib/packwerk/reference_offense.rb +1 -0
  41. data/lib/packwerk/run_context.rb +32 -11
  42. data/lib/packwerk/sanity_checker.rb +1 -1
  43. data/lib/packwerk/spring_command.rb +28 -0
  44. data/lib/packwerk/unresolved_reference.rb +10 -0
  45. data/lib/packwerk/version.rb +1 -1
  46. data/lib/packwerk/violation_type.rb +1 -1
  47. data/lib/packwerk.rb +19 -12
  48. data/packwerk.gemspec +4 -1
  49. data/service.yml +0 -2
  50. data/sorbet/rbi/gems/psych@3.3.2.rbi +24 -0
  51. metadata +41 -11
  52. data/lib/packwerk/checker.rb +0 -17
  53. data/lib/packwerk/dependency_checker.rb +0 -26
  54. data/lib/packwerk/generators/inflections_file.rb +0 -43
  55. data/lib/packwerk/generators/templates/inflections.yml +0 -6
  56. data/lib/packwerk/inflections/custom.rb +0 -33
  57. data/lib/packwerk/inflections/default.rb +0 -73
  58. data/lib/packwerk/inflector.rb +0 -48
  59. data/lib/packwerk/privacy_checker.rb +0 -53
@@ -12,12 +12,12 @@
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
18
 
22
- # Location of inflections file
23
- # inflections_file: "config/inflections.yml"
19
+ # Whether or not you want the cache enabled (disabled by default)
20
+ # cache: true
21
+
22
+ # Where you want the cache to be stored (default below)
23
+ # cache_directory: 'tmp/cache/packwerk'
@@ -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 Parser::AST::Node nodes.
8
9
  module Node
9
10
  class TypeError < ArgumentError; end
10
11
  Location = Struct.new(:line, :column)
@@ -263,6 +264,7 @@ module Packwerk
263
264
  # "Class.new"
264
265
  # "Module.new"
265
266
  method_call?(node) &&
267
+ receiver(node) &&
266
268
  constant?(receiver(node)) &&
267
269
  ["Class", "Module"].include?(constant_name(receiver(node))) &&
268
270
  method_name(node) == :new
@@ -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(UnresolvedReference))
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
 
@@ -24,7 +22,6 @@ module Packwerk
24
22
  sig { params(node: AST::Node).returns(::Packwerk::ReferenceExtractor) }
25
23
  def reference_extractor(node:)
26
24
  ::Packwerk::ReferenceExtractor.new(
27
- context_provider: context_provider,
28
25
  constant_name_inspectors: constant_name_inspectors,
29
26
  root_node: node,
30
27
  root_path: root_path,
@@ -1,14 +1,19 @@
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
7
+ extend T::Sig
8
+
9
+ sig { params(node_processor: NodeProcessor).void }
6
10
  def initialize(node_processor:)
7
11
  @node_processor = node_processor
8
12
  end
9
13
 
10
14
  def visit(node, ancestors:, result:)
11
- result.concat(@node_processor.call(node, ancestors))
15
+ reference = @node_processor.call(node, ancestors)
16
+ result << reference if reference
12
17
 
13
18
  child_ancestors = [node] + ancestors
14
19
  Node.each_child(node) do |child|
@@ -1,37 +1,51 @@
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])
24
+ @public_path = T.let(nil, T.nilable(String))
16
25
  end
17
26
 
27
+ sig { returns(T.nilable(T.any(T::Boolean, T::Array[String]))) }
18
28
  def enforce_privacy
19
29
  @config["enforce_privacy"]
20
30
  end
21
31
 
32
+ sig { returns(T::Boolean) }
22
33
  def enforce_dependencies?
23
34
  @config["enforce_dependencies"] == true
24
35
  end
25
36
 
37
+ sig { params(package: Package).returns(T::Boolean) }
26
38
  def dependency?(package)
27
39
  @dependencies.include?(package.name)
28
40
  end
29
41
 
42
+ sig { params(path: String).returns(T::Boolean) }
30
43
  def package_path?(path)
31
44
  return true if root?
32
45
  path.start_with?(@name)
33
46
  end
34
47
 
48
+ sig { returns(String) }
35
49
  def public_path
36
50
  @public_path ||= begin
37
51
  unprefixed_public_path = user_defined_public_path || "app/public/"
@@ -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,51 @@ 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])
82
+ @package_from_path = T.let({}, T::Hash[String, T.nilable(Package)])
50
83
  end
51
84
 
85
+ sig { override.params(blk: T.proc.params(arg0: Package).returns(T.untyped)).returns(T.untyped) }
52
86
  def each(&blk)
53
- @packages.values.each(&blk)
87
+ packages.values.each(&blk)
54
88
  end
55
89
 
90
+ sig { params(name: String).returns(T.nilable(Package)) }
56
91
  def fetch(name)
57
- @packages[name]
92
+ packages[name]
58
93
  end
59
94
 
95
+ sig { params(file_path: T.any(Pathname, String)).returns(T.nilable(Package)) }
60
96
  def package_from_path(file_path)
61
97
  path_string = file_path.to_s
62
- @packages.values.find { |package| package.package_path?(path_string) }
98
+ @package_from_path[path_string] ||= packages.values.find { |package| package.package_path?(path_string) }
63
99
  end
64
100
  end
65
101
  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 = {}
@@ -24,13 +25,13 @@ module Packwerk
24
25
  def self.reference_qualifications(constant_name, namespace_path:)
25
26
  return [constant_name] if constant_name.start_with?("::")
26
27
 
27
- fully_qualified_constant_name = "::#{constant_name}"
28
+ resolved_constant_name = "::#{constant_name}"
28
29
 
29
30
  possible_namespaces = namespace_path.each_with_object([""]) do |current, acc|
30
31
  acc << "#{acc.last}::#{current}" if acc.last && current
31
32
  end
32
33
 
33
- possible_namespaces.map { |namespace| namespace + fully_qualified_constant_name }
34
+ possible_namespaces.map { |namespace| namespace + resolved_constant_name }
34
35
  end
35
36
 
36
37
  private
@@ -52,9 +53,9 @@ module Packwerk
52
53
  end
53
54
 
54
55
  def add_definition(constant_name, current_namespace_path, location)
55
- fully_qualified_constant = [""].concat(current_namespace_path).push(constant_name).join("::")
56
+ resolved_constant = [""].concat(current_namespace_path).push(constant_name).join("::")
56
57
 
57
- @local_definitions[fully_qualified_constant] = location
58
+ @local_definitions[resolved_constant] = location
58
59
  end
59
60
  end
60
61
  end
@@ -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
@@ -0,0 +1,58 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module ReferenceChecking
6
+ module Checkers
7
+ # Checks whether a given reference references a private constant of another package.
8
+ class PrivacyChecker
9
+ extend T::Sig
10
+ include Checker
11
+
12
+ sig { override.returns(Packwerk::ViolationType) }
13
+ def violation_type
14
+ ViolationType::Privacy
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 if reference.constant.public?
24
+
25
+ privacy_option = reference.constant.package.enforce_privacy
26
+ return false if enforcement_disabled?(privacy_option)
27
+
28
+ return false unless privacy_option == true ||
29
+ explicitly_private_constant?(reference.constant, explicitly_private_constants: privacy_option)
30
+
31
+ true
32
+ end
33
+
34
+ private
35
+
36
+ sig do
37
+ params(
38
+ constant: ConstantDiscovery::ConstantContext,
39
+ explicitly_private_constants: T::Array[String]
40
+ ).returns(T::Boolean)
41
+ end
42
+ def explicitly_private_constant?(constant, explicitly_private_constants:)
43
+ explicitly_private_constants.include?(constant.name) ||
44
+ # nested constants
45
+ explicitly_private_constants.any? { |epc| constant.name.start_with?(epc + "::") }
46
+ end
47
+
48
+ sig do
49
+ params(privacy_option: T.nilable(T.any(T::Boolean, T::Array[String])))
50
+ .returns(T::Boolean)
51
+ end
52
+ def enforcement_disabled?(privacy_option)
53
+ [false, nil].include?(privacy_option)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,33 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module ReferenceChecking
6
+ class ReferenceChecker
7
+ extend T::Sig
8
+
9
+ def initialize(checkers)
10
+ @checkers = checkers
11
+ end
12
+
13
+ sig do
14
+ params(
15
+ reference: T.any(Packwerk::Reference, Packwerk::Offense)
16
+ ).returns(T::Array[Packwerk::Offense])
17
+ end
18
+ def call(reference)
19
+ return [reference] if reference.is_a?(Packwerk::Offense)
20
+
21
+ @checkers.each_with_object([]) do |checker, violations|
22
+ next unless checker.invalid_reference?(reference)
23
+ offense = Packwerk::ReferenceOffense.new(
24
+ location: reference.source_location,
25
+ reference: reference,
26
+ violation_type: checker.violation_type
27
+ )
28
+ violations << offense
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -2,64 +2,111 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
- # extracts a possible constant reference from a given AST node
5
+ # Extracts a possible constant reference from a given AST node.
6
6
  class ReferenceExtractor
7
7
  extend T::Sig
8
8
 
9
9
  sig do
10
10
  params(
11
- context_provider: Packwerk::ConstantDiscovery,
12
11
  constant_name_inspectors: T::Array[Packwerk::ConstantNameInspector],
13
12
  root_node: ::AST::Node,
14
13
  root_path: String,
15
14
  ).void
16
15
  end
17
16
  def initialize(
18
- context_provider:,
19
17
  constant_name_inspectors:,
20
18
  root_node:,
21
19
  root_path:
22
20
  )
23
- @context_provider = context_provider
24
21
  @constant_name_inspectors = constant_name_inspectors
25
22
  @root_path = root_path
26
23
  @local_constant_definitions = ParsedConstantDefinitions.new(root_node: root_node)
27
24
  end
28
25
 
26
+ sig do
27
+ params(
28
+ node: Parser::AST::Node,
29
+ ancestors: T::Array[Parser::AST::Node],
30
+ file_path: String
31
+ ).returns(T.nilable(UnresolvedReference))
32
+ end
29
33
  def reference_from_node(node, ancestors:, file_path:)
30
34
  constant_name = T.let(nil, T.nilable(String))
31
35
 
32
36
  @constant_name_inspectors.each do |inspector|
33
37
  constant_name = inspector.constant_name_from_node(node, ancestors: ancestors)
38
+
34
39
  break if constant_name
35
40
  end
36
41
 
37
42
  reference_from_constant(constant_name, node: node, ancestors: ancestors, file_path: file_path) if constant_name
38
43
  end
39
44
 
40
- private
45
+ sig do
46
+ params(
47
+ unresolved_references_and_offenses: T::Array[T.any(UnresolvedReference, Offense)],
48
+ context_provider: ConstantDiscovery
49
+ ).returns(T::Array[T.any(Reference, Offense)])
50
+ end
51
+ def self.get_fully_qualified_references_and_offenses_from(unresolved_references_and_offenses, context_provider)
52
+ fully_qualified_references_and_offenses = T.let([], T::Array[T.any(Reference, Offense)])
41
53
 
42
- def reference_from_constant(constant_name, node:, ancestors:, file_path:)
43
- namespace_path = Node.enclosing_namespace_path(node, ancestors: ancestors)
44
- return if local_reference?(constant_name, Node.name_location(node), namespace_path)
54
+ unresolved_references_and_offenses.each do |unresolved_references_or_offense|
55
+ if unresolved_references_or_offense.is_a?(Offense)
56
+ fully_qualified_references_and_offenses << unresolved_references_or_offense
57
+
58
+ next
59
+ end
60
+
61
+ unresolved_reference = unresolved_references_or_offense
62
+
63
+ constant =
64
+ context_provider.context_for(
65
+ unresolved_reference.constant_name,
66
+ current_namespace_path: unresolved_reference.namespace_path
67
+ )
68
+
69
+ next if constant&.package.nil?
45
70
 
46
- constant =
47
- @context_provider.context_for(
48
- constant_name,
49
- current_namespace_path: namespace_path
71
+ source_package = context_provider.package_from_path(unresolved_reference.relative_path)
72
+
73
+ next if source_package == constant.package
74
+
75
+ fully_qualified_references_and_offenses << Reference.new(
76
+ source_package,
77
+ unresolved_reference.relative_path,
78
+ constant,
79
+ unresolved_reference.source_location
50
80
  )
81
+ end
51
82
 
52
- return if constant&.package.nil?
83
+ fully_qualified_references_and_offenses
84
+ end
53
85
 
54
- relative_path =
55
- Pathname.new(file_path)
56
- .relative_path_from(@root_path).to_s
86
+ private
87
+
88
+ sig do
89
+ params(
90
+ constant_name: String,
91
+ node: Parser::AST::Node,
92
+ ancestors: T::Array[Parser::AST::Node],
93
+ file_path: String
94
+ ).returns(T.nilable(UnresolvedReference))
95
+ end
96
+ def reference_from_constant(constant_name, node:, ancestors:, file_path:)
97
+ namespace_path = Node.enclosing_namespace_path(node, ancestors: ancestors)
57
98
 
58
- source_package = @context_provider.package_from_path(relative_path)
99
+ return if local_reference?(constant_name, Node.name_location(node), namespace_path)
59
100
 
60
- return if source_package == constant.package
101
+ relative_path = Pathname.new(file_path).relative_path_from(@root_path).to_s
102
+ location = Node.location(node)
61
103
 
62
- Reference.new(source_package, relative_path, constant)
104
+ UnresolvedReference.new(
105
+ constant_name,
106
+ namespace_path,
107
+ relative_path,
108
+ location
109
+ )
63
110
  end
64
111
 
65
112
  def local_reference?(constant_name, name_location, namespace_path)
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
+ # An offense related to a {Packwerk::Reference}.
5
6
  class ReferenceOffense < Offense
6
7
  extend T::Sig
7
8
  extend T::Helpers