packwerk 2.0.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a473db2c54adea132ca7e61a51072b570d7e37b7d63c021259eef3cc7625177b
4
- data.tar.gz: 1b8486e61969621fac562d2fb6ebe47dcf6f71fcd13f6d18524337a3ed831ac2
3
+ metadata.gz: 66332315c1155336fbe79f8aeb6e44a0893d4100a1d4b6b5f08c81574c940fe0
4
+ data.tar.gz: 88da350ce07daaf7556d23edbc318a19ca33b74af71ee624d547a8a8598bc16b
5
5
  SHA512:
6
- metadata.gz: 8c05f0ce6e8b398a87e98453c721d00707628918ce8455600c1cb00fb2cb9c68efc6bea373323e889d36fc3658ff45db3508b0e6386b876da58ca50cdb53ab9f
7
- data.tar.gz: 51586b68f489ff1f32df06338d05a64cab753da56c11575b3c5c8fa55019e9a8527e0a20c54c8695e1432307227d26c10f01b0d6c610e41c55d84296be000bd8
6
+ metadata.gz: b54eeeac5e68285e03c32916d72f2afdc291d9d6d4993de0d2875d7dbb4a9cdf8cde7ca2677107f35288a20a20c1f8fc2f9ddcc330c9a7fe0dfaf8e245c07b3c
7
+ data.tar.gz: d8597878958e00a069cb5e51c609e04da9526b3c1933c4824399e4bad1173d718703c88ec582411a9c73fb5c88899707b04ebdb7b296a22b70e25598e11b57d6
data/Gemfile.lock CHANGED
@@ -87,12 +87,13 @@ GIT
87
87
  PATH
88
88
  remote: .
89
89
  specs:
90
- packwerk (2.0.0)
90
+ packwerk (2.1.0)
91
91
  activesupport (>= 5.2)
92
92
  ast
93
93
  better_html
94
94
  bundler
95
95
  constant_resolver
96
+ digest
96
97
  parallel
97
98
  parser
98
99
  sorbet-runtime
@@ -118,6 +119,7 @@ GEM
118
119
  concurrent-ruby (1.1.8)
119
120
  constant_resolver (0.1.5)
120
121
  crass (1.0.6)
122
+ digest (3.1.0)
121
123
  erubi (1.10.0)
122
124
  globalid (0.4.2)
123
125
  activesupport (>= 4.2.0)
@@ -145,8 +147,6 @@ GEM
145
147
  nokogiri (1.12.5)
146
148
  mini_portile2 (~> 2.6.1)
147
149
  racc (~> 1.4)
148
- nokogiri (1.12.5-x86_64-darwin)
149
- racc (~> 1.4)
150
150
  parallel (1.20.1)
151
151
  parlour (6.0.0)
152
152
  commander (~> 4.5)
@@ -257,4 +257,4 @@ DEPENDENCIES
257
257
  tapioca
258
258
 
259
259
  BUNDLED WITH
260
- 2.2.22
260
+ 2.3.4
data/README.md CHANGED
@@ -56,6 +56,13 @@ Read [USAGE.md](USAGE.md) for usage once Packwerk is installed on your project.
56
56
 
