tapioca 0.4.25 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +14 -14
  3. data/README.md +2 -2
  4. data/Rakefile +5 -7
  5. data/exe/tapioca +2 -2
  6. data/lib/tapioca/cli.rb +256 -2
  7. data/lib/tapioca/compilers/dsl/aasm.rb +122 -0
  8. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +52 -12
  9. data/lib/tapioca/compilers/dsl/action_mailer.rb +6 -9
  10. data/lib/tapioca/compilers/dsl/active_job.rb +8 -12
  11. data/lib/tapioca/compilers/dsl/active_model_attributes.rb +131 -0
  12. data/lib/tapioca/compilers/dsl/active_record_associations.rb +33 -54
  13. data/lib/tapioca/compilers/dsl/active_record_columns.rb +10 -105
  14. data/lib/tapioca/compilers/dsl/active_record_enum.rb +8 -10
  15. data/lib/tapioca/compilers/dsl/active_record_scope.rb +7 -10
  16. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +5 -8
  17. data/lib/tapioca/compilers/dsl/active_resource.rb +9 -37
  18. data/lib/tapioca/compilers/dsl/active_storage.rb +98 -0
  19. data/lib/tapioca/compilers/dsl/active_support_concern.rb +108 -0
  20. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +13 -8
  21. data/lib/tapioca/compilers/dsl/base.rb +96 -82
  22. data/lib/tapioca/compilers/dsl/config.rb +111 -0
  23. data/lib/tapioca/compilers/dsl/frozen_record.rb +5 -7
  24. data/lib/tapioca/compilers/dsl/identity_cache.rb +66 -29
  25. data/lib/tapioca/compilers/dsl/protobuf.rb +19 -69
  26. data/lib/tapioca/compilers/dsl/sidekiq_worker.rb +25 -12
  27. data/lib/tapioca/compilers/dsl/smart_properties.rb +21 -33
  28. data/lib/tapioca/compilers/dsl/state_machines.rb +56 -78
  29. data/lib/tapioca/compilers/dsl/url_helpers.rb +7 -10
  30. data/lib/tapioca/compilers/dsl_compiler.rb +22 -38
  31. data/lib/tapioca/compilers/requires_compiler.rb +2 -2
  32. data/lib/tapioca/compilers/sorbet.rb +26 -5
  33. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +139 -154
  34. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +4 -4
  35. data/lib/tapioca/compilers/symbol_table_compiler.rb +1 -1
  36. data/lib/tapioca/compilers/todos_compiler.rb +1 -1
  37. data/lib/tapioca/config.rb +2 -0
  38. data/lib/tapioca/config_builder.rb +4 -2
  39. data/lib/tapioca/constant_locator.rb +6 -8
  40. data/lib/tapioca/gemfile.rb +10 -12
  41. data/lib/tapioca/generator.rb +129 -45
  42. data/lib/tapioca/generic_type_registry.rb +25 -98
  43. data/lib/tapioca/helpers/active_record_column_type_helper.rb +98 -0
  44. data/lib/tapioca/internal.rb +1 -10
  45. data/lib/tapioca/loader.rb +14 -48
  46. data/lib/tapioca/rbi_ext/model.rb +122 -0
  47. data/lib/tapioca/reflection.rb +131 -0
  48. data/lib/tapioca/sorbet_ext/fixed_hash_patch.rb +1 -1
  49. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +72 -4
  50. data/lib/tapioca/sorbet_ext/name_patch.rb +1 -1
  51. data/lib/tapioca/version.rb +1 -1
  52. data/lib/tapioca.rb +3 -0
  53. metadata +34 -22
  54. data/lib/tapioca/cli/main.rb +0 -146
  55. data/lib/tapioca/core_ext/class.rb +0 -28
  56. data/lib/tapioca/core_ext/string.rb +0 -18
  57. data/lib/tapioca/rbi/model.rb +0 -405
  58. data/lib/tapioca/rbi/printer.rb +0 -410
  59. data/lib/tapioca/rbi/rewriters/group_nodes.rb +0 -106
  60. data/lib/tapioca/rbi/rewriters/nest_non_public_methods.rb +0 -65
  61. data/lib/tapioca/rbi/rewriters/nest_singleton_methods.rb +0 -42
  62. data/lib/tapioca/rbi/rewriters/sort_nodes.rb +0 -86
  63. data/lib/tapioca/rbi/visitor.rb +0 -21
