packwerk 1.4.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -0
  3. data/Gemfile.lock +22 -19
  4. data/README.md +7 -2
  5. data/UPGRADING.md +54 -0
  6. data/USAGE.md +5 -40
  7. data/lib/packwerk/application_validator.rb +88 -93
  8. data/lib/packwerk/association_inspector.rb +1 -1
  9. data/lib/packwerk/cache.rb +169 -0
  10. data/lib/packwerk/cli.rb +32 -28
  11. data/lib/packwerk/configuration.rb +40 -5
  12. data/lib/packwerk/constant_discovery.rb +21 -5
  13. data/lib/packwerk/constant_name_inspector.rb +1 -1
  14. data/lib/packwerk/deprecated_references.rb +1 -1
  15. data/lib/packwerk/file_processor.rb +34 -15
  16. data/lib/packwerk/files_for_processing.rb +49 -22
  17. data/lib/packwerk/formatters/offenses_formatter.rb +1 -1
  18. data/lib/packwerk/formatters/progress_formatter.rb +1 -1
  19. data/lib/packwerk/generators/configuration_file.rb +4 -19
  20. data/lib/packwerk/generators/templates/packwerk.yml.erb +5 -5
  21. data/lib/packwerk/node.rb +2 -1
  22. data/lib/packwerk/node_processor.rb +6 -6
  23. data/lib/packwerk/node_processor_factory.rb +3 -4
  24. data/lib/packwerk/node_visitor.rb +3 -0
  25. data/lib/packwerk/offense.rb +10 -2
  26. data/lib/packwerk/package.rb +1 -1
  27. data/lib/packwerk/package_set.rb +3 -2
  28. data/lib/packwerk/parse_run.rb +37 -17
  29. data/lib/packwerk/parsed_constant_definitions.rb +4 -4
  30. data/lib/packwerk/parsers/erb.rb +2 -0
  31. data/lib/packwerk/parsers/factory.rb +2 -0
  32. data/lib/packwerk/parsers/parser_interface.rb +17 -0
  33. data/lib/packwerk/parsers/ruby.rb +2 -0
  34. data/lib/packwerk/parsers.rb +1 -0
  35. data/lib/packwerk/reference_checking/checkers/checker.rb +1 -1
  36. data/lib/packwerk/reference_checking/reference_checker.rb +2 -1
  37. data/lib/packwerk/reference_extractor.rb +78 -20
  38. data/lib/packwerk/reference_offense.rb +8 -3
  39. data/lib/packwerk/result.rb +2 -2
  40. data/lib/packwerk/run_context.rb +62 -38
  41. data/lib/packwerk/spring_command.rb +1 -1
  42. data/lib/packwerk/unresolved_reference.rb +10 -0
  43. data/lib/packwerk/version.rb +1 -1
  44. data/lib/packwerk.rb +5 -9
  45. data/packwerk.gemspec +1 -0
  46. data/sorbet/config +1 -0
  47. data/sorbet/rbi/gems/tapioca@0.4.19.rbi +1 -1
  48. data/sorbet/tapioca/require.rb +1 -1
  49. metadata +21 -7
  50. data/lib/packwerk/generators/inflections_file.rb +0 -43
  51. data/lib/packwerk/generators/templates/inflections.yml +0 -6
  52. data/lib/packwerk/inflections/custom.rb +0 -33
  53. data/lib/packwerk/inflections/default.rb +0 -73
  54. data/lib/packwerk/inflector.rb +0 -49
@@ -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
@@ -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'
data/lib/packwerk/node.rb CHANGED
@@ -5,7 +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
+ # Convenience methods for working with Parser::AST::Node nodes.
9
9
  module Node
10
10
  class TypeError < ArgumentError; end
11
11
  Location = Struct.new(:line, :column)
@@ -264,6 +264,7 @@ module Packwerk
264
264
  # "Class.new"
265
265
  # "Module.new"
