packwerk 2.0.0 → 2.1.0

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