packwerk 2.3.0 → 3.0.1

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -5
  3. data/.ruby-version +1 -1
  4. data/Gemfile +0 -1
  5. data/Gemfile.lock +5 -95
  6. data/README.md +2 -7
  7. data/RESOLVING_VIOLATIONS.md +3 -8
  8. data/TROUBLESHOOT.md +2 -25
  9. data/UPGRADING.md +12 -0
  10. data/USAGE.md +136 -54
  11. data/dev.yml +1 -1
  12. data/exe/packwerk +4 -0
  13. data/gemfiles/Gemfile-rails-6-1 +1 -1
  14. data/lib/packwerk/application_validator.rb +54 -285
  15. data/lib/packwerk/association_inspector.rb +2 -0
  16. data/lib/packwerk/cache.rb +6 -5
  17. data/lib/packwerk/checker.rb +54 -0
  18. data/lib/packwerk/cli/result.rb +11 -0
  19. data/lib/packwerk/cli.rb +55 -40
  20. data/lib/packwerk/configuration.rb +61 -40
  21. data/lib/packwerk/const_node_inspector.rb +2 -0
  22. data/lib/packwerk/constant_context.rb +8 -0
  23. data/lib/packwerk/constant_discovery.rb +5 -6
  24. data/lib/packwerk/constant_name_inspector.rb +2 -0
  25. data/lib/packwerk/disable_sorbet.rb +41 -0
  26. data/lib/packwerk/extension_loader.rb +24 -0
  27. data/lib/packwerk/file_processor.rb +3 -1
  28. data/lib/packwerk/files_for_processing.rb +25 -12
  29. data/lib/packwerk/formatters/default_offenses_formatter.rb +77 -0
  30. data/lib/packwerk/formatters/progress_formatter.rb +31 -12
  31. data/lib/packwerk/generators/configuration_file.rb +7 -2
  32. data/lib/packwerk/generators/root_package.rb +5 -1
  33. data/lib/packwerk/generators/templates/package.yml +0 -10
  34. data/lib/packwerk/graph.rb +10 -2
  35. data/lib/packwerk/node.rb +1 -1
  36. data/lib/packwerk/node_helpers.rb +14 -7
  37. data/lib/packwerk/node_processor.rb +2 -0
  38. data/lib/packwerk/node_processor_factory.rb +6 -4
  39. data/lib/packwerk/node_visitor.rb +10 -1
  40. data/lib/packwerk/offense_collection.rb +26 -18
  41. data/lib/packwerk/offenses_formatter.rb +59 -2
  42. data/lib/packwerk/package.rb +7 -35
  43. data/lib/packwerk/package_set.rb +1 -1
  44. data/lib/packwerk/package_todo.rb +19 -20
  45. data/lib/packwerk/parse_run.rb +27 -34
  46. data/lib/packwerk/parsed_constant_definitions.rb +28 -5
  47. data/lib/packwerk/parsers/erb.rb +23 -4
  48. data/lib/packwerk/parsers/factory.rb +11 -2
  49. data/lib/packwerk/parsers/parser_interface.rb +1 -1
  50. data/lib/packwerk/parsers/ruby.rb +13 -3
  51. data/lib/packwerk/parsers.rb +6 -2
  52. data/lib/packwerk/{application_load_paths.rb → rails_load_paths.rb} +6 -4
  53. data/lib/packwerk/reference.rb +7 -1
  54. data/lib/packwerk/reference_checking/checkers/dependency_checker.rb +29 -6
  55. data/lib/packwerk/reference_checking/reference_checker.rb +1 -1
  56. data/lib/packwerk/reference_extractor.rb +24 -12
  57. data/lib/packwerk/reference_offense.rb +2 -2
  58. data/lib/packwerk/run_context.rb +7 -10
  59. data/lib/packwerk/spring_command.rb +9 -2
  60. data/lib/packwerk/unresolved_reference.rb +9 -1
  61. data/lib/packwerk/validator/result.rb +18 -0
  62. data/lib/packwerk/validator.rb +90 -0
  63. data/lib/packwerk/validators/dependency_validator.rb +154 -0
  64. data/lib/packwerk/version.rb +1 -1
  65. data/lib/packwerk.rb +64 -26
  66. data/packwerk.gemspec +4 -2
  67. data/sorbet/rbi/gems/{zeitwerk@2.6.0.rbi → zeitwerk@2.6.4.rbi} +291 -228
  68. data/sorbet/rbi/shims/minitest/test.rb +8 -0
  69. data/sorbet/rbi/shims/packwerk/reference.rbi +33 -0
  70. data/sorbet/rbi/shims/packwerk/unresolved_reference.rbi +33 -0
  71. data/sorbet/rbi/shims/parser.rbi +13 -0
  72. metadata +34 -15
  73. data/lib/packwerk/formatters/offenses_formatter.rb +0 -52
  74. data/lib/packwerk/reference_checking/checkers/checker.rb +0 -34
  75. data/lib/packwerk/reference_checking/checkers/privacy_checker.rb +0 -76
  76. data/lib/packwerk/result.rb +0 -9
  77. data/lib/packwerk/sanity_checker.rb +0 -8
  78. data/lib/packwerk/violation_type.rb +0 -11
  79. data/sorbet/rbi/gems/html_tokenizer@0.0.7.rbi +0 -46
  80. data/sorbet/rbi/gems/mini_portile2@2.8.0.rbi +0 -8