@@ -1,8 +1,8 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'json'
5
- require 'tempfile'
4
+ require "json"
5
+ require "tempfile"
6
6
 
7
7
  module Tapioca
8
8
  module Compilers
@@ -25,7 +25,7 @@ module Tapioca
25
25
 
26
26
  sig { params(paths: T::Array[String]).returns(T::Set[String]) }
27
27
  def load_symbols(paths)
28
- output = T.cast(Tempfile.create('sorbet') do |file|
28
+ output = T.cast(Tempfile.create("sorbet") do |file|
29
29
  file.write(Array(paths).join("\n"))
30
30
  file.flush
31
31
 
@@ -69,7 +69,7 @@ module Tapioca
69
69
  # TODO: CLASS is removed since v0.4.4730 of Sorbet
70
70
  # but keeping here for backward compatibility. Remove
71
71
  # once the minimum version is moved past that.
72
- next unless %w[CLASS CLASS_OR_MODULE STATIC_FIELD].include?(kind)
72
+ next unless ["CLASS", "CLASS_OR_MODULE", "STATIC_FIELD"].include?(kind)
73
73
  next if name =~ /[<>()$]/
74
74
  next if name =~ /^[0-9]+$/
75
75
  next if name == "T::Helpers"
@@ -8,7 +8,7 @@ module Tapioca
8
8
 
9
9
  sig do
10
10
  params(
11
- gem: Gemfile::Gem,
11
+ gem: Gemfile::GemSpec,
12
12
  indent: Integer
13
13
  ).returns(String)
14
14
  end
@@ -13,7 +13,7 @@ module Tapioca
13
13
  def compile
14
14
  list_todos.each_line.map do |line|
15
15
  next if line.include?("<") || line.include?("class_of")
16
- "module #{line.strip.gsub('T.untyped::', '')}; end"
16
+ "module #{line.strip.gsub("T.untyped::", "")}; end"
17
17
  end.compact.join("\n")
18
18
  end
19
19
 
@@ -9,9 +9,11 @@ module Tapioca
9
9
  const(:prerequire, T.nilable(String))
10
10
  const(:postrequire, String)
11
11
  const(:exclude, T::Array[String])
12
+ const(:exclude_generators, T::Array[String])
12
13
  const(:typed_overrides, T::Hash[String, String])
13
14
  const(:todos_path, String)
14
15
  const(:generators, T::Array[String])
16
+ const(:file_header, T::Boolean, default: true)
15
17
 
16
18
  sig { returns(Pathname) }
17
19
  def outpath
@@ -1,7 +1,7 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'yaml'
4
+ require "yaml"
5
5
 
6
6
  module Tapioca
7
7
  class ConfigBuilder
@@ -33,7 +33,7 @@ module Tapioca
33
33
  sig { params(command: Symbol).returns(T::Hash[String, T.untyped]) }
34
34
  def default_options(command)
35
35
  default_outdir = case command
36
- when :sync, :generate
36
+ when :sync, :generate, :gem
37
37
  Config::DEFAULT_GEMDIR
38
38
  when :dsl
39
39
  Config::DEFAULT_DSLDIR
@@ -62,9 +62,11 @@ module Tapioca
62
62
  "postrequire" => Config::DEFAULT_POSTREQUIRE,
63
63
  "outdir" => nil,
64
64
  "exclude" => [],
65
+ "exclude_generators" => [],
65
66
  "typed_overrides" => Config::DEFAULT_OVERRIDES,
66
67
  "todos_path" => Config::DEFAULT_TODOSPATH,
67
68
  "generators" => [],
69
+ "file_header" => true,
68
70
  }.freeze, T::Hash[String, T.untyped])
