use_packs 0.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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +71 -0
  3. data/bin/packs +10 -0
  4. data/bin/rubocop +29 -0
  5. data/bin/tapioca +29 -0
  6. data/lib/use_packs/cli.rb +127 -0
  7. data/lib/use_packs/code_ownership_post_processor.rb +58 -0
  8. data/lib/use_packs/configuration.rb +61 -0
  9. data/lib/use_packs/default_user_event_logger.rb +7 -0
  10. data/lib/use_packs/logging.rb +37 -0
  11. data/lib/use_packs/per_file_processor_interface.rb +18 -0
  12. data/lib/use_packs/private/file_move_operation.rb +80 -0
  13. data/lib/use_packs/private/interactive_cli/pack_selector.rb +34 -0
  14. data/lib/use_packs/private/interactive_cli/team_selector.rb +35 -0
  15. data/lib/use_packs/private/interactive_cli/use_cases/add_dependency.rb +30 -0
  16. data/lib/use_packs/private/interactive_cli/use_cases/check.rb +25 -0
  17. data/lib/use_packs/private/interactive_cli/use_cases/create.rb +27 -0
  18. data/lib/use_packs/private/interactive_cli/use_cases/get_info.rb +74 -0
  19. data/lib/use_packs/private/interactive_cli/use_cases/interface.rb +34 -0
  20. data/lib/use_packs/private/interactive_cli/use_cases/lint_package_yml.rb +26 -0
  21. data/lib/use_packs/private/interactive_cli/use_cases/make_public.rb +34 -0
  22. data/lib/use_packs/private/interactive_cli/use_cases/move.rb +36 -0
  23. data/lib/use_packs/private/interactive_cli/use_cases/nest.rb +31 -0
  24. data/lib/use_packs/private/interactive_cli/use_cases/query.rb +51 -0
  25. data/lib/use_packs/private/interactive_cli/use_cases/regenerate_rubocop_todo.rb +26 -0
  26. data/lib/use_packs/private/interactive_cli/use_cases/rename.rb +34 -0
  27. data/lib/use_packs/private/interactive_cli/use_cases/update_deprecations.rb +25 -0
  28. data/lib/use_packs/private/interactive_cli/use_cases/validate.rb +25 -0
  29. data/lib/use_packs/private/interactive_cli/use_cases/visualize.rb +44 -0
  30. data/lib/use_packs/private/interactive_cli.rb +52 -0
  31. data/lib/use_packs/private/pack_relationship_analyzer.rb +135 -0
  32. data/lib/use_packs/private/packwerk_wrapper/offenses_aggregator_formatter.rb +34 -0
  33. data/lib/use_packs/private/packwerk_wrapper.rb +71 -0
  34. data/lib/use_packs/private.rb +453 -0
  35. data/lib/use_packs/rubocop_post_processor.rb +67 -0
  36. data/lib/use_packs/spring_command.rb +28 -0
  37. data/lib/use_packs/user_event_logger.rb +259 -0
  38. data/lib/use_packs.rb +298 -0
  39. metadata +351 -0
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+
3
+ module UsePacks
4
+ module Private
5
+ module PackwerkWrapper
6
+ #
7
+ # This formatter simply collects offenses so we can feed them into other systems
8
+ #
9
+ class OffensesAggregatorFormatter
10
+ extend T::Sig
11
+ include Packwerk::OffensesFormatter
12
+
13
+ sig { returns(T::Array[Packwerk::ReferenceOffense]) }
14
+ attr_reader :aggregated_offenses
15
+
16
+ sig { void }
17
+ def initialize
18
+ @aggregated_offenses = T.let([], T::Array[Packwerk::ReferenceOffense])
19
+ end
20
+
21
+ sig { override.params(offenses: T::Array[T.nilable(Packwerk::Offense)]).returns(String) }
22
+ def show_offenses(offenses)
23
+ @aggregated_offenses = T.unsafe(offenses)
24
+ ''
25
+ end
26
+
27
+ sig { override.params(offense_collection: Packwerk::OffenseCollection, for_files: T::Set[String]).returns(String) }
28
+ def show_stale_violations(offense_collection, for_files)
29
+ ''
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,71 @@
1
+ # typed: strict
2
+
3
+ require 'packwerk'
4
+ require 'use_packs/private/packwerk_wrapper/offenses_aggregator_formatter'
5
+
6
+ module UsePacks
7
+ module Private
8
+ module PackwerkWrapper
9
+ extend T::Sig
10
+
11
+ sig { params(argv: T.untyped, formatter: Packwerk::OffensesFormatter).void }
12
+ def self.packwerk_cli_run_safely(argv, formatter)
13
+ with_safe_exit_if_no_files_found do
14
+ packwerk_cli(formatter).run(argv)
15
+ end
16
+ end
17
+
18
+ #
19
+ # execute_command is like `run` except it does not `exit`
20
+ #
21
+ sig { params(argv: T.untyped, formatter: T.nilable(Packwerk::OffensesFormatter)).void }
22
+ def self.packwerk_cli_execute_safely(argv, formatter = nil)
23
+ with_safe_exit_if_no_files_found do
24
+ packwerk_cli(formatter).execute_command(argv)
25
+ end
26
+ end
27
+
28
+ sig { params(block: T.proc.returns(T.untyped)).void }
29
+ def self.with_safe_exit_if_no_files_found(&block)
30
+ block.call
31
+ rescue SystemExit => e
32
+ # Packwerk should probably exit positively here rather than raising an error -- there should be no
33
+ # errors if the user has excluded all files being checked.
34
+ unless e.message == 'No files found or given. Specify files or check the include and exclude glob in the config file.'
35
+ raise
36
+ end
37
+ end
38
+
39
+ sig { params(formatter: T.nilable(Packwerk::OffensesFormatter)).returns(Packwerk::Cli) }
40
+ def self.packwerk_cli(formatter)
41
+ # This is mostly copied from exe/packwerk within the packwerk gem, but we use our own formatters
42
+ # Note that packwerk does not allow you to pass in your own progress formatter currently
43
+ ENV['RAILS_ENV'] = 'test'
44
+
45
+ style = Packwerk::OutputStyles::Coloured.new
46
+ Packwerk::Cli.new(style: style, offenses_formatter: formatter)
47
+ end
48
+
49
+ sig { params(files: T::Array[String]).returns(T::Array[Packwerk::ReferenceOffense]) }
50
+ def self.get_offenses_for_files(files)
51
+ formatter = OffensesAggregatorFormatter.new
52
+ packwerk_cli_execute_safely(['check', *files], formatter)
53
+ formatter.aggregated_offenses.compact
54
+ end
55
+
56
+ sig { params(files: T::Array[String]).returns(T::Array[Packwerk::ReferenceOffense]) }
57
+ def self.get_offenses_for_files_by_package(files)
58
+ packages = package_names_for_files(files)
59
+ argv = ['check', '--packages', packages.join(',')]
60
+ formatter = OffensesAggregatorFormatter.new
61
+ packwerk_cli_execute_safely(argv, formatter)
62
+ formatter.aggregated_offenses.compact
63
+ end
64
+
65
+ sig { params(files: T::Array[String]).returns(T::Array[String]) }
66
+ def self.package_names_for_files(files)
67
+ files.map { |f| ParsePackwerk.package_from_path(f).name }.compact.uniq
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,453 @@
1
+ # typed: strict
2
+
3
+ require 'pathname'
4
+ require 'fileutils'
5
+ require 'colorized_string'
6
+ require 'sorbet-runtime'
7
+
8
+ require 'use_packs/private/file_move_operation'
9
+ require 'use_packs/private/pack_relationship_analyzer'
10
+ require 'use_packs/private/interactive_cli'
11
+ require 'use_packs/private/packwerk_wrapper'
12
+
13
+ module UsePacks
14
+ module Private
15
+ extend T::Sig
16
+
17
+ sig { params(pack_name: String).returns(String) }
18
+ def self.clean_pack_name(pack_name)
19
+ # The reason we do this is a lot of terminals add an extra `/` when you tab-autocomplete.
20
+ # This results in the pack not being found, but when we write the package YML it writes to the same place,
21
+ # causing a behaviorally confusing diff.
22
+ # We ignore trailing slashes as an ergonomic feature to the user.
23
+ pack_name.gsub(%r{/$}, '')
24
+ end
25
+
26
+ sig do
27
+ params(
28
+ file: String,
29
+ find: Pathname,
30
+ replace_with: Pathname
31
+ ).void
32
+ end
33
+ def self.replace_in_file(file:, find:, replace_with:)
34
+ file = Pathname.new(file)
35
+ return if !file.exist?
36
+
37
+ count = 0
38
+ file.write(file.read.gsub(find.to_s) do
39
+ count += 1
40
+ replace_with.to_s
41
+ end)
42
+ Logging.print "Replaced #{count} occurrence(s) of #{find} in #{file}" if count > 0
43
+ end
44
+
45
+ sig do
46
+ params(
47
+ pack_name: String,
48
+ enforce_privacy: T::Boolean,
49
+ enforce_dependencies: T.nilable(T::Boolean),
50
+ team: T.nilable(CodeTeams::Team)
51
+ ).void
52
+ end
53
+ def self.create_pack!(pack_name:, enforce_privacy:, enforce_dependencies:, team:)
54
+ Logging.section('👋 Hi!') do
55
+ intro = UsePacks.config.user_event_logger.before_create_pack(pack_name)
56
+ Logging.print_bold_green(intro)
57
+ end
58
+
59
+ pack_name = Private.clean_pack_name(pack_name)
60
+
61
+ package = create_pack_if_not_exists!(pack_name: pack_name, enforce_privacy: enforce_privacy, enforce_dependencies: enforce_dependencies, team: team)
62
+ add_public_directory(package)
63
+ add_readme_todo(package)
64
+
65
+ Logging.section('Next steps') do
66
+ next_steps = UsePacks.config.user_event_logger.after_create_pack(pack_name)
67
+
68
+ Logging.print_bold_green(next_steps)
69
+ end
70
+ end
71
+
72
+ sig do
73
+ params(
74
+ pack_name: String,
75
+ paths_relative_to_root: T::Array[String],
76
+ per_file_processors: T::Array[UsePacks::PerFileProcessorInterface]
77
+ ).void
78
+ end
79
+ def self.move_to_pack!(pack_name:, paths_relative_to_root:, per_file_processors: [])
80
+ pack_name = Private.clean_pack_name(pack_name)
81
+ package = ParsePackwerk.all.find { |p| p.name == pack_name }
82
+ if package.nil?
83
+ raise StandardError, "Can not find package with name #{pack_name}. Make sure the argument is of the form `packs/my_pack/`"
84
+ end
85
+
86
+ add_public_directory(package)
87
+ add_readme_todo(package)
88
+ package_location = package.directory
89
+
90
+ file_move_operations = T.let([], T::Array[Private::FileMoveOperation])
91
+
92
+ if paths_relative_to_root.any?
93
+ Logging.section('File Operations') do
94
+ file_paths = paths_relative_to_root.flat_map do |path|
95
+ origin_pathname = Pathname.new(path).cleanpath
96
+ # Note -- we used to `mv` over whole directories, rather than splatting out their contents and merging individual files.
97
+ # The main advantage to moving whole directories is that it's a bit faster and a bit less verbose
98
+ # However, this ended up being tricky and caused complexity to flow down later parts of the implementation.
99
+ # Notably:
100
+ # 1) The `mv` operation doesn't merge directories, so if the destination already has the same directory, then the mv operation
101
+ # will overwrite
102
+ # 2) We could get around this possibly with `cp_r` (https://ruby-doc.org/stdlib-1.9.3/libdoc/fileutils/rdoc/FileUtils.html#method-c-cp_r),
103
+ # but we'd also have to delete the origin destination. On top of this, we still need to splat things out later on so that we can do
104
+ # per file processor operations, and that has some complexity of its own. The simplest thing here would be to simply glob everything out.
105
+ #
106
+ # For now, we sacrifice some small level of speed and conciseness in favor of simpler implementation.
107
+ # Later, if we choose to go back to moving whole directories at a time, it should be a refactor and all tests should still pass
108
+ #
109
+ if origin_pathname.directory?
110
+ origin_pathname.glob('**/*.*').reject do |origin_path|
111
+ origin_path.to_s.include?(ParsePackwerk::PACKAGE_YML_NAME) ||
112
+ origin_path.to_s.include?(ParsePackwerk::DEPRECATED_REFERENCES_YML_NAME)
113
+ end
114
+ else
115
+ origin_pathname
116
+ end
117
+ end
118
+ file_move_operations = file_paths.flat_map do |origin_pathname|
119
+ file_move_operation = FileMoveOperation.new(
120
+ origin_pathname: origin_pathname,
121
+ destination_pathname: FileMoveOperation.destination_pathname_for_package_move(origin_pathname, package_location),
122
+ destination_pack: package
123
+ )
124
+ [
125
+ file_move_operation,
126
+ file_move_operation.spec_file_move_operation
127
+ ]
128
+ end
129
+ file_move_operations.each do |file_move_operation|
130
+ Private.package_filepath(file_move_operation, per_file_processors)
131
+ end
132
+ end
133
+ end
134
+
135
+ per_file_processors.each do |processor|
136
+ processor.after_move_files!(file_move_operations)
137
+ end
138
+ end
139
+
140
+ sig do
141
+ params(
142
+ pack_name: String,
143
+ parent_name: String,
144
+ per_file_processors: T::Array[PerFileProcessorInterface]
145
+ ).void
146
+ end
147
+ def self.move_to_parent!(
148
+ pack_name:,
149
+ parent_name:,
150
+ per_file_processors: []
151
+ )
152
+ pack_name = Private.clean_pack_name(pack_name)
153
+ package = ParsePackwerk.all.find { |p| p.name == pack_name }
154
+ if package.nil?
155
+ raise StandardError, "Can not find package with name #{pack_name}. Make sure the argument is of the form `packs/my_pack/`"
156
+ end
157
+
158
+ parent_name = Private.clean_pack_name(parent_name)
159
+ parent_package = ParsePackwerk.all.find { |p| p.name == parent_name }
160
+ if parent_package.nil?
161
+ parent_package = create_pack_if_not_exists!(pack_name: parent_name, enforce_privacy: true, enforce_dependencies: true)
162
+ end
163
+
164
+ # First we create a new pack that has the exact same properties of the old one!
165
+ package_last_name = package.directory.basename
166
+ new_package_name = parent_package.directory.join(package_last_name).to_s
167
+
168
+ new_package = ParsePackwerk::Package.new(
169
+ name: new_package_name,
170
+ enforce_privacy: package.enforce_dependencies,
171
+ enforce_dependencies: package.enforce_dependencies,
172
+ dependencies: package.dependencies,
173
+ metadata: package.metadata
174
+ )
175
+ ParsePackwerk.write_package_yml!(new_package)
176
+ ParsePackwerk.bust_cache!
177
+
178
+ # Move everything from the old pack to the new one
179
+ move_to_pack!(
180
+ pack_name: new_package_name,
181
+ paths_relative_to_root: [package.directory.to_s],
182
+ per_file_processors: per_file_processors
183
+ )
184
+
185
+ # Then delete the old package.yml and deprecated_references.yml files
186
+ package.yml.delete
187
+ deprecated_references_file = ParsePackwerk::DeprecatedReferences.for(package).pathname
188
+ deprecated_references_file.delete if deprecated_references_file.exist?
189
+
190
+ ParsePackwerk.bust_cache!
191
+
192
+ ParsePackwerk.all.each do |other_package|
193
+ new_dependencies = other_package.dependencies.map { |d| d == pack_name ? new_package_name : d }
194
+ if other_package.name == parent_name && !new_dependencies.include?(new_package_name)
195
+ new_dependencies += [new_package_name]
196
+ end
197
+
198
+ new_other_package = ParsePackwerk::Package.new(
199
+ name: other_package.name,
200
+ enforce_privacy: other_package.enforce_privacy,
201
+ enforce_dependencies: other_package.enforce_dependencies,
202
+ dependencies: new_dependencies.uniq.sort,
203
+ metadata: other_package.metadata
204
+ )
205
+
206
+ ParsePackwerk.write_package_yml!(new_other_package)
207
+ end
208
+
209
+ sorbet_config = Pathname.new('sorbet/config')
210
+ if sorbet_config.exist?
211
+ UsePacks.replace_in_file(
212
+ file: sorbet_config.to_s,
213
+ find: package.directory.join('spec'),
214
+ replace_with: new_package.directory.join('spec')
215
+ )
216
+ end
217
+ end
218
+
219
+ sig do
220
+ params(
221
+ paths_relative_to_root: T::Array[String],
222
+ per_file_processors: T::Array[UsePacks::PerFileProcessorInterface]
223
+ ).void
224
+ end
225
+ def self.make_public!(paths_relative_to_root:, per_file_processors:)
226
+ if paths_relative_to_root.any?
227
+ file_move_operations = T.let([], T::Array[Private::FileMoveOperation])
228
+
229
+ Logging.section('File Operations') do
230
+ file_paths = paths_relative_to_root.flat_map do |path|
231
+ origin_pathname = Pathname.new(path).cleanpath
232
+ if origin_pathname.directory?
233
+ origin_pathname.glob('**/*.*').map(&:to_s)
234
+ else
235
+ path
236
+ end
237
+ end
238
+
239
+ file_move_operations = file_paths.flat_map do |path|
240
+ package = ParsePackwerk.package_from_path(path)
241
+ origin_pathname = Pathname.new(path).cleanpath
242
+
243
+ file_move_operation = FileMoveOperation.new(
244
+ origin_pathname: origin_pathname,
245
+ destination_pathname: FileMoveOperation.destination_pathname_for_new_public_api(origin_pathname),
246
+ destination_pack: package
247
+ )
248
+
249
+ [
250
+ file_move_operation,
251
+ file_move_operation.spec_file_move_operation
252
+ ]
253
+ end
254
+
255
+ file_move_operations.each do |file_move_operation|
256
+ Private.package_filepath(file_move_operation, per_file_processors)
257
+ end
258
+ end
259
+
260
+ per_file_processors.each do |processor|
261
+ processor.after_move_files!(file_move_operations)
262
+ end
263
+ end
264
+ end
265
+
266
+ sig do
267
+ params(
268
+ pack_name: String,
269
+ dependency_name: String
270
+ ).void
271
+ end
272
+ def self.add_dependency!(pack_name:, dependency_name:)
273
+ all_packages = ParsePackwerk.all
274
+
275
+ pack_name = Private.clean_pack_name(pack_name)
276
+ package = all_packages.find { |p| p.name == pack_name }
277
+ if package.nil?
278
+ raise StandardError, "Can not find package with name #{pack_name}. Make sure the argument is of the form `packs/my_pack/`"
279
+ end
280
+
281
+ dependency_name = Private.clean_pack_name(dependency_name)
282
+ package_dependency = all_packages.find { |p| p.name == dependency_name }
283
+ if package_dependency.nil?
284
+ raise StandardError, "Can not find package with name #{dependency_name}. Make sure the argument is of the form `packs/my_pack/`"
285
+ end
286
+
287
+ new_package = ParsePackwerk::Package.new(
288
+ name: pack_name,
289
+ dependencies: (package.dependencies + [dependency_name]).uniq.sort,
290
+ enforce_privacy: package.enforce_privacy,
291
+ enforce_dependencies: package.enforce_dependencies,
292
+ metadata: package.metadata
293
+ )
294
+ ParsePackwerk.write_package_yml!(new_package)
295
+ end
296
+
297
+ sig { params(file_move_operation: FileMoveOperation, per_file_processors: T::Array[UsePacks::PerFileProcessorInterface]).void }
298
+ def self.package_filepath(file_move_operation, per_file_processors)
299
+ per_file_processors.each do |per_file_processor|
300
+ if file_move_operation.origin_pathname.exist?
301
+ per_file_processor.before_move_file!(file_move_operation)
302
+ end
303
+ end
304
+
305
+ origin = file_move_operation.origin_pathname
306
+ destination = file_move_operation.destination_pathname
307
+ idempotent_mv(origin, destination)
308
+ end
309
+
310
+ sig { params(origin: Pathname, destination: Pathname).void }
311
+ def self.idempotent_mv(origin, destination)
312
+ if origin.exist? && destination.exist?
313
+ Logging.print ColorizedString.new("[SKIP] Not moving #{origin}, #{destination} already exists").red
314
+ elsif origin.exist? && !destination.exist?
315
+ destination.dirname.mkpath
316
+
317
+ Logging.print "Moving file #{origin} to #{destination}"
318
+ # use git mv so that git knows that it was a move
319
+ FileUtils.mv(origin, destination)
320
+ elsif !origin.exist? && destination.exist?
321
+ Logging.print ColorizedString.new("[SKIP] Not moving #{origin}, does not exist, (#{destination} already exists)").red
322
+ else
323
+ Logging.print ColorizedString.new("[SKIP] Not moving #{origin}, does not exist").red
324
+ end
325
+ end
326
+
327
+ sig { params(package: ParsePackwerk::Package).void }
328
+ def self.add_public_directory(package)
329
+ public_directory = package.directory.join('app/public')
330
+
331
+ if public_directory.glob('**/**.rb').none?
332
+ FileUtils.mkdir_p(public_directory)
333
+ todo_md = UsePacks.config.user_event_logger.on_create_public_directory_todo(package.name)
334
+ public_directory.join('TODO.md').write(todo_md)
335
+ end
336
+ end
337
+
338
+ sig { params(package: ParsePackwerk::Package).void }
339
+ def self.add_readme_todo(package)
340
+ pack_directory = package.directory
341
+
342
+ if !pack_directory.join('README.md').exist?
343
+ readme_todo_md = UsePacks.config.user_event_logger.on_create_readme_todo(package.name)
344
+ pack_directory.join('README_TODO.md').write(readme_todo_md)
345
+ end
346
+ end
347
+
348
+ sig do
349
+ params(
350
+ pack_name: String,
351
+ enforce_privacy: T::Boolean,
352
+ enforce_dependencies: T.nilable(T::Boolean),
353
+ team: T.nilable(CodeTeams::Team)
354
+ ).returns(ParsePackwerk::Package)
355
+ end
356
+ def self.create_pack_if_not_exists!(pack_name:, enforce_privacy:, enforce_dependencies:, team: nil)
357
+ if PERMITTED_PACK_LOCATIONS.none? { |permitted_location| pack_name.start_with?(permitted_location) }
358
+ raise StandardError, "UsePacks only supports packages in the the following directories: #{PERMITTED_PACK_LOCATIONS.inspect}. Please make sure to pass in the name of the pack including the full directory path, e.g. `packs/my_pack`."
359
+ end
360
+
361
+ existing_package = ParsePackwerk.all.find { |p| p.name == pack_name }
362
+ if existing_package.nil?
363
+ should_enforce_dependenceies = enforce_dependencies.nil? ? UsePacks.config.enforce_dependencies : enforce_dependencies
364
+
365
+ package = ParsePackwerk::Package.new(
366
+ enforce_dependencies: should_enforce_dependenceies,
367
+ enforce_privacy: enforce_privacy,
368
+ dependencies: [],
369
+ metadata: {
370
+ 'owner' => team.nil? ? 'MyTeam' : team.name
371
+ },
372
+ name: pack_name
373
+ )
374
+
375
+ ParsePackwerk.write_package_yml!(package)
376
+ RuboCop::Packs.set_default_rubocop_yml(packs: [package])
377
+
378
+ current_contents = package.yml.read
379
+ new_contents = current_contents.gsub('MyTeam', 'MyTeam # specify your team here, or delete this key if this package is not owned by one team')
380
+ package.yml.write(new_contents)
381
+ existing_package = package
382
+ end
383
+
384
+ existing_package
385
+ end
386
+
387
+ sig { void }
388
+ def self.load_client_configuration
389
+ @loaded_client_configuration ||= T.let(false, T.nilable(T::Boolean))
390
+ return if @loaded_client_configuration
391
+
392
+ @loaded_client_configuration = true
393
+ client_configuration = Pathname.pwd.join('config/use_packs.rb')
394
+ require client_configuration.to_s if client_configuration.exist?
395
+ end
396
+
397
+ sig { void }
398
+ def self.bust_cache!
399
+ UsePacks.config.bust_cache!
400
+ # This comes explicitly after `UsePacks.config.bust_cache!` because
401
+ # otherwise `UsePacks.config` will attempt to reload the client configuratoin.
402
+ @loaded_client_configuration = false
403
+ end
404
+
405
+ sig { returns(T::Hash[String, String]) }
406
+ def self.get_deprecated_references_contents
407
+ deprecated_references = {}
408
+ ParsePackwerk.all.each do |package|
409
+ deprecated_references_yml = ParsePackwerk::DeprecatedReferences.for(package).pathname
410
+ if deprecated_references_yml.exist?
411
+ deprecated_references[deprecated_references_yml.to_s] = deprecated_references_yml.read
412
+ end
413
+ end
414
+
415
+ deprecated_references
416
+ end
417
+
418
+ DeprecatedReferencesFiles = T.type_alias do
419
+ T::Hash[String, T.nilable(String)]
420
+ end
421
+
422
+ sig { params(before: DeprecatedReferencesFiles, after: DeprecatedReferencesFiles).returns(String) }
423
+ def self.diff_deprecated_references_yml(before, after)
424
+ dir_containing_contents_before = Dir.mktmpdir
425
+ dir_containing_contents_after = Dir.mktmpdir
426
+ begin
427
+ write_deprecated_references_to_tmp_folder(before, dir_containing_contents_before)
428
+ write_deprecated_references_to_tmp_folder(after, dir_containing_contents_after)
429
+
430
+ diff = `diff -r #{dir_containing_contents_before}/ #{dir_containing_contents_after}/`
431
+ # For ease of reading, sub out the tmp directory from the diff
432
+ diff.gsub(dir_containing_contents_before, '').gsub(dir_containing_contents_after, '')
433
+ ensure
434
+ FileUtils.remove_entry dir_containing_contents_before
435
+ FileUtils.remove_entry dir_containing_contents_after
436
+ end
437
+ end
438
+
439
+ sig { params(deprecated_references_files: DeprecatedReferencesFiles, tmp_folder: String).void }
440
+ def self.write_deprecated_references_to_tmp_folder(deprecated_references_files, tmp_folder)
441
+ deprecated_references_files.each do |filename, contents|
442
+ next if contents.nil?
443
+
444
+ tmp_folder_pathname = Pathname.new(tmp_folder)
445
+ temp_deprecated_references_yml = tmp_folder_pathname.join(filename)
446
+ FileUtils.mkdir_p(temp_deprecated_references_yml.dirname)
447
+ temp_deprecated_references_yml.write(contents)
448
+ end
449
+ end
450
+ end
451
+
452
+ private_constant :Private
453
+ end
@@ -0,0 +1,67 @@
1
+ # typed: strict
2
+
3
+ module UsePacks
4
+ class RubocopPostProcessor
5
+ include PerFileProcessorInterface
6
+ extend T::Sig
7
+
8
+ sig { override.params(file_move_operation: Private::FileMoveOperation).void }
9
+ def before_move_file!(file_move_operation)
10
+ relative_path_to_origin = file_move_operation.origin_pathname
11
+ relative_path_to_destination = file_move_operation.destination_pathname
12
+
13
+ rubocop_todo = Pathname.new('.rubocop_todo.yml')
14
+ if rubocop_todo.exist?
15
+ UsePacks.replace_in_file(
16
+ file: rubocop_todo.to_s,
17
+ find: relative_path_to_origin,
18
+ replace_with: relative_path_to_destination
19
+ )
20
+ end
21
+
22
+ if file_move_operation.origin_pack.name != ParsePackwerk::ROOT_PACKAGE_NAME && file_move_operation.destination_pack.name != ParsePackwerk::ROOT_PACKAGE_NAME
23
+ origin_rubocop_todo = file_move_operation.origin_pack.directory.join(RuboCop::Packs::PACK_LEVEL_RUBOCOP_TODO_YML)
24
+ # If there were TODOs for this file in the origin pack's pack-based rubocop, we want to move it to the destination
25
+ if origin_rubocop_todo.exist?
26
+ loaded_origin_rubocop_todo = YAML.load_file(origin_rubocop_todo)
27
+ new_origin_rubocop_todo = loaded_origin_rubocop_todo.dup
28
+
29
+ loaded_origin_rubocop_todo.each do |cop_name, cop_config|
30
+ next unless cop_config['Exclude'].include?(relative_path_to_origin.to_s)
31
+
32
+ new_origin_rubocop_todo[cop_name]['Exclude'] = cop_config['Exclude'] - [relative_path_to_origin.to_s]
33
+ origin_rubocop_todo.write(YAML.dump(new_origin_rubocop_todo))
34
+
35
+ destination_rubocop_todo = file_move_operation.destination_pack.directory.join(RuboCop::Packs::PACK_LEVEL_RUBOCOP_TODO_YML)
36
+ if destination_rubocop_todo.exist?
37
+ new_destination_rubocop_todo = YAML.load_file(destination_rubocop_todo).dup
38
+ else
39
+ new_destination_rubocop_todo = {}
40
+ end
41
+
42
+ new_destination_rubocop_todo[cop_name] ||= { 'Exclude' => [] }
43
+
44
+ new_destination_rubocop_todo[cop_name]['Exclude'] += [relative_path_to_destination.to_s]
45
+ destination_rubocop_todo.write(YAML.dump(new_destination_rubocop_todo))
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ sig { params(file_move_operations: T::Array[Private::FileMoveOperation]).void }
52
+ def after_move_files!(file_move_operations)
53
+ # There could also be no TODOs for this file, but moving it produced TODOs. This could happen if:
54
+ # 1) The origin pack did not enforce a rubocop, such as typed public APIs
55
+ # 2) The file satisfied the cop in the origin pack, such as the Packs/RootNamespaceIsPackName, but the desired
56
+ # namespace changed once the file was moved to a different pack.
57
+ files = []
58
+ file_move_operations.each do |file_move_operation|
59
+ if file_move_operation.destination_pathname.exist?
60
+ files << file_move_operation.destination_pathname.to_s
61
+ end
62
+ end
63
+
64
+ RuboCop::Packs.regenerate_todo(files: files)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,28 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'spring/commands'
5
+
6
+ module UsePacks
7
+ class SpringCommand
8
+ def env(*)
9
+ # Packwerk needs to run in a test environment, which has a set of autoload paths that are
10
+ # often a superset of the dev/prod paths (for example, test/support/helpers)
11
+ 'test'
12
+ end
13
+
14
+ def exec_name
15
+ 'packs'
16
+ end
17
+
18
+ def gem_name
19
+ 'use_packs'
20
+ end
21
+
22
+ def call
23
+ load(Gem.bin_path(gem_name, exec_name))
24
+ end
25
+ end
26
+
27
+ Spring.register_command('packs', SpringCommand.new)
28
+ end