packwerk 2.0.0 → 2.2.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +26 -22
  3. data/README.md +13 -1
  4. data/USAGE.md +7 -0
  5. data/lib/packwerk/application_load_paths.rb +12 -18
  6. data/lib/packwerk/application_validator.rb +88 -40
  7. data/lib/packwerk/cache.rb +169 -0
  8. data/lib/packwerk/cli.rb +29 -13
  9. data/lib/packwerk/configuration.rb +17 -12
  10. data/lib/packwerk/constant_discovery.rb +20 -4
  11. data/lib/packwerk/constant_name_inspector.rb +1 -1
  12. data/lib/packwerk/deprecated_references.rb +1 -1
  13. data/lib/packwerk/file_processor.rb +43 -22
  14. data/lib/packwerk/files_for_processing.rb +55 -26
  15. data/lib/packwerk/generators/templates/packwerk.yml.erb +6 -0
  16. data/lib/packwerk/node.rb +2 -1
  17. data/lib/packwerk/node_processor.rb +6 -6
  18. data/lib/packwerk/node_processor_factory.rb +3 -4
  19. data/lib/packwerk/node_visitor.rb +3 -0
  20. data/lib/packwerk/offense.rb +10 -2
  21. data/lib/packwerk/package.rb +1 -1
  22. data/lib/packwerk/package_set.rb +4 -3
  23. data/lib/packwerk/parse_run.rb +37 -17
  24. data/lib/packwerk/parsed_constant_definitions.rb +4 -4
  25. data/lib/packwerk/parsers/erb.rb +2 -0
  26. data/lib/packwerk/parsers/factory.rb +2 -0
  27. data/lib/packwerk/parsers/parser_interface.rb +19 -0
  28. data/lib/packwerk/parsers/ruby.rb +2 -0
  29. data/lib/packwerk/parsers.rb +1 -0
  30. data/lib/packwerk/reference_checking/checkers/checker.rb +1 -1
  31. data/lib/packwerk/reference_checking/reference_checker.rb +3 -4
  32. data/lib/packwerk/reference_extractor.rb +72 -20
  33. data/lib/packwerk/reference_offense.rb +8 -3
  34. data/lib/packwerk/result.rb +2 -2
  35. data/lib/packwerk/run_context.rb +62 -36
  36. data/lib/packwerk/spring_command.rb +1 -1
  37. data/lib/packwerk/unresolved_reference.rb +10 -0
  38. data/lib/packwerk/version.rb +1 -1
  39. data/lib/packwerk.rb +2 -0
  40. data/packwerk.gemspec +4 -2
  41. data/sorbet/config +1 -0
  42. data/sorbet/rbi/gems/tapioca@0.4.19.rbi +1 -1
  43. data/sorbet/tapioca/require.rb +1 -1
  44. metadata +36 -5
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
 
@@ -12,7 +12,7 @@ module Packwerk
12
12
  extend T::Generic
13
13
  include Enumerable
14
14
 
15
- Elem = type_member(fixed: Package)
15
+ Elem = type_member { { fixed: Package } }
16
16
 
17
17
  PACKAGE_CONFIG_FILENAME = "package.yml"
18
18
 
@@ -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_file_set: FilesForProcessing::AbsoluteFileSet,
18
+ configuration: Configuration,
19
+ progress_formatter: Formatters::ProgressFormatter,
20
+ offenses_formatter: OffensesFormatter,
21
+ ).void
22
+ end
11
23
  def initialize(
12
- files:,
24
+ absolute_file_set:,
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_file_set = absolute_file_set
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_file_set)
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_file_set, &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_file_set.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,19 @@
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
+ requires_ancestor { Kernel }
11
+
12
+ interface!
13
+
14
+ sig { abstract.params(io: File, file_path: String).returns(T.untyped) }
15
+ def call(io:, file_path:)
16
+ end
17
+ end
18
+ end
19
+ 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,18 +6,17 @@ 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
12
13
 
13
14
  sig do
14
15
  params(
15
- reference: T.any(Packwerk::Reference, Packwerk::Offense)
16
+ reference: Reference
16
17
  ).returns(T::Array[Packwerk::Offense])
17
18
  end
18
19
  def call(reference)
19
- return [reference] if reference.is_a?(Packwerk::Offense)
20
-
21
20
  @checkers.each_with_object([]) do |checker, violations|
22
21
  next unless checker.invalid_reference?(reference)
23
22
  offense = Packwerk::ReferenceOffense.new(
@@ -8,58 +8,110 @@ 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: T::Array[UnresolvedReference],
55
+ context_provider: ConstantDiscovery
56
+ ).returns(T::Array[Reference])
57
+ end
58
+ def self.get_fully_qualified_references_from(unresolved_references, context_provider)
59
+ fully_qualified_references = T.let([], T::Array[Reference])
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.each do |unresolved_references_or_offense|
62
+ unresolved_reference = unresolved_references_or_offense
45
63
 
46
- constant =
47
- @context_provider.context_for(
48
- constant_name,
49
- current_namespace_path: namespace_path
64
+ constant =
65
+ context_provider.context_for(
66
+ unresolved_reference.constant_name,
67
+ current_namespace_path: unresolved_reference.namespace_path
68
+ )
69
+
70
+ next if constant.nil?
71
+
72
+ package_for_constant = constant.package
73
+
74
+ next if package_for_constant.nil?
75
+
76
+ source_package = context_provider.package_from_path(unresolved_reference.relative_path)
77
+
78
+ next if source_package == package_for_constant
79
+
80
+ fully_qualified_references << Reference.new(
81
+ source_package,
82
+ unresolved_reference.relative_path,
83
+ constant,
84
+ unresolved_reference.source_location
50
85
  )
86
+ end
51
87
 
52
- return if constant&.package.nil?
88
+ fully_qualified_references
89
+ end
53
90
 
54
- relative_path =
55
- Pathname.new(file_path)
56
- .relative_path_from(@root_path).to_s
91
+ private
57
92
 
58
- source_package = @context_provider.package_from_path(relative_path)
93
+ sig do
94
+ params(
95
+ constant_name: String,
96
+ node: Parser::AST::Node,
97
+ ancestors: T::Array[Parser::AST::Node],
98
+ absolute_file: String
99
+ ).returns(T.nilable(UnresolvedReference))
100
+ end
101
+ def reference_from_constant(constant_name, node:, ancestors:, absolute_file:)
102
+ namespace_path = Node.enclosing_namespace_path(node, ancestors: ancestors)
59
103
 
60
- return if source_package == constant.package
104
+ return if local_reference?(constant_name, Node.name_location(node), namespace_path)
61
105
 
62
- Reference.new(source_package, relative_path, constant, Node.location(node))
106
+ relative_file = Pathname.new(absolute_file).relative_path_from(@root_path).to_s
107
+ location = Node.location(node)
108
+
109
+ UnresolvedReference.new(
110
+ constant_name,
111
+ namespace_path,
112
+ relative_file,
113
+ location
114
+ )
63
115
  end
64
116
 
65
117
  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