69
71
  end
70
72
  end
@@ -1,7 +1,7 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'set'
4
+ require "set"
5
5
 
6
6
  module Tapioca
7
7
  # Registers a TracePoint immediately upon load to track points at which
@@ -9,15 +9,14 @@ module Tapioca
9
9
  # correspondence between classes/modules and files, as this information isn't
10
10
  # available in the ruby runtime without extra accounting.
11
11
  module ConstantLocator
12
- @class_files = {}
12
+ extend Reflection
13
13
 
14
- NAME = Module.instance_method(:name)
15
- private_constant :NAME
14
+ @class_files = {}
16
15
 
17
16
  # Immediately activated upon load. Observes class/module definition.
18
17
  TracePoint.trace(:class) do |tp|
19
18
  unless tp.self.singleton_class?
20
- key = NAME.bind(tp.self).call
19
+ key = name_of(tp.self)
21
20
  @class_files[key] ||= Set.new
22
21
  @class_files[key] << tp.path
23
22
  end
@@ -26,11 +25,10 @@ module Tapioca
26
25
  # Returns the files in which this class or module was opened. Doesn't know
27
26
  # about situations where the class was opened prior to +require+ing,
28
27
  # or where metaprogramming was used via +eval+, etc.
29
- def files_for(klass)
30
- name = String === klass ? klass : NAME.bind(klass).call
28
+ def self.files_for(klass)
29
+ name = String === klass ? klass : name_of(klass)
31
30
  files = @class_files[name]
32
31
  files || Set.new
33
32
  end
34
- module_function :files_for
35
33
  end
36
34
  end
@@ -20,7 +20,7 @@ module Tapioca
20
20
  sig { returns(Bundler::Definition) }
21
21
  attr_reader(:definition)
22
22
 
23
- sig { returns(T::Array[Gem]) }
23
+ sig { returns(T::Array[GemSpec]) }
24
24
  attr_reader(:dependencies)
25
25
 
26
26
  sig { returns(T::Array[String]) }
@@ -32,18 +32,18 @@ module Tapioca
32
32
  @lockfile = T.let(File.new(Bundler.default_lockfile), File)
33
33
  @definition = T.let(Bundler::Dsl.evaluate(gemfile, lockfile, {}), Bundler::Definition)
34
34
  dependencies, missing_specs = load_dependencies
35
- @dependencies = T.let(dependencies, T::Array[Gem])
35
+ @dependencies = T.let(dependencies, T::Array[GemSpec])
36
36
  @missing_specs = T.let(missing_specs, T::Array[String])
37
37
  end
38
38
 
39
- sig { params(gem_name: String).returns(T.nilable(Gem)) }
39
+ sig { params(gem_name: String).returns(T.nilable(GemSpec)) }
40
40
  def gem(gem_name)
41
41
  dependencies.detect { |dep| dep.name == gem_name }
42
42
  end
43
43
 
44
44
  sig { void }
45
- def require
46
- T.unsafe(runtime).setup(*groups).require(*groups)
45
+ def require_bundle
46
+ T.unsafe(runtime).require(*groups)
47
47
  end
48
48
 
49
49
  private
@@ -51,18 +51,18 @@ module Tapioca
51
51
  sig { returns(File) }
52
52
  attr_reader(:gemfile, :lockfile)
53
53
 
54
- sig { returns([T::Array[Gem], T::Array[String]]) }
54
+ sig { returns([T::Array[GemSpec], T::Array[String]]) }
55
55
  def load_dependencies
56
56
  materialized_dependencies, missing_specs = materialize_deps
57
57
  dependencies = materialized_dependencies