data/lib/packwerk/cli.rb CHANGED
@@ -14,8 +14,8 @@ module Packwerk
14
14
  out: T.any(StringIO, IO),
15
15
  err_out: T.any(StringIO, IO),
16
16
  environment: String,
17
- style: Packwerk::OutputStyle,
18
- offenses_formatter: T.nilable(Packwerk::OffensesFormatter)
17
+ style: OutputStyle,
18
+ offenses_formatter: T.nilable(OffensesFormatter)
19
19
  ).void
20
20
  end
21
21
  def initialize(
@@ -33,7 +33,7 @@ module Packwerk
33
33
  @configuration = T.let(configuration || Configuration.from_path, Configuration)
34
34
  @progress_formatter = T.let(Formatters::ProgressFormatter.new(@out, style: style), Formatters::ProgressFormatter)
35
35
  @offenses_formatter = T.let(
36
- offenses_formatter || Formatters::OffensesFormatter.new(style: @style),
36
+ offenses_formatter || @configuration.offenses_formatter,
37
37
  OffensesFormatter
38
38
  )
39
39
  end
@@ -52,22 +52,13 @@ module Packwerk
52
52
  init
53
53
  when "check"
54
54
  output_result(parse_run(args).check)
55
- when "detect-stale-violations"
56
- output_result(parse_run(args).detect_stale_violations)
57
- when "update-deprecations"
58
- warning = <<~WARNING.squish
59
- DEPRECATION WARNING: `update-deprecations` is deprecated in favor of
60
- `update-todo`.
61
- WARNING
62
-
63
- warn(warning)
64
- output_result(parse_run(args).update_todo)
65
- when "update-todo"
66
- output_result(parse_run(args).update_todo)
67
- when "update"
55
+ when "update-todo", "update"
68
56
  output_result(parse_run(args).update_todo)
69
57
  when "validate"
70
58
  validate(args)
59
+ when "version"
60
+ @out.puts(Packwerk::VERSION)
61
+ true
71
62
  when nil, "help"
72
63
  usage
73
64
  else
@@ -89,12 +80,12 @@ module Packwerk
89
80
 
90
81
  sig { returns(T::Boolean) }
91
82
  def generate_configs
92
- configuration_file = Packwerk::Generators::ConfigurationFile.generate(
83
+ configuration_file = Generators::ConfigurationFile.generate(
93
84
  root: @configuration.root_path,
94
85
  out: @out
95
86
  )
96
87
 
97
- root_package = Packwerk::Generators::RootPackage.generate(root: @configuration.root_path, out: @out)
88
+ root_package = Generators::RootPackage.generate(root: @configuration.root_path, out: @out)
98
89
 
99
90
  success = configuration_file && root_package
100
91
 
@@ -124,8 +115,9 @@ module Packwerk
124
115
  Subcommands:
125
116
  init - set up packwerk
126
117
  check - run all checks
127
- update-deprecations - update deprecated_references.yml files
118
+ update-todo - update package_todo.yml files
128
119
  validate - verify integrity of packwerk and package configuration
120
+ version - output packwerk version
129
121
  help - display help information about packwerk
130
122
  USAGE
131
123
  true
@@ -142,72 +134,95 @@ module Packwerk
142
134
  params(
143
135
  relative_file_paths: T::Array[String],
144
136
  ignore_nested_packages: T::Boolean
145
- ).returns(FilesForProcessing::RelativeFileSet)
137
+ ).returns(FilesForProcessing)
146
138
  end
147
139
  def fetch_files_to_process(relative_file_paths, ignore_nested_packages)
148
- relative_file_set = FilesForProcessing.fetch(
140
+ files_for_processing = FilesForProcessing.fetch(
149
141
  relative_file_paths: relative_file_paths,
150
142
  ignore_nested_packages: ignore_nested_packages,
151
143
  configuration: @configuration
152
144
  )
153
- abort("No files found or given. "\
154
- "Specify files or check the include and exclude glob in the config file.") if relative_file_set.empty?
155
- relative_file_set
145
+ @out.puts(<<~MSG.squish) if files_for_processing.files.empty?
146
+ No files found or given.
147
+ Specify files or check the include and exclude glob in the config file.
148
+ MSG
149
+
150
+ files_for_processing
156
151
  end
157
152
 
158
153
  sig { params(_paths: T::Array[String]).returns(T::Boolean) }
159
154
  def validate(_paths)
155
+ result = T.let(nil, T.nilable(Validator::Result))
156
+
160
157
  @progress_formatter.started_validation do
161
- result = checker.check_all
158
+ result = validator.check_all(package_set, @configuration)
162
159
 
163
160
  list_validation_errors(result)
164
-
165
- return result.ok?
166
161
  end
162
+
163
+ T.must(result).ok?
167
164
  end
168
165
 
169
166
  sig { returns(ApplicationValidator) }
170
- def checker
171
- Packwerk::ApplicationValidator.new(
172
- config_file_path: @configuration.config_path,
173
- configuration: @configuration,
174
- environment: @environment,
167
+ def validator
168
+ ApplicationValidator.new
169
+ end
170
+
171
+ sig { returns(PackageSet) }
172
+ def package_set
173
+ PackageSet.load_all_from(
174
+ @configuration.root_path,
175
+ package_pathspec: @configuration.package_paths
175
176
  )
176
177
  end
177
178
 
178
- sig { params(result: ApplicationValidator::Result).void }
179
+ sig { params(result: Validator::Result).void }
179
180
  def list_validation_errors(result)
180
181
  @out.puts
181
182
  if result.ok?
182
183
  @out.puts("Validation successful 🎉")
183
184
  else
184
185
  @out.puts("Validation failed ❗")
186
+ @out.puts
185
187
  @out.puts(result.error_value)
186
188
  end
187
189
  end
188
190
 
189
- sig { params(params: T.untyped).returns(ParseRun) }
190
- def parse_run(params)
191
+ sig { params(args: T::Array[String]).returns(ParseRun) }
192
+ def parse_run(args)
191
193
  relative_file_paths = T.let([], T::Array[String])
192
194
  ignore_nested_packages = nil
195
+ formatter = @offenses_formatter
193
196
 
194
- if params.any? { |p| p.include?("--packages") }
197
+ if args.any? { |arg| arg.include?("--packages") }
195
198
  OptionParser.new do |parser|
196
199
  parser.on("--packages=PACKAGESLIST", Array, "package names, comma separated") do |p|
197
200
  relative_file_paths = p
198
201
  end
199
- end.parse!(params)
202
+ end.parse!(args)
200
203
  ignore_nested_packages = true
201
204
  else
202
- relative_file_paths = params
205
+ relative_file_paths = args
203
206
  ignore_nested_packages = false
204
207
  end
205
208
 
209
+ if args.any? { |arg| arg.include?("--offenses-formatter") }
210
+ OptionParser.new do |parser|
211
+ parser.on("--offenses-formatter=FORMATTER", String,
212
+ "identifier of offenses formatter to use") do |formatter_identifier|
213
+ formatter = OffensesFormatter.find(formatter_identifier)
214
+ end
215
+ end.parse!(args)
216
+ end
217
+
218
+ files_for_processing = fetch_files_to_process(relative_file_paths, ignore_nested_packages)
219
+
206
220
  ParseRun.new(
207
- relative_file_set: fetch_files_to_process(relative_file_paths, ignore_nested_packages),
221
+ relative_file_set: files_for_processing.files,
222
+ file_set_specified: files_for_processing.files_specified?,
208
223
  configuration: @configuration,
209
224
  progress_formatter: @progress_formatter,
210
- offenses_formatter: @offenses_formatter
225
+ offenses_formatter: formatter
211
226
  )
212
227
  end
213
228
  end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "pathname"
@@ -6,7 +6,12 @@ require "yaml"
6
6
 
7
7
  module Packwerk
8
8
  class Configuration
9
+ extend T::Sig
10
+
9
11
  class << self
12
+ extend T::Sig
13
+
14
+ sig { params(path: String).returns(Configuration) }
10
15
  def from_path(path = Dir.pwd)
11
16
  raise ArgumentError, "#{File.expand_path(path)} does not exist" unless File.exist?(path)
12
17
 
@@ -21,6 +26,7 @@ module Packwerk
21
26
 
22
27
  private
23
28
 
29
+ sig { params(path: String).returns(Configuration) }
24
30
  def from_packwerk_config(path)
25
31
  new(
26
32
  YAML.load_file(path) || {},
@@ -30,63 +36,78 @@ module Packwerk
30
36
  end
31
37
 
32
38
  DEFAULT_CONFIG_PATH = "packwerk.yml"
33
- DEFAULT_INCLUDE_GLOBS = ["**/*.{rb,rake,erb}"]
34
- DEFAULT_EXCLUDE_GLOBS = ["{bin,node_modules,script,tmp,vendor}/**/*"]
39
+ DEFAULT_INCLUDE_GLOBS = T.let(["**/*.{rb,rake,erb}"], T::Array[String])
40
+ DEFAULT_EXCLUDE_GLOBS = T.let(["{bin,node_modules,script,tmp,vendor}/**/*"], T::Array[String])
35
41
 
36
- attr_reader(
37
- :include, :exclude, :root_path, :package_paths, :custom_associations, :config_path, :cache_directory
38
- )
42
+ sig { returns(T::Array[String]) }
43
+ attr_reader(:include)
39
44
 
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
45
+ sig { returns(T::Array[String]) }
46
+ attr_reader(:exclude)
51
47
 
52
- if configs["load_paths"]
53
- warning = <<~WARNING
54
- DEPRECATION WARNING: The 'load_paths' key in `packwerk.yml` is deprecated.
55
- This value is no longer cached, and you can remove the key from `packwerk.yml`.
56
- WARNING
48
+ sig { returns(String) }
49
+ attr_reader(:root_path)
57
50
 
58
- warn(warning)
59
- end
51
+ sig { returns(T.any(String, T::Array[String])) }
52
+ attr_reader(:package_paths)
60
53
 
61
- if configs["inflections_file"]
62
- warning = <<~WARNING
63
- DEPRECATION WARNING: The 'inflections_file' key in `packwerk.yml` is deprecated.
64
- This value is no longer cached, and you can remove the key from `packwerk.yml`.
65
- You can also delete #{configs["inflections_file"]}.
66
- WARNING
54
+ sig { returns(T::Array[Symbol]) }
55
+ attr_reader(:custom_associations)
67
56
 
68
- warn(warning)
69
- end
57
+ sig { returns(T.nilable(String)) }
58
+ attr_reader(:config_path)
59
+
60
+ sig { returns(Pathname) }
61
+ attr_reader(:cache_directory)
70
62
 
71
- inflection_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
72
- if Pathname.new(inflection_file).exist?
73
- warning = <<~WARNING
74
- DEPRECATION WARNING: Inflections YMLs in packwerk are now deprecated.
75
- This value is no longer cached, and you can now delete #{inflection_file}
76
- WARNING
63
+ sig do
64
+ params(
65
+ configs: T::Hash[String, T.untyped],
66
+ config_path: T.nilable(String),
67
+ ).void
68
+ end
69
+ def initialize(configs = {}, config_path: nil)
70
+ @include = T.let(configs["include"] || DEFAULT_INCLUDE_GLOBS, T::Array[String])
71
+ @exclude = T.let(configs["exclude"] || DEFAULT_EXCLUDE_GLOBS, T::Array[String])
72
+ root = config_path ? File.dirname(config_path) : "."
73
+ @root_path = T.let(File.expand_path(root), String)
74
+ @package_paths = T.let(configs["package_paths"] || "**/", T.any(String, T::Array[String]))
75
+ @custom_associations = T.let(configs["custom_associations"] || [], T::Array[Symbol])
76
+ @parallel = T.let(configs.key?("parallel") ? configs["parallel"] : true, T::Boolean)
77
+ @cache_enabled = T.let(configs.key?("cache") ? configs["cache"] : false, T::Boolean)
78
+ @cache_directory = T.let(Pathname.new(configs["cache_directory"] || "tmp/cache/packwerk"), Pathname)
79
+ @config_path = config_path
77
80
 
78
- warn(warning)
81
+ @offenses_formatter_identifier = T.let(
82
+ configs["offenses_formatter"] || Formatters::DefaultOffensesFormatter::IDENTIFIER, String
83
+ )
84
+
85
+ if configs.key?("require")
86
+ configs["require"].each do |require_directive|
87
+ ExtensionLoader.load(require_directive, @root_path)
88
+ end
79
89
  end
80
90
  end
81
91
 
92
+ sig { returns(T::Hash[String, Module]) }
82
93
  def load_paths
83
- @load_paths ||= ApplicationLoadPaths.extract_relevant_paths(@root_path, "test")
94
+ @load_paths ||= T.let(
95
+ RailsLoadPaths.for(@root_path, environment: "test"),
96
+ T.nilable(T::Hash[String, Module]),
97
+ )
84
98
  end
85
99
 
100
+ sig { returns(T::Boolean) }
86
101
  def parallel?
87
102
  @parallel
88
103
  end
89
104
 
105
+ sig { returns(OffensesFormatter) }
106
+ def offenses_formatter
107
+ OffensesFormatter.find(@offenses_formatter_identifier)
108
+ end
109
+
110
+ sig { returns(T::Boolean) }
90
111
  def cache_enabled?
91
112
  @cache_enabled
92
113
  end
@@ -52,4 +52,6 @@ module Packwerk
52
52
  "::" + NodeHelpers.parent_module_name(ancestors: ancestors)
53
53
  end
54
54
  end
55
+
56
+ private_constant :ConstNodeInspector
55
57
  end
@@ -0,0 +1,8 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ extend T::Sig
6
+
7
+ ConstantContext = Struct.new(:name, :location, :package)
8
+ end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "constant_resolver"
@@ -17,8 +17,6 @@ module Packwerk
17
17
  class ConstantDiscovery
18
18
  extend T::Sig
19
19
 
20
- ConstantContext = Struct.new(:name, :location, :package, :public?)
21
-
22
20
  # @param constant_resolver [ConstantResolver]
23
21
  # @param packages [Packwerk::PackageSet]
24
22
  sig do
@@ -50,12 +48,12 @@ module Packwerk
50
48
  # @param const_name [String] The unresolved constant's name.
51
49
  # @param current_namespace_path [Array<String>] (optional) The namespace of the context in which the constant is
52
50
  # used, e.g. ["Apps", "Models"] for `Apps::Models`. Defaults to [] which means top level.
53
- # @return [Packwerk::ConstantDiscovery::ConstantContext]
51
+ # @return [ConstantContext]
54
52
  sig do
55
53
  params(
56
54
  const_name: String,
57
55
  current_namespace_path: T.nilable(T::Array[String]),
58
- ).returns(T.nilable(ConstantDiscovery::ConstantContext))
56
+ ).returns(T.nilable(ConstantContext))
59
57
  end
60
58
  def context_for(const_name, current_namespace_path: [])
61
59
  begin
@@ -71,8 +69,9 @@ module Packwerk
71
69
  constant.name,
72
70
  constant.location,
73
71
  package,
74
- package.public_path?(constant.location),
75
72
  )
76
73
  end
77
74
  end
75
+
76
+ private_constant :ConstantDiscovery
78
77
  end
@@ -18,4 +18,6 @@ module Packwerk
18
18
  end
19
19
  def constant_name_from_node(node, ancestors:); end
20
20
  end
21
+
22
+ private_constant :ConstantNameInspector
21
23
  end
@@ -0,0 +1,41 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ begin
7
+ T::Configuration.default_checked_level = :never
8
+
9
+ T.singleton_class.prepend(
10
+ Module.new do
11
+ def cast(value, type, checked: true)
12
+ value
13
+ end
14
+
15
+ def let(value, type, checked: true)
16
+ value
17
+ end
18
+
19
+ def must(arg)
20
+ arg
21
+ end
22
+
23
+ def absurd(value)
24
+ value
25
+ end
26
+
27
+ def bind(value, type, checked: true)
28
+ value
29
+ end
30
+ end
31
+ )
32
+ rescue RuntimeError => error
33
+ # From https://github.com/sorbet/sorbet/blob/dcf1b069cfb0d6624c027e45e59f4c6ca33de970/gems/sorbet-runtime/lib/types/private/runtime_levels.rb#L54
34
+ # Sorbet has already evaluated a method call somewhere, so we can't disable it.
35
+ # In this case, we want to log a warning so Packwerk can still be used (but will be slower).
36
+ if /Set the default checked level earlier./.match?(error.message)
37
+ warn("Packwerk couldn't disable Sorbet. Please ensure it isn't being used before Packwerk is loaded.")
38
+ else
39
+ raise error
40
+ end
41
+ end
@@ -0,0 +1,24 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ # This class handles loading extensions to packwerk using the `require` directive
6
+ # in the `packwerk.yml` configuration.
7
+ module ExtensionLoader
8
+ class << self
9
+ extend T::Sig
10
+ sig { params(require_directive: String, config_dir_path: String).void }
11
+ def load(require_directive, config_dir_path)
12
+ # We want to transform the require directive to behave differently
13
+ # if it's a specific local file being required versus a gem
14
+ if require_directive.start_with?(".")
15
+ require File.join(config_dir_path, require_directive)
16
+ else
17
+ require require_directive
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ private_constant :ExtensionLoader
24
+ end
@@ -64,7 +64,7 @@ module Packwerk
64
64
  references = []
65
65
 
66
66
  node_processor = @node_processor_factory.for(relative_file: relative_file, node: node)
67
- node_visitor = Packwerk::NodeVisitor.new(node_processor: node_processor)
67
+ node_visitor = NodeVisitor.new(node_processor: node_processor)
68
68
  node_visitor.visit(node, ancestors: [], result: references)
69
69
 
70
70
  references
@@ -82,4 +82,6 @@ module Packwerk
82
82
  @parser_factory.for_path(file_path)
83
83
  end
84
84
  end
85
+
86
+ private_constant :FileProcessor
85
87
  end
@@ -15,10 +15,10 @@ module Packwerk
15
15
  relative_file_paths: T::Array[String],
16
16
  configuration: Configuration,
17
17
  ignore_nested_packages: T::Boolean
18
- ).returns(RelativeFileSet)
18
+ ).returns(FilesForProcessing)
19
19
  end