266
266
  method_call?(node) &&
267
+ receiver(node) &&
267
268
  constant?(receiver(node)) &&
268
269
  ["Class", "Module"].include?(constant_name(receiver(node))) &&
269
270
  method_name(node) == :new
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
@@ -9,23 +9,23 @@ module Packwerk
9
9
  sig do
10
10
  params(
11
11
  reference_extractor: ReferenceExtractor,
12
- filename: String,
12
+ absolute_file: String,
13
13
  ).void
14
14
  end
15
- def initialize(reference_extractor:, filename:)
15
+ def initialize(reference_extractor:, absolute_file:)
16
16
  @reference_extractor = reference_extractor
17
- @filename = filename
17
+ @absolute_file = absolute_file
18
18
  end
19
19
 
20
20
  sig do
21
21
  params(
22
22
  node: Parser::AST::Node,
23
23
  ancestors: T::Array[Parser::AST::Node]
24
- ).returns(T.nilable(Packwerk::Reference))
24
+ ).returns(T.nilable(UnresolvedReference))
25
25
  end
26
26
  def call(node, ancestors)
27
27
  return unless Node.method_call?(node) || Node.constant?(node)
28
- @reference_extractor.reference_from_node(node, ancestors: ancestors, file_path: @filename)
28
+ @reference_extractor.reference_from_node(node, ancestors: ancestors, absolute_file: @absolute_file)
29
29
  end
30
30
  end
31
31
  end
@@ -9,11 +9,11 @@ module Packwerk
9
9
  const :context_provider, Packwerk::ConstantDiscovery
10
10
  const :constant_name_inspectors, T::Array[ConstantNameInspector]
11
11
 
12
- sig { params(filename: String, node: AST::Node).returns(NodeProcessor) }
13
- def for(filename:, node:)
12
+ sig { params(absolute_file: String, node: AST::Node).returns(NodeProcessor) }
13
+ def for(absolute_file:, node:)
14
14
  ::Packwerk::NodeProcessor.new(
15
15
  reference_extractor: reference_extractor(node: node),
16
- filename: filename,
16
+ absolute_file: absolute_file,
17
17
  )
18
18
  end
19
19
 
@@ -22,7 +22,6 @@ module Packwerk
22
22
  sig { params(node: AST::Node).returns(::Packwerk::ReferenceExtractor) }
23
23
  def reference_extractor(node:)
