tapioca 0.5.1 → 0.5.5

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/lib/tapioca/cli.rb +54 -139
  4. data/lib/tapioca/compilers/dsl/active_model_secure_password.rb +101 -0
  5. data/lib/tapioca/compilers/dsl/active_record_enum.rb +1 -1
  6. data/lib/tapioca/compilers/dsl/active_record_fixtures.rb +86 -0
  7. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +41 -33
  8. data/lib/tapioca/compilers/dsl/active_support_concern.rb +0 -2
  9. data/lib/tapioca/compilers/dsl/base.rb +12 -0
  10. data/lib/tapioca/compilers/dsl/identity_cache.rb +0 -1
  11. data/lib/tapioca/compilers/dsl/mixed_in_class_attributes.rb +74 -0
  12. data/lib/tapioca/compilers/dsl/smart_properties.rb +4 -4
  13. data/lib/tapioca/compilers/dsl_compiler.rb +7 -6
  14. data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +198 -0
  15. data/lib/tapioca/compilers/sorbet.rb +0 -1
  16. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +84 -153
  17. data/lib/tapioca/compilers/symbol_table_compiler.rb +5 -11
  18. data/lib/tapioca/config.rb +1 -0
  19. data/lib/tapioca/config_builder.rb +1 -0
  20. data/lib/tapioca/constant_locator.rb +7 -1
  21. data/lib/tapioca/gemfile.rb +11 -5
  22. data/lib/tapioca/generators/base.rb +61 -0
  23. data/lib/tapioca/generators/dsl.rb +362 -0
  24. data/lib/tapioca/generators/gem.rb +345 -0
  25. data/lib/tapioca/generators/init.rb +79 -0
  26. data/lib/tapioca/generators/require.rb +52 -0
  27. data/lib/tapioca/generators/todo.rb +76 -0
  28. data/lib/tapioca/generators.rb +9 -0
  29. data/lib/tapioca/internal.rb +1 -2
  30. data/lib/tapioca/loader.rb +2 -2
  31. data/lib/tapioca/rbi_ext/model.rb +44 -0
  32. data/lib/tapioca/reflection.rb +8 -1
  33. data/lib/tapioca/version.rb +1 -1
  34. data/lib/tapioca.rb +2 -0
  35. metadata +34 -12
  36. data/lib/tapioca/generator.rb +0 -717