57
57
  "Packwerk" is pronounced [[ˈpakvɛʁk]](https://cdn.shopify.com/s/files/1/0258/7469/files/packwerk.mp3).
58
58
 
59
+ ## Ecosystem
60
+
61
+ You can use these third party tools to enhance your packwerk experience:
62
+
63
+ - https://github.com/bellroy/graphwerk draws a graph of your package dependencies
64
+ - https://github.com/Gusto/packwerk-vscode integrates packwerk into Visual Studio Code so you can see violations right in your editor
65
+
59
66
  ## Development
60
67
 
61
68
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/USAGE.md CHANGED
@@ -77,6 +77,8 @@ Packwerk reads from the `packwerk.yml` configuration file in the root directory.
77
77
  | package_paths | **/ | a single pattern or a list of patterns to find package configuration files, see: [Defining packages](#Defining-packages) |
78
78
  | custom_associations | N/A | list of custom associations, if any |
79
79
  | parallel | true | when true, fork code parsing out to subprocesses |
80
+ | cache | false | when true, caches the results of parsing files |
81
+ | cache_directory | tmp/cache/packwerk | the directory that will hold the packwerk cache |
80
82
 
81
83
  ### Using a custom ERB parser
82
84
 
@@ -100,6 +102,11 @@ end
100
102
  Packwerk::Parsers::Factory.instance.erb_parser_class = CustomParser
101
103
  ```
102
104
 
105
+ ## Using the cache
106
+ Packwerk ships with an cache to help speed up file parsing. You can turn this on by setting `cache: true` in `packwerk.yml`.
107
+
108
+ This will write to `tmp/cache/packwerk`.
109
+
103
110
  ## Validating the package system
104
111
 
105
112
  There are some criteria that an application must meet in order to have a valid package system. These criteria include having a valid autoload path cache, package definition files, and application folder structure. The dependency graph within the package system also has to be acyclic.
@@ -39,13 +39,13 @@ module Packwerk
39
39
  load_paths: @configuration.load_paths
40
40
  )
41
41
 
42
- results = []
42
+ results = T.let([], T::Array[Packwerk::Result])
43
43
 
44
44
  privacy_settings.each do |config_file_path, setting|
45
45
  next unless setting.is_a?(Array)
46
46
  constants = setting
47
47
 
48
- assert_constants_can_be_loaded(constants)
48
+ results += assert_constants_can_be_loaded(constants, config_file_path)
49
49
 
50
50
  constant_locations = constants.map { |c| [c, resolver.resolve(c)&.location] }
51
51
 
@@ -265,9 +265,18 @@ module Packwerk
265
265
  !File.file?(package_path)
266
266
  end
267
267
 
268
- def assert_constants_can_be_loaded(constants)
269
- constants.each(&:constantize)
270
- nil
268
+ def assert_constants_can_be_loaded(constants, config_file_path)
269
+ constants.map do |constant|
270
+ if !constant.start_with?("::")
271
+ Result.new(
272
+ false,
273
+ "'#{constant}', listed in the 'enforce_privacy' option in #{config_file_path}, is invalid.\n"\
274
+ "Private constants need to be prefixed with the top-level namespace operator `::`."
275
+ )
276
+ else
277
+ constant.try(&:constantize) && Result.new(true)
278
+ end
279
+ end
271
280
  end
272
281
 
273
282
  def private_constant_unresolvable(name, config_file_path)
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "digest"
5
+
6
+ module Packwerk
7
+ class Cache
8
+ extend T::Sig
9
+
10
+ class CacheContents < T::Struct
11
+ extend T::Sig
12
+
13
+ const :file_contents_digest, String
14
+ const :unresolved_references, T::Array[UnresolvedReference]
15
+
16
+ sig { returns(String) }
17
+ def serialize
18
+ to_json
19
+ end
20
+
21
+ sig { params(serialized_cache_contents: String).returns(CacheContents) }
22
+ def self.deserialize(serialized_cache_contents)
23
+ cache_contents_json = JSON.parse(serialized_cache_contents)
24
+ unresolved_references = cache_contents_json["unresolved_references"].map do |json|
25
+ UnresolvedReference.new(
26
+ json["constant_name"],
27
+ json["namespace_path"],
28
+ json["relative_path"],
29
+ Node::Location.new(json["source_location"]["line"], json["source_location"]["column"],)
30
+ )
31
+ end
32
+
33
+ CacheContents.new(
34
+ file_contents_digest: cache_contents_json["file_contents_digest"],
35
+ unresolved_references: unresolved_references,
36
+ )
37
+ end
38
+ end
39
+
40
+ CACHE_SHAPE = T.type_alias do
41
+ T::Hash[
42
+ String,
43
+ CacheContents
44
+ ]
45
+ end
46
+
47
+ sig { params(enable_cache: T::Boolean, cache_directory: Pathname, config_path: String).void }
48
+ def initialize(enable_cache:, cache_directory:, config_path:)
49
+ @enable_cache = enable_cache
50
+ @cache = T.let({}, CACHE_SHAPE)
51
+ @files_by_digest = T.let({}, T::Hash[String, String])
52
+ @config_path = config_path
53
+ @cache_directory = cache_directory
54
+
55
+ if @enable_cache
56
+ create_cache_directory!
57
+ bust_cache_if_packwerk_yml_has_changed!
58
+ bust_cache_if_inflections_have_changed!
59
+ end
60
+ end
61
+
62
+ sig { void }
63
+ def bust_cache!
64
+ FileUtils.rm_rf(@cache_directory)
65
+ end
66
+
67
+ sig do
68
+ params(
69
+ file_path: String,
70
+ block: T.proc.returns(T::Array[UnresolvedReference])
71
+ ).returns(T::Array[UnresolvedReference])
72
+ end
73
+ def with_cache(file_path, &block)
74
+ return block.call unless @enable_cache
75
+
76
+ cache_location = @cache_directory.join(digest_for_string(file_path))
77
+
78
+ cache_contents = if cache_location.exist?
79
+ T.let(CacheContents.deserialize(cache_location.read),
80
+ CacheContents)
81
+ end
82
+
83
+ file_contents_digest = digest_for_file(file_path)
84
+
85
+ if !cache_contents.nil? && cache_contents.file_contents_digest == file_contents_digest
86
+ Debug.out("Cache hit for #{file_path}")
87
+
88
+ cache_contents.unresolved_references
89
+ else
90
+ Debug.out("Cache miss for #{file_path}")
91
+
92
+ unresolved_references = block.call
93
+
94
+ cache_contents = CacheContents.new(
95
+ file_contents_digest: file_contents_digest,
96
+ unresolved_references: unresolved_references,
97
+ )
98
+ cache_location.write(cache_contents.serialize)
99
+
100
+ unresolved_references
101
+ end
102
+ end
103
+
104
+ sig { params(file: String).returns(String) }
105
+ def digest_for_file(file)
106
+ digest_for_string(File.read(file))
107
+ end
108
+
109
+ sig { params(str: String).returns(String) }
110
+ def digest_for_string(str)
111
+ # MD5 appears to be the fastest
112
+ # https://gist.github.com/morimori/1330095
113
+ Digest::MD5.hexdigest(str)
114
+ end
115
+
116
+ sig { void }
117
+ def bust_cache_if_packwerk_yml_has_changed!
118
+ bust_cache_if_contents_have_changed(File.read(@config_path), :packwerk_yml)
119
+ end
120
+
121
+ sig { void }
122
+ def bust_cache_if_inflections_have_changed!
123
+ bust_cache_if_contents_have_changed(YAML.dump(ActiveSupport::Inflector.inflections), :inflections)
124
+ end
125
+
126
+ sig { params(contents: String, contents_key: Symbol).void }
127
+ def bust_cache_if_contents_have_changed(contents, contents_key)
128
+ current_digest = digest_for_string(contents)
129
+ cached_digest_path = @cache_directory.join(contents_key.to_s)
130
+
131
+ if !cached_digest_path.exist?
132
+ # In this case, we have nothing cached
133
+ # We save the current digest. This way the next time we compare current digest to cached digest,
134
+ # we can accurately determine if we should bust the cache
135
+ cached_digest_path.write(current_digest)
136
+
137
+ nil
138
+ elsif cached_digest_path.read == current_digest
139
+ Debug.out("#{contents_key} contents have NOT changed, preserving cache")
140
+ else
141
+ Debug.out("#{contents_key} contents have changed, busting cache")
142
+
143
+ bust_cache!
144
+ create_cache_directory!
145
+
146
+ cached_digest_path.write(current_digest)
147
+ end
148
+ end
149
+
150
+ sig { void }
151
+ def create_cache_directory!
152
+ FileUtils.mkdir_p(@cache_directory)
153
+ end
154
+ end
155
+
156
+ class Debug
157
+ extend T::Sig
158
+
159
+ sig { params(out: String).void }
160
+ def self.out(out)
161
+ if ENV["DEBUG_PACKWERK_CACHE"]
162
+ puts(out)
163
+ end
164
+ end
165
+ end
166
+
167
+ private_constant :Debug
168
+ end
@@ -34,10 +34,21 @@ module Packwerk
34
34
  DEFAULT_EXCLUDE_GLOBS = ["{bin,node_modules,script,tmp,vendor}/**/*"]
35
35
 
36
36
  attr_reader(
37
- :include, :exclude, :root_path, :package_paths, :custom_associations, :config_path
37
+ :include, :exclude, :root_path, :package_paths, :custom_associations, :config_path, :cache_directory
38
38
  )
39
39
 
40
40
  def initialize(configs = {}, config_path: nil)
41
+ @include = configs["include"] || DEFAULT_INCLUDE_GLOBS
42
+ @exclude = configs["exclude"] || DEFAULT_EXCLUDE_GLOBS
43
+ root = config_path ? File.dirname(config_path) : "."
44
+ @root_path = File.expand_path(root)
45
+ @package_paths = configs["package_paths"] || "**/"
46
+ @custom_associations = configs["custom_associations"] || []
47
+ @parallel = configs.key?("parallel") ? configs["parallel"] : true
48
+ @cache_enabled = configs.key?("cache") ? configs["cache"] : false
49
+ @cache_directory = Pathname.new(configs["cache_directory"] || "tmp/cache/packwerk")
50
+ @config_path = config_path
51
+
41
52
  if configs["load_paths"]
42
53
  warning = <<~WARNING
43
54
  DEPRECATION WARNING: The 'load_paths' key in `packwerk.yml` is deprecated.
@@ -47,7 +58,6 @@ module Packwerk
47
58
  warn(warning)
48
59
  end
49
60
 
50
- inflection_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
51
61
  if configs["inflections_file"]
52
62
  warning = <<~WARNING
53
63
  DEPRECATION WARNING: The 'inflections_file' key in `packwerk.yml` is deprecated.
@@ -58,6 +68,7 @@ module Packwerk
58
68
  warn(warning)
59
69
  end
60
70
 
71
+ inflection_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
61
72
  if Pathname.new(inflection_file).exist?
62
73
  warning = <<~WARNING
63
74
  DEPRECATION WARNING: Inflections YMLs in packwerk are now deprecated.
@@ -66,16 +77,6 @@ module Packwerk
66
77
 
67
78
  warn(warning)
68
79
  end
69
-
70
- @include = configs["include"] || DEFAULT_INCLUDE_GLOBS
71
- @exclude = configs["exclude"] || DEFAULT_EXCLUDE_GLOBS
72
- root = config_path ? File.dirname(config_path) : "."
73
- @root_path = File.expand_path(root)
74
- @package_paths = configs["package_paths"] || "**/"
75
- @custom_associations = configs["custom_associations"] || []
76
- @parallel = configs.key?("parallel") ? configs["parallel"] : true
77
-
78
- @config_path = config_path
79
80
  end
80
81
 
81
82
  def load_paths
@@ -85,5 +86,9 @@ module Packwerk
85
86
  def parallel?
86
87
  @parallel
87
88
  end
89
+
90
+ def cache_enabled?
91
+ @cache_enabled
92
+ end
88
93
  end
89
94
  end
@@ -4,7 +4,7 @@
4
4
  require "constant_resolver"
5
5
 
6
6
  module Packwerk
7
- # Get information about (partially qualified) constants without loading the application code.
7
+ # Get information about unresolved constants without loading the application code.
8
8
  # Information gathered: Fully qualified name, path to file containing the definition, package,
9
9
  # and visibility (public/private to the package).
10
10
  #
@@ -35,9 +35,9 @@ module Packwerk
35
35
  end
36
36
 
37
37
  # Analyze a constant via its name.
38
- # If the name is partially qualified, we need the current namespace path to correctly infer its full name
38
+ # If the constant is unresolved, we need the current namespace path to correctly infer its full name
39
39
  #
40
- # @param const_name [String] The constant's name, fully or partially qualified.
40
+ # @param const_name [String] The unresolved constant's name.
41
41
  # @param current_namespace_path [Array<String>] (optional) The namespace of the context in which the constant is
42
42
  # used, e.g. ["Apps", "Models"] for `Apps::Models`. Defaults to [] which means top level.
43
43
  # @return [Packwerk::ConstantDiscovery::ConstantContext]
@@ -16,6 +16,7 @@ module Packwerk
16
16
  @package = package
17
17
  @filepath = filepath
18
18
  @new_entries = T.let({}, ENTRIES_TYPE)
19
+ @deprecated_references = T.let(nil, T.nilable(ENTRIES_TYPE))
19
20
  end
20
21
 
21
22
  sig do
@@ -103,7 +104,6 @@ module Packwerk
103
104
 
104
105
  sig { returns(ENTRIES_TYPE) }
105
106
  def deprecated_references
106
- @deprecated_references ||= T.let(@deprecated_references, T.nilable(ENTRIES_TYPE))
107
107
  @deprecated_references ||= if File.exist?(@filepath)
108
108
  load_yaml(@filepath)
109
109
  else
@@ -13,8 +13,16 @@ module Packwerk
13
13
  end
14
14
  end
15
15
 
16
- def initialize(node_processor_factory:, parser_factory: nil)
16
+ sig do
17
+ params(
18
+ node_processor_factory: NodeProcessorFactory,
19
+ cache: Cache,
20
+ parser_factory: T.nilable(Parsers::Factory)
21
+ ).void
22
+ end
23
+ def initialize(node_processor_factory:, cache:, parser_factory: nil)
17
24
  @node_processor_factory = node_processor_factory
25
+ @cache = cache
18
26
  @parser_factory = parser_factory || Packwerk::Parsers::Factory.instance
19
27
  end
20
28
 
@@ -22,7 +30,7 @@ module Packwerk
22
30
  params(file_path: String).returns(
23
31
  T::Array[
24
32
  T.any(
25
- Packwerk::Reference,
33
+ Packwerk::UnresolvedReference,
26
34
  Packwerk::Offense,
27
35
  )
28
36
  ]
@@ -31,16 +39,22 @@ module Packwerk
31
39
  def call(file_path)
32
40
  return [UnknownFileTypeResult.new(file: file_path)] if parser_for(file_path).nil?
33
41
 
34
- node = parse_into_ast(file_path)
35
- return [] unless node
42
+ @cache.with_cache(file_path) do
43
+ node = parse_into_ast(file_path)
44
+
45
+ return [] unless node
36
46
 
37
- references_from_ast(node, file_path)
47
+ references_from_ast(node, file_path)
48
+ end
38
49
  rescue Parsers::ParseError => e
39
50
  [e.result]
40
51
  end
41
52
 
42
53
  private
43
54
 
55
+ sig do
56
+ params(node: Parser::AST::Node, file_path: String).returns(T::Array[UnresolvedReference])
57
+ end
44
58
  def references_from_ast(node, file_path)
45
59
  references = []
46
60
 
@@ -15,3 +15,9 @@
15
15
  # List of custom associations, if any
16
16
  # custom_associations:
17
17
  # - "cache_belongs_to"
18
+
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
@@ -21,7 +21,7 @@ module Packwerk
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)
@@ -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
@@ -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) }
@@ -94,7 +95,7 @@ module Packwerk
94
95
  sig { params(file_path: T.any(Pathname, String)).returns(T.nilable(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] ||= packages.values.find { |package| package.package_path?(path_string) }
98
99
  end
99
100
  end
100
101
  end
@@ -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
@@ -8,58 +8,105 @@ 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
 
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, Node.location(node))
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)
@@ -25,12 +25,16 @@ module Packwerk
25
25
  class << self
26
26
  def from_configuration(configuration)
27
27
  inflector = ActiveSupport::Inflector
28
+
28
29
  new(
29
30
  root_path: configuration.root_path,
30
31
  load_paths: configuration.load_paths,
31
32
  package_paths: configuration.package_paths,
32
33
  inflector: inflector,
33
- custom_associations: configuration.custom_associations
34
+ custom_associations: configuration.custom_associations,
35
+ cache_enabled: configuration.cache_enabled?,
36
+ cache_directory: configuration.cache_directory,
37
+ config_path: configuration.config_path,
34
38
  )
35
39
  end
36
40
  end
@@ -41,7 +45,10 @@ module Packwerk
41
45
  package_paths: nil,
42
46
  inflector: nil,
43
47
  custom_associations: [],
44
- checker_classes: DEFAULT_CHECKERS
48
+ checker_classes: DEFAULT_CHECKERS,
49
+ cache_enabled: false,
50
+ cache_directory: nil,
51
+ config_path: nil
45
52
  )
46
53
  @root_path = root_path
47
54
  @load_paths = load_paths
@@ -49,21 +56,27 @@ module Packwerk
49
56
  @inflector = inflector
50
57
  @custom_associations = custom_associations
51
58
  @checker_classes = checker_classes
59
+ @cache_enabled = cache_enabled
60
+ @cache_directory = cache_directory
61
+ @config_path = config_path
52
62
  end
53
63
 
54
64
  sig { params(file: String).returns(T::Array[Packwerk::Offense]) }
55
65
  def process_file(file:)
56
- references = file_processor.call(file)
57
-
66
+ unresolved_references_and_offenses = file_processor.call(file)
67
+ references_and_offenses = ReferenceExtractor.get_fully_qualified_references_and_offenses_from(
68
+ unresolved_references_and_offenses,
69
+ context_provider
70
+ )
58
71
  reference_checker = ReferenceChecking::ReferenceChecker.new(checkers)
59
- references.flat_map { |reference| reference_checker.call(reference) }
72
+ references_and_offenses.flat_map { |reference| reference_checker.call(reference) }
60
73
  end
61
74
 
62
75
  private
63
76
 
64
77
  sig { returns(FileProcessor) }
65
78
  def file_processor
66
- @file_processor ||= FileProcessor.new(node_processor_factory: node_processor_factory)
79
+ @file_processor ||= FileProcessor.new(node_processor_factory: node_processor_factory, cache: cache)
67
80
  end
68
81
 
69
82
  sig { returns(NodeProcessorFactory) }
@@ -77,7 +90,7 @@ module Packwerk
77
90
 
78
91
  sig { returns(ConstantDiscovery) }
79
92
  def context_provider
80
- ::Packwerk::ConstantDiscovery.new(
93
+ @context_provider ||= ::Packwerk::ConstantDiscovery.new(
81
94
  constant_resolver: resolver,
82
95
  packages: package_set
83
96
  )
@@ -92,6 +105,11 @@ module Packwerk
92
105
  )
93
106
  end
94
107
 
108
+ sig { returns(Cache) }
109
+ def cache
110
+ @cache ||= Cache.new(enable_cache: @cache_enabled, cache_directory: @cache_directory, config_path: @config_path)
111
+ end
112
+
95
113
  sig { returns(PackageSet) }
96
114
  def package_set
97
115
  ::Packwerk::PackageSet.load_all_from(root_path, package_pathspec: package_paths)
@@ -0,0 +1,10 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ # An unresolved reference from a file in one package to a constant that may be defined in a different package.
6
+ # Unresolved means that we know how it's referred to in the file,
7
+ # and we have enough context on that reference to figure out the fully qualified reference such that we
8
+ # can produce a Reference in a separate pass. However, we have not yet resolved it to its fully qualified version.
9
+ UnresolvedReference = Struct.new(:constant_name, :namespace_path, :relative_path, :source_location)
10
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
- VERSION = "2.0.0"
5
+ VERSION = "2.1.0"
6
6
  end
data/lib/packwerk.rb CHANGED
@@ -15,6 +15,7 @@ module Packwerk
15
15
  autoload :ApplicationValidator
16
16
  autoload :AssociationInspector
17
17
  autoload :OffenseCollection
18
+ autoload :Cache
18
19
  autoload :Cli
19
20
  autoload :Configuration
20
21
  autoload :ConstantDiscovery
@@ -36,6 +37,7 @@ module Packwerk
36
37
  autoload :ParsedConstantDefinitions
37
38
  autoload :Parsers
38
39
  autoload :ParseRun
40
+ autoload :UnresolvedReference
39
41
  autoload :Reference
40
42
  autoload :ReferenceExtractor
41
43
  autoload :ReferenceOffense
data/packwerk.gemspec CHANGED
@@ -45,6 +45,7 @@ Gem::Specification.new do |spec|
45
45
  spec.add_dependency("parallel")
46
46
  spec.add_dependency("sorbet-runtime")
47
47
  spec.add_dependency("bundler")
48
+ spec.add_dependency("digest")
48
49
 
49
50
  spec.add_development_dependency("rake")
50
51
  spec.add_development_dependency("sorbet")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: packwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-08 00:00:00.000000000 Z
11
+ date: 2022-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: digest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rake
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -222,6 +236,7 @@ files:
222
236
  - lib/packwerk/application_load_paths.rb
223
237
  - lib/packwerk/application_validator.rb
224
238
  - lib/packwerk/association_inspector.rb
239
+ - lib/packwerk/cache.rb
225
240
  - lib/packwerk/cli.rb
226
241
  - lib/packwerk/configuration.rb
227
242
  - lib/packwerk/const_node_inspector.rb
@@ -266,6 +281,7 @@ files:
266
281
  - lib/packwerk/run_context.rb
267
282
  - lib/packwerk/sanity_checker.rb
268
283
  - lib/packwerk/spring_command.rb
284
+ - lib/packwerk/unresolved_reference.rb
269
285
  - lib/packwerk/version.rb
270
286
  - lib/packwerk/violation_type.rb
271
287
  - library.yml