use_packs 0.0.1

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