20
20
  def fetch(relative_file_paths:, configuration:, ignore_nested_packages: false)
21
- new(relative_file_paths, configuration, ignore_nested_packages).files
21
+ new(relative_file_paths, configuration, ignore_nested_packages)
22
22
  end
23
23
  end
24
24
 
@@ -33,37 +33,48 @@ module Packwerk
33
33
  @relative_file_paths = relative_file_paths
34
34
  @configuration = configuration
35
35
  @ignore_nested_packages = ignore_nested_packages
36
- @custom_files = T.let(nil, T.nilable(RelativeFileSet))
36
+ @specified_files = T.let(nil, T.nilable(RelativeFileSet))
37
+ @files = T.let(nil, T.nilable(RelativeFileSet))
37
38
  end
38
39
 
39
40
  sig { returns(RelativeFileSet) }
40
41
  def files
41
- include_files = if custom_files.empty?
42
+ @files ||= files_for_processing
43
+ end
44
+
45
+ sig { returns(T::Boolean) }
46
+ def files_specified?
47
+ specified_files.any?
48
+ end
49
+
50
+ private
51
+
52
+ sig { returns(RelativeFileSet) }
53
+ def files_for_processing
54
+ all_included_files = if specified_files.empty?
42
55
  configured_included_files
43
56
  else
