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
@@ -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'