58
- .map { |spec| Gem.new(spec) }
58
+ .map { |spec| GemSpec.new(spec) }
59
59
  .reject { |gem| gem.ignore?(dir) }
60
60
  .uniq(&:rbi_file_name)
61
61
  .sort_by(&:rbi_file_name)
62
62
  [dependencies, missing_specs]
63
63
  end
64
64
 
65
- sig { returns([T::Array[::Gem::Specification], T::Array[String]]) }
65
+ sig { returns([T::Enumerable[Spec], T::Array[String]]) }
66
66
  def materialize_deps
67
67
  deps = definition.locked_gems.dependencies.values
68
68
  missing_specs = T::Array[String].new
@@ -92,12 +92,10 @@ module Tapioca
92
92
  File.expand_path(gemfile.path + "/..")
93
93
  end
94
94
 
95
- class Gem
95
+ class GemSpec
96
96
  extend(T::Sig)
97
97
 
98
- IGNORED_GEMS = T.let(%w{
99
- sorbet sorbet-static sorbet-runtime
100
- }.freeze, T::Array[String])
98
+ IGNORED_GEMS = T.let(["sorbet", "sorbet-static", "sorbet-runtime"].freeze, T::Array[String])
101
99
 
102
100
  sig { returns(String) }
103
101
  attr_reader :full_gem_path, :version
@@ -1,9 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'pathname'
5
- require 'thor'
6
- require "tapioca/core_ext/string"
4
+ require "pathname"
5
+ require "thor"
7
6
 
8
7
  module Tapioca
9
8
  class Generator < ::Thor::Shell::Color
@@ -63,11 +62,8 @@ module Tapioca
63
62
  File.delete(requires_path) if File.exist?(requires_path)
64
63
 
65
64
  content = String.new
66
- content << rbi_header(
67
- "#{Config::DEFAULT_COMMAND} require",
68
- reason: "explicit gem requires",
69
- strictness: "false"
70
- )
65
+ content << "# typed: true\n"
66
+ content << "# frozen_string_literal: true\n\n"
71
67
  content << rb_string
72
68
 
73
69
  outdir = File.dirname(requires_path)
@@ -77,7 +73,7 @@ module Tapioca
77
73
  say("Done", :green)
78
74
 
79
75
  say("All requires from this application have been written to #{name}.", [:green, :bold])
80
- cmd = set_color("#{Config::DEFAULT_COMMAND} sync", :yellow, :bold)
76
+ cmd = set_color("#{Config::DEFAULT_COMMAND} gem", :yellow, :bold)
81
77
  say("Please review changes and commit them, then run `#{cmd}`.", [:green, :bold])
82
78
  end
83
79
 
@@ -121,11 +117,13 @@ module Tapioca
121
117
  params(
122
118
  requested_constants: T::Array[String],
123
119
  should_verify: T::Boolean,
124
- quiet: T::Boolean
120
+ quiet: T::Boolean,
121
+ verbose: T::Boolean
125
122
  ).void
126
123
  end
127
- def build_dsl(requested_constants, should_verify: false, quiet: false)
124
+ def build_dsl(requested_constants, should_verify: false, quiet: false, verbose: false)
128
125
  load_application(eager_load: requested_constants.empty?)
126
+ abort_if_pending_migrations!
129
127
  load_dsl_generators
130
128
 
131
129
  if should_verify
@@ -140,20 +138,26 @@ module Tapioca
140
138
 
141
139
  compiler = Compilers::DslCompiler.new(
142
140
  requested_constants: constantize(requested_constants),
143
- requested_generators: config.generators,
141
+ requested_generators: constantize_generators(config.generators),
142
+ excluded_generators: constantize_generators(config.exclude_generators),
144
143
  error_handler: ->(error) {
145
144
  say_error(error, :bold, :red)
146
145
  }
147
146
  )
148
147
 
149
148
  compiler.run do |constant, contents|