44
- configured_included_files & custom_files
57
+ configured_included_files & specified_files
45
58
  end
46
59
 
47
- include_files - configured_excluded_files
60
+ all_included_files - configured_excluded_files
48
61
  end
49
62
 
50
- private
51
-
52
63
  sig { returns(RelativeFileSet) }
53
- def custom_files
54
- @custom_files ||= Set.new(
64
+ def specified_files
65
+ @specified_files ||= Set.new(
55
66
  @relative_file_paths.map do |relative_file_path|
56
67
  if File.file?(relative_file_path)
57
68
  relative_file_path
58
69
  else
59
- custom_included_files(relative_file_path)
70
+ specified_included_files(relative_file_path)
60
71
  end
61
72
  end
62
73
  ).flatten
63
74
  end
64
75
 
65
76
  sig { params(relative_file_path: String).returns(RelativeFileSet) }
66
- def custom_included_files(relative_file_path)
77
+ def specified_included_files(relative_file_path)
67
78
  # Note, assuming include globs are always relative paths
68
79
  relative_includes = @configuration.include
69
80
  relative_files = Dir.glob([File.join(relative_file_path, "**", "*")]).select do |relative_path|
@@ -100,4 +111,6 @@ module Packwerk
100
111
  Set.new(relative_globs.flat_map { |glob| Dir[glob] })
101
112
  end
102
113
  end
114
+
115
+ private_constant :FilesForProcessing
103
116
  end
@@ -0,0 +1,77 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Formatters
6
+ class DefaultOffensesFormatter
7
+ include OffensesFormatter
8
+
9
+ IDENTIFIER = T.let("default", String)
10
+
11
+ extend T::Sig
12
+
13
+ sig { override.params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
14
+ def show_offenses(offenses)
15
+ return "No offenses detected" if offenses.empty?
16
+
17
+ <<~EOS
18
+ #{offenses_list(offenses)}
19
+ #{offenses_summary(offenses)}
20
+ EOS
21
+ end
22
+
23
+ sig { override.params(offense_collection: OffenseCollection, fileset: T::Set[String]).returns(String) }
24
+ def show_stale_violations(offense_collection, fileset)
25
+ if offense_collection.stale_violations?(fileset)
26
+ "There were stale violations found, please run `packwerk update-todo`"
27
+ else
28
+ "No stale violations detected"
29
+ end
30
+ end
31
+
32
+ sig { override.returns(String) }
33
+ def identifier
34
+ IDENTIFIER
35
+ end
36
+
37
+ sig { override.params(strict_mode_violations: T::Array[ReferenceOffense]).returns(String) }
38
+ def show_strict_mode_violations(strict_mode_violations)
39
+ if strict_mode_violations.any?
40
+ strict_mode_violations.compact.map { |offense| format_strict_mode_violation(offense) }.join("\n")
41
+ else
42
+ ""
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ sig { returns(OutputStyle) }
49
+ def style
50
+ @style ||= T.let(Packwerk::OutputStyles::Coloured.new, T.nilable(Packwerk::OutputStyles::Coloured))
51
+ end
52
+
53
+ sig { params(offense: ReferenceOffense).returns(String) }
54
+ def format_strict_mode_violation(offense)
55
+ reference_package = offense.reference.package
56
+ defining_package = offense.reference.constant.package
57
+ "#{reference_package} cannot have #{offense.violation_type} violations on #{defining_package} "\
58
+ "because strict mode is enabled for #{offense.violation_type} violations in "\
59
+ "the enforcing package's package.yml"
60
+ end
61
+
62
+ sig { params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
63
+ def offenses_list(offenses)
64
+ offenses
65
+ .compact
66
+ .map { |offense| offense.to_s(style) }
67
+ .join("\n")
68
+ end
69
+
70
+ sig { params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
71
+ def offenses_summary(offenses)
72
+ offenses_string = "offense".pluralize(offenses.length)
73
+ "#{offenses.length} #{offenses_string} detected"
74
+ end
75
+ end
76
+ end
77
+ end