24
24
  ::Packwerk::ReferenceExtractor.new(
25
- context_provider: context_provider,
26
25
  constant_name_inspectors: constant_name_inspectors,
27
26
  root_node: node,
28
27
  root_path: root_path,
@@ -4,6 +4,9 @@
4
4
  module Packwerk
5
5
  # Visits all nodes of an AST, processing them using a given node processor.
6
6
  class NodeVisitor
7
+ extend T::Sig
8
+
9
+ sig { params(node_processor: NodeProcessor).void }
7
10
  def initialize(node_processor:)
8
11
  @node_processor = node_processor
9
12
  end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "parser/source/map"
@@ -8,7 +8,14 @@ module Packwerk
8
8
  extend T::Sig
9
9
  extend T::Helpers
10
10
 
11
- attr_reader :location, :file, :message
11
+ sig { returns(T.nilable(Node::Location)) }
12
+ attr_reader :location
13
+
14
+ sig { returns(String) }
15
+ attr_reader :file
16
+
17
+ sig { returns(String) }
18
+ attr_reader :message
12
19
 
13
20
  sig do
14
21
  params(file: String, message: String, location: T.nilable(Node::Location))
@@ -22,6 +29,7 @@ module Packwerk
22
29
 
23
30
  sig { params(style: OutputStyle).returns(String) }
24
31
  def to_s(style = OutputStyles::Plain.new)
32
+ location = self.location
25
33
  if location
26
34
  <<~EOS
27
35
  #{style.filename}#{file}#{style.reset}:#{location.line}:#{location.column}
@@ -21,6 +21,7 @@ module Packwerk
21
21
  @name = name
22
22
  @config = T.let(config || {}, T::Hash[T.untyped, T.untyped])
23
23
  @dependencies = T.let(Array(@config["dependencies"]).freeze, T::Array[String])
24
+ @public_path = T.let(nil, T.nilable(String))
24
25
  end
25
26
 
26
27
  sig { returns(T.nilable(T.any(T::Boolean, T::Array[String]))) }
@@ -46,7 +47,6 @@ module Packwerk
46
47
 
47
48
  sig { returns(String) }
48
49
  def public_path
49
- @public_path = T.let(@public_path, T.nilable(String))
50
50
  @public_path ||= begin
51
51
  unprefixed_public_path = user_defined_public_path || "app/public/"
52
52
 
@@ -79,6 +79,7 @@ module Packwerk
79
79
  sorted_packages = packages.sort_by { |package| -package.name.length }
80
80
  packages = sorted_packages.each_with_object({}) { |package, hash| hash[package.name] = package }
81
81
  @packages = T.let(packages, T::Hash[String, Package])
82
+ @package_from_path = T.let({}, T::Hash[String, T.nilable(Package)])
82
83
  end
83
84
 
84
85
  sig { override.params(blk: T.proc.params(arg0: Package).returns(T.untyped)).returns(T.untyped) }
@@ -91,10 +92,10 @@ module Packwerk
91
92
  packages[name]
92
93
  end
93
94
 
94
- sig { params(file_path: T.any(Pathname, String)).returns(T.nilable(Package)) }
95
+ sig { params(file_path: T.any(Pathname, String)).returns(Package) }
95
96
  def package_from_path(file_path)
96
97
  path_string = file_path.to_s
97
- packages.values.find { |package| package.package_path?(path_string) }
98
+ @package_from_path[path_string] ||= T.must(packages.values.find { |package| package.package_path?(path_string) })
98
99
  end
99
100
  end
100
101
  end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "benchmark"
@@ -8,8 +8,20 @@ module Packwerk
8
8
  class ParseRun
9
9
  extend T::Sig
10
10
 
11
+ ProcessFileProc = T.type_alias do
12
+ T.proc.params(path: String).returns(T::Array[Offense])
13
+ end
14
+
15
+ sig do
16
+ params(
17
+ absolute_files: T::Array[String],
18
+ configuration: Configuration,
19
+ progress_formatter: Formatters::ProgressFormatter,
20
+ offenses_formatter: OffensesFormatter,
21
+ ).void
22
+ end
11
23
  def initialize(
12
- files:,
24
+ absolute_files:,
13
25
  configuration:,
14
26
  progress_formatter: Formatters::ProgressFormatter.new(StringIO.new),
15
27
  offenses_formatter: Formatters::OffensesFormatter.new
@@ -17,9 +29,10 @@ module Packwerk
17
29
  @configuration = configuration
18
30
  @progress_formatter = progress_formatter
19
31
  @offenses_formatter = offenses_formatter
20
- @files = files
32
+ @absolute_files = absolute_files
21
33
  end
22
34
 
35
+ sig { returns(Result) }
23
36
  def detect_stale_violations
24
37
  offense_collection = find_offenses
25
38
 
@@ -29,6 +42,7 @@ module Packwerk
29
42
  Result.new(message: message, status: result_status)
30
43
  end
31
44
 
45
+ sig { returns(Result) }
32
46
  def update_deprecations
33
47
  offense_collection = find_offenses
34
48
  offense_collection.dump_deprecated_references_files
@@ -41,6 +55,7 @@ module Packwerk
41
55
  Result.new(message: message, status: offense_collection.errors.empty?)
42
56
  end
43
57
 
58
+ sig { returns(Result) }
44
59
  def check
45
60
  offense_collection = find_offenses(show_errors: true)
46
61
 
@@ -55,23 +70,24 @@ module Packwerk
55
70
 
56
71
  private
57
72
 
73
+ sig { params(show_errors: T::Boolean).returns(OffenseCollection) }
58
74
  def find_offenses(show_errors: false)
59
75
  offense_collection = OffenseCollection.new(@configuration.root_path)
60
- @progress_formatter.started(@files)
76
+ @progress_formatter.started(@absolute_files)
61
77
 
62
78
  run_context = Packwerk::RunContext.from_configuration(@configuration)
63
- all_offenses = T.let([], T.untyped)
79
+ all_offenses = T.let([], T::Array[Offense])
64
80
 
65
- process_file = -> (path) do
66
- run_context.process_file(file: path).tap do |offenses|
81
+ process_file = T.let(-> (absolute_file) do
82
+ run_context.process_file(absolute_file: absolute_file).tap do |offenses|
67
83
  failed = show_errors && offenses.any? { |offense| !offense_collection.listed?(offense) }
68
84
  update_progress(failed: failed)
69
85
  end
70
- end
86
+ end, ProcessFileProc)
71
87
 
72
88
  execution_time = Benchmark.realtime do
73
89
  all_offenses = if @configuration.parallel?
74
- Parallel.flat_map(@files, &process_file)
90
+ Parallel.flat_map(@absolute_files, &process_file)
75
91
  else
76
92
  serial_find_offenses(&process_file)
77
93
  end
@@ -83,18 +99,22 @@ module Packwerk
83
99
  offense_collection
84
100
  end
85
101
 
86
- def serial_find_offenses
87
- all_offenses = T.let([], T.untyped)
88
- @files.each do |path|
89
- offenses = yield path
90
- all_offenses.concat(offenses)
102
+ sig { params(block: ProcessFileProc).returns(T::Array[Offense]) }
103
+ def serial_find_offenses(&block)
104
+ all_offenses = T.let([], T::Array[Offense])
105
+ begin
106
+ @absolute_files.each do |absolute_file|
107
+ offenses = block.call(absolute_file)
108
+ all_offenses.concat(offenses)
109
+ end
110
+ rescue Interrupt
111
+ @progress_formatter.interrupted
112
+ all_offenses
91
113
  end
92
114
  all_offenses
93
- rescue Interrupt
94
- @progress_formatter.interrupted
95
- all_offenses
96
115
  end
97
116
 
117
+ sig { params(failed: T::Boolean).void }
98
118
  def update_progress(failed: false)
99
119
  if failed
100
120
  @progress_formatter.mark_as_failed
@@ -25,13 +25,13 @@ module Packwerk
25
25
  def self.reference_qualifications(constant_name, namespace_path:)
26
26
  return [constant_name] if constant_name.start_with?("::")
27
27
 
28
- fully_qualified_constant_name = "::#{constant_name}"
28
+ resolved_constant_name = "::#{constant_name}"
29
29
 
30
30
  possible_namespaces = namespace_path.each_with_object([""]) do |current, acc|
31
31
  acc << "#{acc.last}::#{current}" if acc.last && current
32
32
  end
33
33
 
34
- possible_namespaces.map { |namespace| namespace + fully_qualified_constant_name }
34
+ possible_namespaces.map { |namespace| namespace + resolved_constant_name }
35
35
  end
36
36
 
37
37
  private
@@ -53,9 +53,9 @@ module Packwerk
53
53
  end
54
54
 
55
55
  def add_definition(constant_name, current_namespace_path, location)
56
- fully_qualified_constant = [""].concat(current_namespace_path).push(constant_name).join("::")
56
+ resolved_constant = [""].concat(current_namespace_path).push(constant_name).join("::")
57
57
 
58
- @local_definitions[fully_qualified_constant] = location
58
+ @local_definitions[resolved_constant] = location
59
59
  end
60
60
  end
61
61
  end
@@ -9,6 +9,8 @@ require "parser/source/buffer"
9
9
  module Packwerk
10
10
  module Parsers
11
11
  class Erb
12
+ include ParserInterface
13
+
12
14
  def initialize(parser_class: BetterHtml::Parser, ruby_parser: Ruby.new)
13
15
  @parser_class = parser_class
14
16
  @ruby_parser = ruby_parser
@@ -6,6 +6,7 @@ require "singleton"
6
6
  module Packwerk
7
7
  module Parsers
8
8
  class Factory
9
+ extend T::Sig
9
10
  include Singleton
10
11
 
11
12
  RUBY_REGEX = %r{
@@ -19,6 +20,7 @@ module Packwerk
19
20
  ERB_REGEX = /\.erb\Z/
20
21
  private_constant :ERB_REGEX
21
22
 
23
+ sig { params(path: String).returns(T.nilable(ParserInterface)) }
22
24
  def for_path(path)
23
25
  case path
24
26
  when RUBY_REGEX
@@ -0,0 +1,17 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Parsers
6
+ module ParserInterface
7
+ extend T::Helpers
8
+ extend T::Sig
9
+
10
+ interface!
11
+
12
+ sig { abstract.params(io: File, file_path: String).returns(T.untyped) }
13
+ def call(io:, file_path:)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -7,6 +7,8 @@ require "parser/current"
7
7
  module Packwerk
8
8
  module Parsers
9
9
  class Ruby
10
+ include ParserInterface
11
+
10
12
  class RaiseExceptionsParser < Parser::CurrentRuby
11
13
  def initialize(builder)
12
14
  super(builder)
@@ -5,6 +5,7 @@ module Packwerk
5
5
  module Parsers
6
6
  autoload :Erb, "packwerk/parsers/erb"
7
7
  autoload :Factory, "packwerk/parsers/factory"
8
+ autoload :ParserInterface, "packwerk/parsers/parser_interface"
8
9
  autoload :Ruby, "packwerk/parsers/ruby"
9
10
 
10
11
  class ParseResult < Offense; end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
@@ -6,6 +6,7 @@ module Packwerk
6
6
  class ReferenceChecker
7
7
  extend T::Sig
8
8
 
9
+ sig { params(checkers: T::Array[Checkers::Checker]).void }
9
10
  def initialize(checkers)
10
11
  @checkers = checkers
11
12
  end
@@ -8,58 +8,116 @@ module Packwerk
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
 
29
- def reference_from_node(node, ancestors:, file_path:)
26
+ sig do
27
+ params(
28
+ node: Parser::AST::Node,
29
+ ancestors: T::Array[Parser::AST::Node],
30
+ absolute_file: String
31
+ ).returns(T.nilable(UnresolvedReference))
32
+ end
33
+ def reference_from_node(node, ancestors:, absolute_file:)
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
- reference_from_constant(constant_name, node: node, ancestors: ancestors, file_path: file_path) if constant_name
42
+ if constant_name
43
+ reference_from_constant(
44
+ constant_name,
45
+ node: node,
46
+ ancestors: ancestors,
47
+ absolute_file: absolute_file
48
+ )
49
+ end
38
50
  end
39
51
 
40
- private
52
+ sig do
53
+ params(
54
+ unresolved_references_and_offenses: T::Array[T.any(UnresolvedReference, Offense)],
55
+ context_provider: ConstantDiscovery
56
+ ).returns(T::Array[T.any(Reference, Offense)])
57
+ end
58
+ def self.get_fully_qualified_references_and_offenses_from(unresolved_references_and_offenses, context_provider)
59
+ fully_qualified_references_and_offenses = T.let([], T::Array[T.any(Reference, Offense)])
41
60
 
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)
61
+ unresolved_references_and_offenses.each do |unresolved_references_or_offense|
62
+ if unresolved_references_or_offense.is_a?(Offense)
63
+ fully_qualified_references_and_offenses << unresolved_references_or_offense
45
64
 
46
- constant =
47
- @context_provider.context_for(
48
- constant_name,
49
- current_namespace_path: namespace_path
65
+ next
66
+ end
67
+
68
+ unresolved_reference = unresolved_references_or_offense
69
+
70
+ constant =
71
+ context_provider.context_for(
72
+ unresolved_reference.constant_name,
73
+ current_namespace_path: unresolved_reference.namespace_path
74
+ )
75
+
76
+ next if constant.nil?
77
+
78
+ package_for_constant = constant.package
79
+
80
+ next if package_for_constant.nil?
81
+
82
+ source_package = context_provider.package_from_path(unresolved_reference.relative_path)
83
+
84
+ next if source_package == package_for_constant
85
+
86
+ fully_qualified_references_and_offenses << Reference.new(
87
+ source_package,
88
+ unresolved_reference.relative_path,
89
+ constant,
90
+ unresolved_reference.source_location
50
91
  )
92
+ end
93
+
94
+ fully_qualified_references_and_offenses
95
+ end
51
96
 
52
- return if constant&.package.nil?
97
+ private
53
98
 
54
- relative_path =
55
- Pathname.new(file_path)
56
- .relative_path_from(@root_path).to_s
99
+ sig do
100
+ params(
101
+ constant_name: String,
102
+ node: Parser::AST::Node,
103
+ ancestors: T::Array[Parser::AST::Node],
104
+ absolute_file: String
105
+ ).returns(T.nilable(UnresolvedReference))
106
+ end
107
+ def reference_from_constant(constant_name, node:, ancestors:, absolute_file:)
108
+ namespace_path = Node.enclosing_namespace_path(node, ancestors: ancestors)
57
109
 
58
- source_package = @context_provider.package_from_path(relative_path)
110
+ return if local_reference?(constant_name, Node.name_location(node), namespace_path)
59
111
 
60
- return if source_package == constant.package
112
+ relative_file = Pathname.new(absolute_file).relative_path_from(@root_path).to_s
113
+ location = Node.location(node)
61
114
 
62
- Reference.new(source_package, relative_path, constant, Node.location(node))
115
+ UnresolvedReference.new(
116
+ constant_name,
117
+ namespace_path,
118
+ relative_file,
119
+ location
120
+ )
63
121
  end
64
122
 
65
123
  def local_reference?(constant_name, name_location, namespace_path)
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
@@ -7,7 +7,11 @@ module Packwerk
7
7
  extend T::Sig
8
8
  extend T::Helpers
9
9
 
10
- attr_reader :reference, :violation_type
10
+ sig { returns(Reference) }
11
+ attr_reader :reference
12
+
13
+ sig { returns(ViolationType) }
14
+ attr_reader :violation_type
11
15
 
12
16
  sig do
13
17
  params(
@@ -25,10 +29,11 @@ module Packwerk
25
29
 
26
30
  private
27
31
 
32
+ sig { params(reference: Reference, violation_type: ViolationType).returns(String) }
28
33
  def build_message(reference, violation_type)
29
34
  violation_message = case violation_type
30
35
  when ViolationType::Privacy
31
- source_desc = reference.source_package ? "'#{reference.source_package}'" : "here"
36
+ source_desc = "'#{reference.source_package}'"
32
37
  "Privacy violation: '#{reference.constant.name}' is private to '#{reference.constant.package}' but " \
33
38
  "referenced from #{source_desc}.\n" \
34
39
  "Is there a public entrypoint in '#{reference.constant.package.public_path}' that you can use instead?"
@@ -3,7 +3,7 @@
3
3
 
4
4
  module Packwerk
5
5
  class Result < T::Struct
6
- prop :message, String
7
- prop :status, T::Boolean
6
+ const :message, String
7
+ const :status, T::Boolean
8
8
  end
9
9
  end