packwerk 2.0.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,169 @@
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: T.nilable(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
+ return nil if @config_path.nil?
119
+ bust_cache_if_contents_have_changed(File.read(@config_path), :packwerk_yml)
120
+ end
121
+
122
+ sig { void }
123
+ def bust_cache_if_inflections_have_changed!
124
+ bust_cache_if_contents_have_changed(YAML.dump(ActiveSupport::Inflector.inflections), :inflections)
125
+ end
126
+
127
+ sig { params(contents: String, contents_key: Symbol).void }
128
+ def bust_cache_if_contents_have_changed(contents, contents_key)
129
+ current_digest = digest_for_string(contents)
130
+ cached_digest_path = @cache_directory.join(contents_key.to_s)
131
+
132
+ if !cached_digest_path.exist?
133
+ # In this case, we have nothing cached
134
+ # We save the current digest. This way the next time we compare current digest to cached digest,
135
+ # we can accurately determine if we should bust the cache
136
+ cached_digest_path.write(current_digest)
137
+
138
+ nil
139
+ elsif cached_digest_path.read == current_digest
140
+ Debug.out("#{contents_key} contents have NOT changed, preserving cache")
141
+ else
142
+ Debug.out("#{contents_key} contents have changed, busting cache")
143
+
144
+ bust_cache!
145
+ create_cache_directory!
146
+
147
+ cached_digest_path.write(current_digest)
148
+ end
149
+ end
150
+
151
+ sig { void }
152
+ def create_cache_directory!
153
+ FileUtils.mkdir_p(@cache_directory)
154
+ end
155
+ end
156
+
157
+ class Debug
158
+ extend T::Sig
159
+
160
+ sig { params(out: String).void }
161
+ def self.out(out)
162
+ if ENV["DEBUG_PACKWERK_CACHE"]
163
+ puts(out)
164
+ end
165
+ end
166
+ end
167
+
168
+ private_constant :Debug
169
+ end
data/lib/packwerk/cli.rb CHANGED
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "optparse"
@@ -30,9 +30,12 @@ module Packwerk
30
30
  @err_out = err_out
31
31
  @environment = environment
32
32
  @style = style
33
- @configuration = configuration || Configuration.from_path
34
- @progress_formatter = Formatters::ProgressFormatter.new(@out, style: style)
35
- @offenses_formatter = offenses_formatter || Formatters::OffensesFormatter.new(style: @style)
33
+ @configuration = T.let(configuration || Configuration.from_path, Configuration)
34
+ @progress_formatter = T.let(Formatters::ProgressFormatter.new(@out, style: style), Formatters::ProgressFormatter)
35
+ @offenses_formatter = T.let(
36
+ offenses_formatter || Formatters::OffensesFormatter.new(style: @style),
37
+ OffensesFormatter
38
+ )
36
39
  end
37
40
 
38
41
  sig { params(args: T::Array[String]).returns(T.noreturn) }
@@ -78,12 +81,14 @@ module Packwerk
78
81
 
79
82
  private
80
83
 
84
+ sig { returns(T::Boolean) }
81
85
  def init
82
86
  @out.puts("📦 Initializing Packwerk...")
83
87
 
84
88
  generate_configs
85
89
  end
86
90
 
91
+ sig { returns(T::Boolean) }
87
92
  def generate_configs
88
93
  configuration_file = Packwerk::Generators::ConfigurationFile.generate(
89
94
  root: @configuration.root_path,
@@ -112,23 +117,31 @@ module Packwerk
112
117
  success
113
118
  end
114
119
 
120
+ sig { params(result: Result).returns(T::Boolean) }
115
121
  def output_result(result)
116
122
  @out.puts
117
123
  @out.puts(result.message)
118
124
  result.status
119
125
  end
120
126
 
121
- def fetch_files_to_process(paths, ignore_nested_packages)
122
- files = FilesForProcessing.fetch(
123
- paths: paths,
127
+ sig do
128
+ params(
129
+ relative_file_paths: T::Array[String],
130
+ ignore_nested_packages: T::Boolean
131
+ ).returns(FilesForProcessing::AbsoluteFileSet)
132
+ end
133
+ def fetch_files_to_process(relative_file_paths, ignore_nested_packages)
134
+ absolute_file_set = FilesForProcessing.fetch(
135
+ relative_file_paths: relative_file_paths,
124
136
  ignore_nested_packages: ignore_nested_packages,
125
137
  configuration: @configuration
126
138
  )
127
139
  abort("No files found or given. "\
128
- "Specify files or check the include and exclude glob in the config file.") if files.empty?
129
- files
140
+ "Specify files or check the include and exclude glob in the config file.") if absolute_file_set.empty?
141
+ absolute_file_set
130
142
  end
131
143
 
144
+ sig { params(_paths: T::Array[String]).returns(T::Boolean) }
132
145
  def validate(_paths)
133
146
  @progress_formatter.started_validation do
134
147
  result = checker.check_all
@@ -139,6 +152,7 @@ module Packwerk
139
152
  end
140
153
  end
141
154
 
155
+ sig { returns(ApplicationValidator) }
142
156
  def checker
143
157
  Packwerk::ApplicationValidator.new(
144
158
  config_file_path: @configuration.config_path,
@@ -147,6 +161,7 @@ module Packwerk
147
161
  )
148
162
  end
149
163
 
164
+ sig { params(result: ApplicationValidator::Result).void }
150
165
  def list_validation_errors(result)
151
166
  @out.puts
152
167
  if result.ok?
@@ -157,24 +172,25 @@ module Packwerk
157
172
  end
158
173
  end
159
174
 
175
+ sig { params(params: T.untyped).returns(ParseRun) }
160
176
  def parse_run(params)
161
- paths = T.let([], T::Array[String])
177
+ relative_file_paths = T.let([], T::Array[String])
162
178
  ignore_nested_packages = nil
163
179
 
164
180
  if params.any? { |p| p.include?("--packages") }
165
181
  OptionParser.new do |parser|
166
182
  parser.on("--packages=PACKAGESLIST", Array, "package names, comma separated") do |p|
167
- paths = p
183
+ relative_file_paths = p
168
184
  end
169
185
  end.parse!(params)
170
186
  ignore_nested_packages = true
171
187
  else
172
- paths = params
188
+ relative_file_paths = params
173
189
  ignore_nested_packages = false
174
190
  end
175
191
 
176
192
  ParseRun.new(
177
- files: fetch_files_to_process(paths, ignore_nested_packages),
193
+ absolute_file_set: fetch_files_to_process(relative_file_paths, ignore_nested_packages),
178
194
  configuration: @configuration,
179
195
  progress_formatter: @progress_formatter,
180
196
  offenses_formatter: @offenses_formatter
@@ -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
  #
@@ -15,10 +15,15 @@ module Packwerk
15
15
  # have no way of inferring the file it is defined in. You could argue though that inheritance means that another
16
16
  # constant with the same name exists in the inheriting class, and this view is sufficient for all our use cases.
17
17
  class ConstantDiscovery
18
+ extend T::Sig
19
+
18
20
  ConstantContext = Struct.new(:name, :location, :package, :public?)
19
21
 
20
22
  # @param constant_resolver [ConstantResolver]
21
23
  # @param packages [Packwerk::PackageSet]
24
+ sig do
25
+ params(constant_resolver: ConstantResolver, packages: Packwerk::PackageSet).void
26
+ end
22
27
  def initialize(constant_resolver:, packages:)
23
28
  @packages = packages
24
29
  @resolver = constant_resolver
@@ -30,17 +35,28 @@ module Packwerk
30
35
  #
31
36
  # @return [Packwerk::Package] the package that contains the given file,
32
37
  # or nil if the path is not owned by any component
38
+ sig do
39
+ params(
40
+ path: String,
41
+ ).returns(Packwerk::Package)
42
+ end
33
43
  def package_from_path(path)
34
44
  @packages.package_from_path(path)
35
45
  end
36
46
 
37
47
  # 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
48
+ # If the constant is unresolved, we need the current namespace path to correctly infer its full name
39
49
  #
40
- # @param const_name [String] The constant's name, fully or partially qualified.
50
+ # @param const_name [String] The unresolved constant's name.
41
51
  # @param current_namespace_path [Array<String>] (optional) The namespace of the context in which the constant is
42
52
  # used, e.g. ["Apps", "Models"] for `Apps::Models`. Defaults to [] which means top level.
43
53
  # @return [Packwerk::ConstantDiscovery::ConstantContext]
54
+ sig do
55
+ params(
56
+ const_name: String,
57
+ current_namespace_path: T.nilable(T::Array[String]),
58
+ ).returns(T.nilable(ConstantDiscovery::ConstantContext))
59
+ end
44
60
  def context_for(const_name, current_namespace_path: [])
45
61
  begin
46
62
  constant = @resolver.resolve(const_name, current_namespace_path: current_namespace_path)
@@ -55,7 +71,7 @@ module Packwerk
55
71
  constant.name,
56
72
  constant.location,
57
73
  package,
58
- package&.public_path?(constant.location),
74
+ package.public_path?(constant.location),
59
75
  )
60
76
  end
61
77
  end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "ast"
@@ -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
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "ast/node"
@@ -8,55 +8,76 @@ module Packwerk
8
8
  extend T::Sig
9
9
 
10
10
  class UnknownFileTypeResult < Offense
11
+ extend T::Sig
12
+
13
+ sig { params(file: String).void }
11
14
  def initialize(file:)
12
15
  super(file: file, message: "unknown file type")
13
16
  end
14
17
  end
15
18
 
16
- def initialize(node_processor_factory:, parser_factory: nil)
19
+ sig do
20
+ params(
21
+ node_processor_factory: NodeProcessorFactory,
22
+ cache: Cache,
23
+ parser_factory: T.nilable(Parsers::Factory)
24
+ ).void
25
+ end
26
+ def initialize(node_processor_factory:, cache:, parser_factory: nil)
17
27
  @node_processor_factory = node_processor_factory
18
- @parser_factory = parser_factory || Packwerk::Parsers::Factory.instance
28
+ @cache = cache
29
+ @parser_factory = T.let(parser_factory || Packwerk::Parsers::Factory.instance, Parsers::Factory)
30
+ end
31
+
32
+ class ProcessedFile < T::Struct
33
+ const :unresolved_references, T::Array[UnresolvedReference], default: []
34
+ const :offenses, T::Array[Offense], default: []
19
35
  end
20
36
 
21
37
  sig do
22
- params(file_path: String).returns(
23
- T::Array[
24
- T.any(
25
- Packwerk::Reference,
26
- Packwerk::Offense,
27
- )
28
- ]
29
- )
38
+ params(absolute_file: String).returns(ProcessedFile)
30
39
  end
31
- def call(file_path)
32
- return [UnknownFileTypeResult.new(file: file_path)] if parser_for(file_path).nil?
40
+ def call(absolute_file)
41
+ parser = parser_for(absolute_file)
42
+ if parser.nil?
43
+ return ProcessedFile.new(offenses: [UnknownFileTypeResult.new(file: absolute_file)])
44
+ end
45
+
46
+ unresolved_references = @cache.with_cache(absolute_file) do
47
+ node = parse_into_ast(absolute_file, parser)
48
+ return ProcessedFile.new unless node
33
49
 
34
- node = parse_into_ast(file_path)
35
- return [] unless node
50
+ references_from_ast(node, absolute_file)
51
+ end
36
52
 
37
- references_from_ast(node, file_path)
53
+ ProcessedFile.new(unresolved_references: unresolved_references)
38
54
  rescue Parsers::ParseError => e
39
- [e.result]
55
+ ProcessedFile.new(offenses: [e.result])
40
56
  end
41
57
 
42
58
  private
43
59
 
44
- def references_from_ast(node, file_path)
60
+ sig do
61
+ params(node: Parser::AST::Node, absolute_file: String).returns(T::Array[UnresolvedReference])
62
+ end
63
+ def references_from_ast(node, absolute_file)
45
64
  references = []
46
65
 
47
- node_processor = @node_processor_factory.for(filename: file_path, node: node)
66
+ node_processor = @node_processor_factory.for(absolute_file: absolute_file, node: node)
48
67
  node_visitor = Packwerk::NodeVisitor.new(node_processor: node_processor)
49
68
  node_visitor.visit(node, ancestors: [], result: references)
50
69
 
51
70
  references
52
71
  end
53
72
 
54
- def parse_into_ast(file_path)
55
- File.open(file_path, "r", nil, external_encoding: Encoding::UTF_8) do |file|
56
- parser_for(file_path).call(io: file, file_path: file_path)
73
+ sig { params(absolute_file: String, parser: Parsers::ParserInterface).returns(T.untyped) }
74
+ def parse_into_ast(absolute_file, parser)
75
+ File.open(absolute_file, "r", nil, external_encoding: Encoding::UTF_8) do |file|
76
+ parser.call(io: file, file_path: absolute_file)
57
77
  end
58
78
  end
59
79
 
80
+ sig { params(file_path: String).returns(T.nilable(Parsers::ParserInterface)) }
60
81
  def parser_for(file_path)
61
82
  @parser_factory.for_path(file_path)
62
83
  end
@@ -1,20 +1,42 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
5
  class FilesForProcessing
6
+ extend T::Sig
7
+
8
+ AbsoluteFileSet = T.type_alias { T::Set[String] }
9
+
6
10
  class << self
7
- def fetch(paths:, configuration:, ignore_nested_packages: false)
8
- new(paths, configuration, ignore_nested_packages).files
11
+ extend T::Sig
12
+
13
+ sig do
14
+ params(
15
+ relative_file_paths: T::Array[String],
16
+ configuration: Configuration,
17
+ ignore_nested_packages: T::Boolean
18
+ ).returns(AbsoluteFileSet)
19
+ end
20
+ def fetch(relative_file_paths:, configuration:, ignore_nested_packages: false)
21
+ new(relative_file_paths, configuration, ignore_nested_packages).files
9
22
  end
10
23
  end
11
24
 
12
- def initialize(paths, configuration, ignore_nested_packages)
13
- @paths = paths
25
+ sig do
26
+ params(
27
+ relative_file_paths: T::Array[String],
28
+ configuration: Configuration,
29
+ ignore_nested_packages: T::Boolean
30
+ ).void
31
+ end
32
+ def initialize(relative_file_paths, configuration, ignore_nested_packages)
33
+ @relative_file_paths = relative_file_paths
14
34
  @configuration = configuration
15
35
  @ignore_nested_packages = ignore_nested_packages
36
+ @custom_files = T.let(nil, T.nilable(AbsoluteFileSet))
16
37
  end
17
38
 
39
+ sig { returns(AbsoluteFileSet) }
18
40
  def files
19
41
  include_files = if custom_files.empty?
20
42
  configured_included_files
@@ -27,52 +49,59 @@ module Packwerk
27
49
 
28
50
  private
29
51
 
52
+ sig { returns(AbsoluteFileSet) }
30
53
  def custom_files
31
- @custom_files ||= @paths.flat_map do |path|
32
- path = File.expand_path(path, @configuration.root_path)
33
- if File.file?(path)
34
- path
35
- else
36
- custom_included_files(path)
54
+ @custom_files ||= Set.new(
55
+ @relative_file_paths.map do |relative_file_path|
56
+ absolute_file_path = File.expand_path(relative_file_path, @configuration.root_path)
57
+ if File.file?(absolute_file_path)
58
+ absolute_file_path
59
+ else
60
+ custom_included_files(absolute_file_path)
61
+ end
37
62
  end
38
- end
63
+ ).flatten
39
64
  end
40
65
 
41
- def custom_included_files(path)
66
+ sig { params(absolute_file_path: String).returns(AbsoluteFileSet) }
67
+ def custom_included_files(absolute_file_path)
42
68
  # Note, assuming include globs are always relative paths
43
69
  absolute_includes = @configuration.include.map do |glob|
44
70
  File.expand_path(glob, @configuration.root_path)
45
71
  end
46
72
 
47
- files = Dir.glob([File.join(path, "**", "*")]).select do |file_path|
73
+ absolute_files = Dir.glob([File.join(absolute_file_path, "**", "*")]).select do |absolute_path|
48
74
  absolute_includes.any? do |pattern|
49
- File.fnmatch?(pattern, file_path, File::FNM_EXTGLOB)
75
+ File.fnmatch?(pattern, absolute_path, File::FNM_EXTGLOB)
50
76
  end
51
77
  end
52
78
 
53
79
  if @ignore_nested_packages
54
- nested_packages_paths = Dir.glob(File.join(path, "*", "**", "package.yml"))
55
- nested_packages_globs = nested_packages_paths.map { |npp| npp.gsub("package.yml", "**/*") }
56
- nested_packages_globs.each do |glob|
57
- files -= Dir.glob(glob)
80
+ nested_packages_absolute_file_paths = Dir.glob(File.join(absolute_file_path, "*", "**", "package.yml"))
81
+ nested_packages_absolute_globs = nested_packages_absolute_file_paths.map do |npp|
82
+ npp.gsub("package.yml", "**/*")
83
+ end
84
+ nested_packages_absolute_globs.each do |absolute_glob|
85
+ absolute_files -= Dir.glob(absolute_glob)
58
86
  end
59
87
  end
60
88
 
61
- files
89
+ Set.new(absolute_files)
62
90
  end
63
91
 
92
+ sig { returns(AbsoluteFileSet) }
64
93
  def configured_included_files
65
- files_for_globs(@configuration.include)
94
+ absolute_files_for_globs(@configuration.include)
66
95
  end
67
96
 
97
+ sig { returns(AbsoluteFileSet) }
68
98
  def configured_excluded_files
69
- files_for_globs(@configuration.exclude)
99
+ absolute_files_for_globs(@configuration.exclude)
70
100
  end
71
101
 
72
- def files_for_globs(globs)
73
- globs
74
- .flat_map { |glob| Dir[File.expand_path(glob, @configuration.root_path)] }
75
- .uniq
102
+ sig { params(relative_globs: T::Array[String]).returns(AbsoluteFileSet) }
103
+ def absolute_files_for_globs(relative_globs)
104
+ Set.new(relative_globs.flat_map { |glob| Dir[File.expand_path(glob, @configuration.root_path)] })
76
105
  end
77
106
  end
78
107
  end
@@ -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'