150
- constant_name = Module.instance_method(:name).bind(constant).call
149
+ constant_name = T.must(Reflection.name_of(constant))
150
+
151
+ if verbose && !quiet
152
+ say("Processing: ", [:yellow])
153
+ say(constant_name)
154
+ end
151
155
 
152
156
  filename = compile_dsl_rbi(
153
157
  constant_name,
154
158
  contents,
155
159
  outpath: outpath,
156
- quiet: should_verify || quiet
160
+ quiet: should_verify || quiet && !verbose
157
161
  )
158
162
 
159
163
  if filename
@@ -174,8 +178,15 @@ module Tapioca
174
178
  end
175
179
  end
176
180
 
177
- sig { void }
178
- def sync_rbis_with_gemfile
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
+
179
190
  anything_done = [
180
191
  perform_removals,
181
192
  perform_additions,
@@ -195,7 +206,7 @@ module Tapioca
195
206
 
196
207
  EMPTY_RBI_COMMENT = <<~CONTENT
197
208
  # THIS IS AN EMPTY RBI FILE.
198
- # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
209
+ # see https://github.com/Shopify/tapioca/wiki/Manual-Gem-Requires
199
210
  CONTENT
200
211
 
201
212
  sig { returns(Gemfile) }
@@ -205,7 +216,7 @@ module Tapioca
205
216
 
206
217
  sig { returns(Loader) }
207
218
  def loader
208
- @loader ||= Loader.new(bundle)
219
+ @loader ||= Loader.new
209
220
  end
210
221
 
211
222
  sig { returns(Compilers::SymbolTableCompiler) }
@@ -217,7 +228,7 @@ module Tapioca
217
228
  def require_gem_file
218
229
  say("Requiring all gems to prepare for compiling... ")
219
230
  begin
220
- loader.load_bundle(config.prerequire, config.postrequire)
231
+ loader.load_bundle(bundle, config.prerequire, config.postrequire)
221
232
  rescue LoadError => e
222
233
  explain_failed_require(config.postrequire, e)
223
234
  exit(1)
@@ -225,7 +236,7 @@ module Tapioca
225
236
  say(" Done", :green)
226
237
  unless bundle.missing_specs.empty?
227
238
  say(" completed with missing specs: ")
228
- say(bundle.missing_specs.join(', '), :yellow)
239
+ say(bundle.missing_specs.join(", "), :yellow)
229
240
  end
230
241
  puts
231
242
  end
@@ -260,7 +271,7 @@ module Tapioca
260
271
  def load_application(eager_load:)
261
272
  say("Loading Rails application... ")
262
273
 
263
- loader.load_rails(
274
+ loader.load_rails_application(
264
275
  environment_load: true,
265
276
  eager_load: eager_load
266
277
  )
@@ -285,11 +296,9 @@ module Tapioca
285
296
  sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
286
297
  def constantize(constant_names)
287
298
  constant_map = constant_names.map do |name|
288
- begin
289
- [name, Object.const_get(name)]
290
- rescue NameError
291
- [name, nil]
292
- end
299
+ [name, Object.const_get(name)]
300
+ rescue NameError
301
+ [name, nil]
293
302
  end.to_h
294
303
 
295
304
  unprocessable_constants = constant_map.select { |_, v| v.nil? }
@@ -305,6 +314,32 @@ module Tapioca
305
314
  constant_map.values
306
315
  end
307
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
+
308
343
  sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) }
309
344
  def existing_rbi_filenames(requested_constants, path: config.outpath)
310
345
  filenames = if requested_constants.empty?
@@ -321,7 +356,7 @@ module Tapioca
321
356
  sig { returns(T::Hash[String, String]) }
322
357
  def existing_rbis
323
358
  @existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s)
324
- .map { |f| T.cast(f.basename(".*").to_s.split('@', 2), [String, String]) }
359
+ .map { |f| T.cast(f.basename(".*").to_s.split("@", 2), [String, String]) }
325
360
  .to_h