@@ -1,717 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require "pathname"
5
- require "thor"
6
-
7
- module Tapioca
8
- class Generator < ::Thor::Shell::Color
9
- extend(T::Sig)
10
-
11
- sig { returns(Config) }
12
- attr_reader :config
13
-
14
- sig do
15
- params(
16
- config: Config
17
- ).void
18
- end
19
- def initialize(config)
20
- @config = config
21
- @bundle = T.let(nil, T.nilable(Gemfile))
22
- @loader = T.let(nil, T.nilable(Loader))
23
- @compiler = T.let(nil, T.nilable(Compilers::SymbolTableCompiler))
24
- @existing_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
25
- @expected_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
26
- super()
27
- end
28
-
29
- sig { params(gem_names: T::Array[String]).void }
30
- def build_gem_rbis(gem_names)
31
- require_gem_file
32
-
33
- gems_to_generate(gem_names)
34
- .reject { |gem| config.exclude.include?(gem.name) }
35
- .each do |gem|
36
- say("Processing '#{gem.name}' gem:", :green)
37
- indent do
38
- compile_gem_rbi(gem)
39
- puts
40
- end
41
- end
42
-
43
- say("All operations performed in working directory.", [:green, :bold])
44
- say("Please review changes and commit them.", [:green, :bold])
45
- end
46
-
47
- sig { void }
48
- def build_requires
49
- requires_path = Config::DEFAULT_POSTREQUIRE
50
- compiler = Compilers::RequiresCompiler.new(Config::SORBET_CONFIG)
51
- name = set_color(requires_path, :yellow, :bold)
52
- say("Compiling #{name}, this may take a few seconds... ")
53
-
54
- rb_string = compiler.compile
55
- if rb_string.empty?
56
- say("Nothing to do", :green)
57
- return
58
- end
59
-
60
- # Clean all existing requires before regenerating the list so we update
61
- # it with the new one found in the client code and remove the old ones.
62
- File.delete(requires_path) if File.exist?(requires_path)
63
-
64
- content = String.new
65
- content << "# typed: true\n"
66
- content << "# frozen_string_literal: true\n\n"
67
- content << rb_string
68
-
69
- outdir = File.dirname(requires_path)
70
- FileUtils.mkdir_p(outdir)
71
- File.write(requires_path, content)
72
-
73
- say("Done", :green)
74
-
75
- say("All requires from this application have been written to #{name}.", [:green, :bold])
76
- cmd = set_color("#{Config::DEFAULT_COMMAND} sync", :yellow, :bold)
77
- say("Please review changes and commit them, then run `#{cmd}`.", [:green, :bold])
78
- end
79
-
80
- sig { void }
81
- def build_todos
82
- todos_path = config.todos_path
83
- compiler = Compilers::TodosCompiler.new
84
- name = set_color(todos_path, :yellow, :bold)
85
- say("Compiling #{name}, this may take a few seconds... ")
86
-
87
- # Clean all existing unresolved constants before regenerating the list
88
- # so Sorbet won't grab them as already resolved.
89
- File.delete(todos_path) if File.exist?(todos_path)
90
-
91
- rbi_string = compiler.compile
92
- if rbi_string.empty?
93
- say("Nothing to do", :green)
94
- return
95
- end
96
-
97
- content = String.new
98
- content << rbi_header(
99
- "#{Config::DEFAULT_COMMAND} todo",
100
- reason: "unresolved constants",
101
- strictness: "false"
102
- )
103
- content << rbi_string
104
- content << "\n"
105
-
106
- outdir = File.dirname(todos_path)
107
- FileUtils.mkdir_p(outdir)
108
- File.write(todos_path, content)
109
-
110
- say("Done", :green)
111
-
112
- say("All unresolved constants have been written to #{name}.", [:green, :bold])
113
- say("Please review changes and commit them.", [:green, :bold])
114
- end
115
-
116
- sig do
117
- params(
118
- requested_constants: T::Array[String],
119
- should_verify: T::Boolean,
120
- quiet: T::Boolean,
121
- verbose: T::Boolean
122
- ).void
123
- end
124
- def build_dsl(requested_constants, should_verify: false, quiet: false, verbose: false)
125
- load_application(eager_load: requested_constants.empty?)
126
- abort_if_pending_migrations!
127
- load_dsl_generators
128
-
129
- if should_verify
130
- say("Checking for out-of-date RBIs...")
131
- else
132
- say("Compiling DSL RBI files...")
133
- end
134
- say("")
135
-
136
- outpath = should_verify ? Pathname.new(Dir.mktmpdir) : config.outpath
137
- rbi_files_to_purge = existing_rbi_filenames(requested_constants)
138
-
139
- compiler = Compilers::DslCompiler.new(
140
- requested_constants: constantize(requested_constants),
141
- requested_generators: constantize_generators(config.generators),
142
- excluded_generators: constantize_generators(config.exclude_generators),
143
- error_handler: ->(error) {
144
- say_error(error, :bold, :red)
145
- }
146
- )
147
-
148
- compiler.run do |constant, contents|
149
- constant_name = T.must(Reflection.name_of(constant))
150
-
151
- if verbose && !quiet
152
- say("Processing: ", [:yellow])
153
- say(constant_name)
154
- end
155
-
156
- filename = compile_dsl_rbi(
157
- constant_name,
158
- contents,
159
- outpath: outpath,
160
- quiet: should_verify || quiet && !verbose
161
- )
162
-
163
- if filename
164
- rbi_files_to_purge.delete(filename)
165
- end
166
- end
167
- say("")
168
-
169
- if should_verify
170
- perform_dsl_verification(outpath)
171
- else
172
- purge_stale_dsl_rbi_files(rbi_files_to_purge)
173
-
174
- say("Done", :green)
175
-
176
- say("All operations performed in working directory.", [:green, :bold])
177
- say("Please review changes and commit them.", [:green, :bold])
178
- end
179
- end
180
-
181
- sig { params(should_verify: T::Boolean).void }
182
- def sync_rbis_with_gemfile(should_verify: false)
183
- if should_verify
184
- say("Checking for out-of-date RBIs...")
185
- say("")
186
- perform_sync_verification
187
- return
188
- end
189
-
190
- anything_done = [
191
- perform_removals,
192
- perform_additions,
193
- ].any?
194
-
195
- if anything_done
196
- say("All operations performed in working directory.", [:green, :bold])
197
- say("Please review changes and commit them.", [:green, :bold])
198
- else
199
- say("No operations performed, all RBIs are up-to-date.", [:green, :bold])
200
- end
201
-
202
- puts
203
- end
204
-
205
- private
206
-
207
- EMPTY_RBI_COMMENT = <<~CONTENT
208
- # THIS IS AN EMPTY RBI FILE.
209
- # see https://github.com/Shopify/tapioca/wiki/Manual-Gem-Requires
210
- CONTENT
211
-
212
- sig { returns(Gemfile) }
213
- def bundle
214
- @bundle ||= Gemfile.new
215
- end
216
-
217
- sig { returns(Loader) }
218
- def loader
219
- @loader ||= Loader.new
220
- end
221
-
222
- sig { returns(Compilers::SymbolTableCompiler) }
223
- def compiler
224
- @compiler ||= Compilers::SymbolTableCompiler.new
225
- end
226
-
227
- sig { void }
228
- def require_gem_file
229
- say("Requiring all gems to prepare for compiling... ")
230
- begin
231
- loader.load_bundle(bundle, config.prerequire, config.postrequire)
232
- rescue LoadError => e
233
- explain_failed_require(config.postrequire, e)
234
- exit(1)
235
- end
236
- say(" Done", :green)
237
- unless bundle.missing_specs.empty?
238
- say(" completed with missing specs: ")
239
- say(bundle.missing_specs.join(", "), :yellow)
240
- end
241
- puts
242
- end
243
-
244
- sig { params(file: String, error: LoadError).void }
245
- def explain_failed_require(file, error)
246
- say_error("\n\nLoadError: #{error}", :bold, :red)
247
- say_error("\nTapioca could not load all the gems required by your application.", :yellow)
248
- say_error("If you populated ", :yellow)
249
- say_error("#{file} ", :bold, :blue)
250
- say_error("with ", :yellow)
251
- say_error("`#{Config::DEFAULT_COMMAND} require`", :bold, :blue)
252
- say_error("you should probably review it and remove the faulty line.", :yellow)
253
- end
254
-
255
- sig do
256
- params(
257
- message: String,
258
- color: T.any(Symbol, T::Array[Symbol]),
259
- ).void
260
- end
261
- def say_error(message = "", *color)
262
- force_new_line = (message.to_s !~ /( |\t)\Z/)
263
- buffer = prepare_message(*T.unsafe([message, *T.unsafe(color)]))
264
- buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
265
-
266
- stderr.print(buffer)
267
- stderr.flush
268
- end
269
-
270
- sig { params(eager_load: T::Boolean).void }
271
- def load_application(eager_load:)
272
- say("Loading Rails application... ")
273
-
274
- loader.load_rails_application(
275
- environment_load: true,
276
- eager_load: eager_load
277
- )
278
-
279
- say("Done", :green)
280
- end
281
-
282
- sig { void }
283
- def load_dsl_generators
284
- say("Loading DSL generator classes... ")
285
-
286
- Dir.glob([
287
- "#{__dir__}/compilers/dsl/*.rb",
288
- "#{Config::TAPIOCA_PATH}/generators/**/*.rb",
289
- ]).each do |generator|
290
- require File.expand_path(generator)
291
- end
292
-
293
- say("Done", :green)
294
- end
295
-
296
- sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
297
- def constantize(constant_names)
298
- constant_map = constant_names.map do |name|
299
- [name, Object.const_get(name)]
300
- rescue NameError
301
- [name, nil]
302
- end.to_h
303
-
304
- unprocessable_constants = constant_map.select { |_, v| v.nil? }
305
- unless unprocessable_constants.empty?
306
- unprocessable_constants.each do |name, _|
307
- say("Error: Cannot find constant '#{name}'", :red)
308
- remove(dsl_rbi_filename(name))
309
- end
310
-
311
- exit(1)
312
- end
313
-
314
- constant_map.values
315
- end
316
-
317
- sig { params(generator_names: T::Array[String]).returns(T::Array[T.class_of(Compilers::Dsl::Base)]) }
318
- def constantize_generators(generator_names)
319
- generator_map = generator_names.map do |name|
320
- # Try to find built-in tapioca generator first, then globally defined generator. The
321
- # explicit `break` ensures the class is returned, not the `potential_name`.
322
- generator_klass = ["Tapioca::Compilers::Dsl::#{name}", name].find do |potential_name|
323
- break Object.const_get(potential_name)
324
- rescue NameError
325
- # Skip if we can't find generator by the potential name
326
- end
327
-
328
- [name, generator_klass]
329
- end.to_h
330
-
331
- unprocessable_generators = generator_map.select { |_, v| v.nil? }
332
- unless unprocessable_generators.empty?
333
- unprocessable_generators.each do |name, _|
334
- say("Error: Cannot find generator '#{name}'", :red)
335
- end
336
-
337
- exit(1)
338
- end
339
-
340
- generator_map.values
341
- end
342
-
343
- sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) }
344
- def existing_rbi_filenames(requested_constants, path: config.outpath)
345
- filenames = if requested_constants.empty?
346
- Pathname.glob(path / "**/*.rbi")
347
- else
348
- requested_constants.map do |constant_name|
349
- dsl_rbi_filename(constant_name)
350
- end
351
- end
352
-
353
- filenames.to_set
354
- end
355
-
356
- sig { returns(T::Hash[String, String]) }
357
- def existing_rbis
358
- @existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s)
359
- .map { |f| T.cast(f.basename(".*").to_s.split("@", 2), [String, String]) }
360
- .to_h
361
- end
362
-
363
- sig { returns(T::Hash[String, String]) }
364
- def expected_rbis
365
- @expected_rbis ||= bundle.dependencies
366
- .reject { |gem| config.exclude.include?(gem.name) }
367
- .map { |gem| [gem.name, gem.version.to_s] }
368
- .to_h
369
- end
370
-
371
- sig { params(constant_name: String).returns(Pathname) }
372
- def dsl_rbi_filename(constant_name)
373
- config.outpath / "#{underscore(constant_name)}.rbi"
374
- end
375
-
376
- sig { params(gem_name: String, version: String).returns(Pathname) }
377
- def gem_rbi_filename(gem_name, version)
378
- config.outpath / "#{gem_name}@#{version}.rbi"
379
- end
380
-
381
- sig { params(gem_name: String).returns(Pathname) }
382
- def existing_rbi(gem_name)
383
- gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
384
- end
385
-
386
- sig { params(gem_name: String).returns(Pathname) }
387
- def expected_rbi(gem_name)
388
- gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
389
- end
390
-
391
- sig { params(gem_name: String).returns(T::Boolean) }
392
- def gem_rbi_exists?(gem_name)
393
- existing_rbis.key?(gem_name)
394
- end
395
-
396
- sig { returns(T::Array[String]) }
397
- def removed_rbis
398
- (existing_rbis.keys - expected_rbis.keys).sort
399
- end
400
-
401
- sig { returns(T::Array[String]) }
402
- def added_rbis
403
- expected_rbis.select do |name, value|
404
- existing_rbis[name] != value
405
- end.keys.sort
406
- end
407
-
408
- sig { params(filename: Pathname).void }
409
- def add(filename)
410
- say("++ Adding: #{filename}")
411
- end
412
-
413
- sig { params(filename: Pathname).void }
414
- def remove(filename)
415
- return unless filename.exist?
416
- say("-- Removing: #{filename}")
417
- filename.unlink
418
- end
419
-
420
- sig { params(old_filename: Pathname, new_filename: Pathname).void }
421
- def move(old_filename, new_filename)
422
- say("-> Moving: #{old_filename} to #{new_filename}")
423
- old_filename.rename(new_filename.to_s)
424
- end
425
-
426
- sig { void }
427
- def perform_removals
428
- say("Removing RBI files of gems that have been removed:", [:blue, :bold])
429
- puts
430
-
431
- anything_done = T.let(false, T::Boolean)
432
-
433
- gems = removed_rbis
434
-
435
- indent do
436
- if gems.empty?
437
- say("Nothing to do.")
438
- else
439
- gems.each do |removed|
440
- filename = existing_rbi(removed)
441
- remove(filename)
442
- end
443
-
444
- anything_done = true
445
- end
446
- end
447
-
448
- puts
449
-
450
- anything_done
451
- end
452
-
453
- sig { void }
454
- def perform_additions
455
- say("Generating RBI files of gems that are added or updated:", [:blue, :bold])
456
- puts
457
-
458
- anything_done = T.let(false, T::Boolean)
459
-
460
- gems = added_rbis
461
-
462
- indent do
463
- if gems.empty?
464
- say("Nothing to do.")
465
- else
466
- require_gem_file
467
-
468
- gems.each do |gem_name|
469
- filename = expected_rbi(gem_name)
470
-
471
- if gem_rbi_exists?(gem_name)
472
- old_filename = existing_rbi(gem_name)
473
- move(old_filename, filename) unless old_filename == filename
474
- end
475
-
476
- gem = T.must(bundle.gem(gem_name))
477
- compile_gem_rbi(gem)
478
- add(filename)
479
-
480
- puts
481
- end
482
- end
483
-
484
- anything_done = true
485
- end
486
-
487
- puts
488
-
489
- anything_done
490
- end
491
-
492
- sig do
493
- params(gem_names: T::Array[String])
494
- .returns(T::Array[Gemfile::GemSpec])
495
- end
496
- def gems_to_generate(gem_names)
497
- return bundle.dependencies if gem_names.empty?
498
-
499
- gem_names.map do |gem_name|
500
- gem = bundle.gem(gem_name)
501
- if gem.nil?
502
- say("Error: Cannot find gem '#{gem_name}'", :red)
503
- exit(1)
504
- end
505
- gem
506
- end
507
- end
508
-
509
- sig { params(command: String, reason: T.nilable(String), strictness: T.nilable(String)).returns(String) }
510
- def rbi_header(command, reason: nil, strictness: nil)
511
- statement = <<~HEAD
512
- # DO NOT EDIT MANUALLY
513
- # This is an autogenerated file for #{reason}.
514
- # Please instead update this file by running `#{command}`.
515
- HEAD
516
-
517
- sigil = <<~SIGIL if strictness
518
- # typed: #{strictness}
519
- SIGIL
520
-
521
- if config.file_header
522
- [statement, sigil].compact.join("\n").strip.concat("\n\n")
523
- elsif sigil
524
- sigil.strip.concat("\n\n")
525
- else
526
- ""
527
- end
528
- end
529
-
530
- sig { params(gem: Gemfile::GemSpec).void }
531
- def compile_gem_rbi(gem)
532
- compiler = Compilers::SymbolTableCompiler.new
533
- gem_name = set_color(gem.name, :yellow, :bold)
534
- say("Compiling #{gem_name}, this may take a few seconds... ")
535
-
536
- strictness = config.typed_overrides[gem.name] || "true"
537
- rbi_body_content = compiler.compile(gem)
538
- content = String.new
539
- content << rbi_header(
540
- "#{Config::DEFAULT_COMMAND} sync",
541
- reason: "types exported from the `#{gem.name}` gem",
542
- strictness: strictness
543
- )
544
-
545
- FileUtils.mkdir_p(config.outdir)
546
- filename = config.outpath / gem.rbi_file_name
547
-
548
- if rbi_body_content.strip.empty?
549
- content << EMPTY_RBI_COMMENT
550
- say("Done (empty output)", :yellow)
551
- else
552
- content << rbi_body_content
553
- say("Done", :green)
554
- end
555
- File.write(filename.to_s, content)
556
-
557
- T.unsafe(Pathname).glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
558
- remove(file) unless file.basename.to_s == gem.rbi_file_name
559
- end
560
- end
561
-
562
- sig do
563
- params(constant_name: String, contents: String, outpath: Pathname, quiet: T::Boolean)
564
- .returns(T.nilable(Pathname))
565
- end
566
- def compile_dsl_rbi(constant_name, contents, outpath: config.outpath, quiet: false)
567
- return if contents.nil?
568
-
569
- rbi_name = underscore(constant_name) + ".rbi"
570
- filename = outpath / rbi_name
571
-
572
- out = String.new
573
- out << rbi_header(
574
- "#{Config::DEFAULT_COMMAND} dsl #{constant_name}",
575
- reason: "dynamic methods in `#{constant_name}`"
576
- )
577
- out << contents
578
-
579
- FileUtils.mkdir_p(File.dirname(filename))
580
- File.write(filename, out)
581
-
582
- unless quiet
583
- say("Wrote: ", [:green])
584
- say(filename)
585
- end
586
-
587
- filename
588
- end
589
-
590
- sig { params(tmp_dir: Pathname).returns(T::Hash[String, Symbol]) }
591
- def verify_dsl_rbi(tmp_dir:)
592
- diff = {}
593
-
594
- existing_rbis = rbi_files_in(config.outpath)
595
- new_rbis = rbi_files_in(tmp_dir)
596
-
597
- added_files = (new_rbis - existing_rbis)
598
-
599
- added_files.each do |file|
600
- diff[file] = :added
601
- end
602
-
603
- removed_files = (existing_rbis - new_rbis)
604
-
605
- removed_files.each do |file|
606
- diff[file] = :removed
607
- end
608
-
609
- common_files = (existing_rbis & new_rbis)
610
-
611
- changed_files = common_files.map do |filename|
612
- filename unless FileUtils.identical?(config.outpath / filename, tmp_dir / filename)
613
- end.compact
614
-
615
- changed_files.each do |file|
616
- diff[file] = :changed
617
- end
618
-
619
- diff
620
- end
621
-
622
- sig { params(cause: Symbol, files: T::Array[String]).returns(String) }
623
- def build_error_for_files(cause, files)
624
- filenames = files.map do |file|
625
- config.outpath / file
626
- end.join("\n - ")
627
-
628
- " File(s) #{cause}:\n - #{filenames}"
629
- end
630
-
631
- sig { params(path: Pathname).returns(T::Array[Pathname]) }
632
- def rbi_files_in(path)
633
- Pathname.glob(path / "**/*.rbi").map do |file|
634
- file.relative_path_from(path)
635
- end.sort
636
- end
637
-
638
- sig { params(dir: Pathname).void }
639
- def perform_dsl_verification(dir)
640
- diff = verify_dsl_rbi(tmp_dir: dir)
641
-
642
- report_diff_and_exit_if_out_of_date(diff, "dsl")
643
- ensure
644
- FileUtils.remove_entry(dir)
645
- end
646
-
647
- sig { params(files: T::Set[Pathname]).void }
648
- def purge_stale_dsl_rbi_files(files)
649
- if files.any?
650
- say("Removing stale RBI files...")
651
-
652
- files.sort.each do |filename|
653
- remove(filename)
654
- end
655
- say("")
656
- end
657
- end
658
-
659
- sig { void }
660
- def perform_sync_verification
661
- diff = {}
662
-
663
- removed_rbis.each do |gem_name|
664
- filename = existing_rbi(gem_name)
665
- diff[filename] = :removed
666
- end
667
-
668
- added_rbis.each do |gem_name|
669
- filename = expected_rbi(gem_name)
670
- diff[filename] = gem_rbi_exists?(gem_name) ? :changed : :added
671
- end
672
-
673
- report_diff_and_exit_if_out_of_date(diff, "sync")
674
- end
675
-
676
- sig { params(diff: T::Hash[String, Symbol], command: String).void }
677
- def report_diff_and_exit_if_out_of_date(diff, command)
678
- if diff.empty?
679
- say("Nothing to do, all RBIs are up-to-date.")
680
- else
681
- say("RBI files are out-of-date. In your development environment, please run:", :green)
682
- say(" `#{Config::DEFAULT_COMMAND} #{command}`", [:green, :bold])
683
- say("Once it is complete, be sure to commit and push any changes", :green)
684
-
685
- say("")
686
-
687
- say("Reason:", [:red])
688
- diff.group_by(&:last).sort.each do |cause, diff_for_cause|
689
- say(build_error_for_files(cause, diff_for_cause.map(&:first)))
690
- end
691
-
692
- exit(1)
693
- end
694
- end
695
-
696
- sig { void }
697
- def abort_if_pending_migrations!
698
- return unless File.exist?("config/application.rb")
699
- return unless defined?(::Rake)
700
-
701
- Rails.application.load_tasks
702
- Rake::Task["db:abort_if_pending_migrations"].invoke if Rake::Task.task_defined?("db:abort_if_pending_migrations")
703
- end
704
-
705
- sig { params(class_name: String).returns(String) }
706
- def underscore(class_name)
707
- return class_name unless /[A-Z-]|::/.match?(class_name)
708
-
709
- word = class_name.to_s.gsub("::", "/")
710
- word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
711
- word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
712
- word.tr!("-", "_")
713
- word.downcase!
714
- word
715
- end
716
- end
717
- end