tapioca 0.4.24 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +19 -31
  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 +26 -19
  41. data/lib/tapioca/generator.rb +127 -43
  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 -9
  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 +2 -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,23 +51,32 @@ 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
- deps = definition.locked_gems.dependencies.values
57
-
58
- missing_specs = T::Array[String].new
59
-
60
- dependencies = definition
61
- .resolve
62
- .materialize(deps, missing_specs)
63
- .map { |spec| Gem.new(spec) }
56
+ materialized_dependencies, missing_specs = materialize_deps
57
+ dependencies = materialized_dependencies
58
+ .map { |spec| GemSpec.new(spec) }
64
59
  .reject { |gem| gem.ignore?(dir) }
65
60
  .uniq(&:rbi_file_name)
66
61
  .sort_by(&:rbi_file_name)
67
-
68
62
  [dependencies, missing_specs]
69
63
  end
70
64
 
65
+ sig { returns([T::Enumerable[Spec], T::Array[String]]) }
66
+ def materialize_deps
67
+ deps = definition.locked_gems.dependencies.values
68
+ missing_specs = T::Array[String].new
69
+ materialized_dependencies = if definition.resolve.method(:materialize).arity == 1 # Support bundler >= v2.2.25
70
+ md = definition.resolve.materialize(deps)
71
+ missing_spec_names = md.missing_specs.map(&:name)
72
+ missing_specs = T.cast(md.missing_specs.map { |spec| "#{spec.name} (#{spec.version})" }, T::Array[String])
73
+ md.to_a.reject { |spec| missing_spec_names.include?(spec.name) }
74
+ else
75
+ definition.resolve.materialize(deps, missing_specs)
76
+ end
77
+ [materialized_dependencies, missing_specs]
78
+ end
79
+
71
80
  sig { returns(Bundler::Runtime) }
72
81
  def runtime
73
82
  Bundler::Runtime.new(File.dirname(gemfile.path), definition)
@@ -83,12 +92,10 @@ module Tapioca
83
92
  File.expand_path(gemfile.path + "/..")
84
93
  end
85
94
 
86
- class Gem
95
+ class GemSpec
87
96
  extend(T::Sig)
88
97
 
89
- IGNORED_GEMS = T.let(%w{
90
- sorbet sorbet-static sorbet-runtime
91
- }.freeze, T::Array[String])
98
+ IGNORED_GEMS = T.let(["sorbet", "sorbet-static", "sorbet-runtime"].freeze, T::Array[String])
92
99
 
93
100
  sig { returns(String) }
94
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)
@@ -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)
@@ -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, "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)
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