326
361
  end
327
362
 
@@ -335,7 +370,7 @@ module Tapioca
335
370
 
336
371
  sig { params(constant_name: String).returns(Pathname) }
337
372
  def dsl_rbi_filename(constant_name)
338
- config.outpath / "#{constant_name.underscore}.rbi"
373
+ config.outpath / "#{underscore(constant_name)}.rbi"
339
374
  end
340
375
 
341
376
  sig { params(gem_name: String, version: String).returns(Pathname) }
@@ -456,7 +491,7 @@ module Tapioca
456
491
 
457
492
  sig do
458
493
  params(gem_names: T::Array[String])
459
- .returns(T::Array[Gemfile::Gem])
494
+ .returns(T::Array[Gemfile::GemSpec])
460
495
  end
461
496
  def gems_to_generate(gem_names)
462
497
  return bundle.dependencies if gem_names.empty?
@@ -483,10 +518,16 @@ module Tapioca
483
518
  # typed: #{strictness}
484
519
  SIGIL
485
520
 
486
- [statement, sigil].compact.join("\n").strip.concat("\n\n")
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
487
528
  end
488
529
 
489
- sig { params(gem: Gemfile::Gem).void }
530
+ sig { params(gem: Gemfile::GemSpec).void }
490
531
  def compile_gem_rbi(gem)
491
532
  compiler = Compilers::SymbolTableCompiler.new
492
533
  gem_name = set_color(gem.name, :yellow, :bold)
@@ -496,7 +537,7 @@ module Tapioca
496
537
  rbi_body_content = compiler.compile(gem)
497
538
  content = String.new
498
539
  content << rbi_header(
499
- "#{Config::DEFAULT_COMMAND} sync",
540
+ "#{Config::DEFAULT_COMMAND} gem #{gem.name}",
500
541
  reason: "types exported from the `#{gem.name}` gem",
501
542
  strictness: strictness
502
543
  )
@@ -525,7 +566,7 @@ module Tapioca
525
566
  def compile_dsl_rbi(constant_name, contents, outpath: config.outpath, quiet: false)
526
567
  return if contents.nil?
527
568
 
528
- rbi_name = constant_name.underscore + ".rbi"
569
+ rbi_name = underscore(constant_name) + ".rbi"
529
570
  filename = outpath / rbi_name
530
571
 
531
572
  out = String.new
@@ -598,11 +639,47 @@ module Tapioca
598
639
  def perform_dsl_verification(dir)
599
640
  diff = verify_dsl_rbi(tmp_dir: dir)
600
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, "gem")
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)
601
678
  if diff.empty?
602
679
  say("Nothing to do, all RBIs are up-to-date.")
603
680
  else
604
681
  say("RBI files are out-of-date. In your development environment, please run:", :green)
605
- say(" `#{Config::DEFAULT_COMMAND} dsl`", [:green, :bold])
682
+ say(" `#{Config::DEFAULT_COMMAND} #{command}`", [:green, :bold])
606
683
  say("Once it is complete, be sure to commit and push any changes", :green)
607
684
 
608
685
  say("")
@@ -614,20 +691,27 @@ module Tapioca
614
691
 
615
692
  exit(1)
616
693
  end
617
- ensure
618
- FileUtils.remove_entry(dir)
619
694
  end
620
695
 
621
- sig { params(files: T::Set[Pathname]).void }
622
- def purge_stale_dsl_rbi_files(files)
623
- if files.any?
624
- say("Removing stale RBI files...")
696
+ sig { void }
697
+ def abort_if_pending_migrations!
698
+ return unless File.exist?("config/application.rb")
699
+ return unless defined?(::Rake)
625
700
 
626
- files.sort.each do |filename|
627
- remove(filename)
628
- end
629
- say("")
630
- end
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
631
715
  end
632
716
  end
633
717
  end