tapioca 0.5.1 → 0.5.5

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