tapioca 0.4.23 → 0.5.0

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 +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 +35 -23
  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 -82
  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,9 @@
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"
6
+ require "rake"
7
7
 
8
8
  module Tapioca
9
9
  class Generator < ::Thor::Shell::Color
@@ -63,11 +63,8 @@ module Tapioca
63
63
  File.delete(requires_path) if File.exist?(requires_path)
64
64
 
65
65
  content = String.new
66
- content << rbi_header(
67
- "#{Config::DEFAULT_COMMAND} require",
68
- reason: "explicit gem requires",
69
- strictness: "false"
70
- )
66
+ content << "# typed: true\n"
67
+ content << "# frozen_string_literal: true\n\n"
71
68
  content << rb_string
72
69
 
73
70
  outdir = File.dirname(requires_path)
@@ -121,11 +118,13 @@ module Tapioca
121
118
  params(
122
119
  requested_constants: T::Array[String],
123
120
  should_verify: T::Boolean,
124
- quiet: T::Boolean
121
+ quiet: T::Boolean,
122
+ verbose: T::Boolean
125
123
  ).void
126
124
  end
127
- def build_dsl(requested_constants, should_verify: false, quiet: false)
125
+ def build_dsl(requested_constants, should_verify: false, quiet: false, verbose: false)
128
126
  load_application(eager_load: requested_constants.empty?)
127
+ abort_if_pending_migrations!
129
128
  load_dsl_generators
130
129
 
131
130
  if should_verify
@@ -140,20 +139,26 @@ module Tapioca
140
139
 
141
140
  compiler = Compilers::DslCompiler.new(
142
141
  requested_constants: constantize(requested_constants),
143
- requested_generators: config.generators,
142
+ requested_generators: constantize_generators(config.generators),
143
+ excluded_generators: constantize_generators(config.exclude_generators),
144
144
  error_handler: ->(error) {
145
145
  say_error(error, :bold, :red)
146
146
  }
147
147
  )
148
148
 
149
149
  compiler.run do |constant, contents|
150
- constant_name = Module.instance_method(:name).bind(constant).call
150
+ constant_name = T.must(Reflection.name_of(constant))
151
+
152
+ if verbose && !quiet
153
+ say("Processing: ", [:yellow])
154
+ say(constant_name)
155
+ end
151
156
 
152
157
  filename = compile_dsl_rbi(
153
158
  constant_name,
154
159
  contents,
155
160
  outpath: outpath,
156
- quiet: should_verify || quiet
161
+ quiet: should_verify || quiet && !verbose
157
162
  )
158
163
 
159
164
  if filename
@@ -174,8 +179,15 @@ module Tapioca
174
179
  end
175
180
  end
176
181
 
177
- sig { void }
178
- def sync_rbis_with_gemfile
182
+ sig { params(should_verify: T::Boolean).void }
183
+ def sync_rbis_with_gemfile(should_verify: false)
184
+ if should_verify
185
+ say("Checking for out-of-date RBIs...")
186
+ say("")
187
+ perform_sync_verification
188
+ return
189
+ end
190
+
179
191
  anything_done = [
180
192
  perform_removals,
181
193
  perform_additions,
@@ -195,7 +207,7 @@ module Tapioca
195
207
 
196
208
  EMPTY_RBI_COMMENT = <<~CONTENT
197
209
  # THIS IS AN EMPTY RBI FILE.
198
- # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
210
+ # see https://github.com/Shopify/tapioca/wiki/Manual-Gem-Requires
199
211
  CONTENT
200
212
 
201
213
  sig { returns(Gemfile) }
@@ -205,7 +217,7 @@ module Tapioca
205
217
 
206
218
  sig { returns(Loader) }
207
219
  def loader
208
- @loader ||= Loader.new(bundle)
220
+ @loader ||= Loader.new
209
221
  end
210
222
 
211
223
  sig { returns(Compilers::SymbolTableCompiler) }
@@ -217,7 +229,7 @@ module Tapioca
217
229
  def require_gem_file
218
230
  say("Requiring all gems to prepare for compiling... ")
219
231
  begin
220
- loader.load_bundle(config.prerequire, config.postrequire)
232
+ loader.load_bundle(bundle, config.prerequire, config.postrequire)
221
233
  rescue LoadError => e
222
234
  explain_failed_require(config.postrequire, e)
223
235
  exit(1)
@@ -225,7 +237,7 @@ module Tapioca
225
237
  say(" Done", :green)
226
238
  unless bundle.missing_specs.empty?
227
239
  say(" completed with missing specs: ")
228
- say(bundle.missing_specs.join(', '), :yellow)
240
+ say(bundle.missing_specs.join(", "), :yellow)
229
241
  end
230
242
  puts
231
243
  end
@@ -260,7 +272,7 @@ module Tapioca
260
272
  def load_application(eager_load:)
261
273
  say("Loading Rails application... ")
262
274
 
263
- loader.load_rails(
275
+ loader.load_rails_application(
264
276
  environment_load: true,
265
277
  eager_load: eager_load
266
278
  )
@@ -285,11 +297,9 @@ module Tapioca
285
297
  sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
286
298
  def constantize(constant_names)
287
299
  constant_map = constant_names.map do |name|
288
- begin
289
- [name, Object.const_get(name)]
290
- rescue NameError
291
- [name, nil]
292
- end
300
+ [name, Object.const_get(name)]
301
+ rescue NameError
302
+ [name, nil]
293
303
  end.to_h
294
304
 
295
305
  unprocessable_constants = constant_map.select { |_, v| v.nil? }
@@ -305,6 +315,32 @@ module Tapioca
305
315
  constant_map.values
306
316
  end
307
317
 
318
+ sig { params(generator_names: T::Array[String]).returns(T::Array[T.class_of(Compilers::Dsl::Base)]) }
319
+ def constantize_generators(generator_names)
320
+ generator_map = generator_names.map do |name|
321
+ # Try to find built-in tapioca generator first, then globally defined generator. The
322
+ # explicit `break` ensures the class is returned, not the `potential_name`.
323
+ generator_klass = ["Tapioca::Compilers::Dsl::#{name}", name].find do |potential_name|
324
+ break Object.const_get(potential_name)
325
+ rescue NameError
326
+ # Skip if we can't find generator by the potential name
327
+ end
328
+
329
+ [name, generator_klass]
330
+ end.to_h
331
+
332
+ unprocessable_generators = generator_map.select { |_, v| v.nil? }
333
+ unless unprocessable_generators.empty?
334
+ unprocessable_generators.each do |name, _|
335
+ say("Error: Cannot find generator '#{name}'", :red)
336
+ end
337
+
338
+ exit(1)
339
+ end
340
+
341
+ generator_map.values
342
+ end
343
+
308
344
  sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) }
309
345
  def existing_rbi_filenames(requested_constants, path: config.outpath)
310
346
  filenames = if requested_constants.empty?
@@ -321,7 +357,7 @@ module Tapioca
321
357
  sig { returns(T::Hash[String, String]) }
322
358
  def existing_rbis
323
359
  @existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s)
324
- .map { |f| T.cast(f.basename(".*").to_s.split('@', 2), [String, String]) }
360
+ .map { |f| T.cast(f.basename(".*").to_s.split("@", 2), [String, String]) }
325
361
  .to_h
326
362
  end
327
363
 
@@ -335,7 +371,7 @@ module Tapioca
335
371
 
336
372
  sig { params(constant_name: String).returns(Pathname) }
337
373
  def dsl_rbi_filename(constant_name)
338
- config.outpath / "#{constant_name.underscore}.rbi"
374
+ config.outpath / "#{underscore(constant_name)}.rbi"
339
375
  end
340
376
 
341
377
  sig { params(gem_name: String, version: String).returns(Pathname) }
@@ -456,7 +492,7 @@ module Tapioca
456
492
 
457
493
  sig do
458
494
  params(gem_names: T::Array[String])
459
- .returns(T::Array[Gemfile::Gem])
495
+ .returns(T::Array[Gemfile::GemSpec])
460
496
  end
461
497
  def gems_to_generate(gem_names)
462
498
  return bundle.dependencies if gem_names.empty?
@@ -483,10 +519,16 @@ module Tapioca
483
519
  # typed: #{strictness}
484
520
  SIGIL
485
521
 
486
- [statement, sigil].compact.join("\n").strip.concat("\n\n")
522
+ if config.file_header
523
+ [statement, sigil].compact.join("\n").strip.concat("\n\n")
524
+ elsif sigil
525
+ sigil.strip.concat("\n\n")
526
+ else
527
+ ""
528
+ end
487
529
  end
488
530
 
489
- sig { params(gem: Gemfile::Gem).void }
531
+ sig { params(gem: Gemfile::GemSpec).void }
490
532
  def compile_gem_rbi(gem)
491
533
  compiler = Compilers::SymbolTableCompiler.new
492
534
  gem_name = set_color(gem.name, :yellow, :bold)
@@ -525,7 +567,7 @@ module Tapioca
525
567
  def compile_dsl_rbi(constant_name, contents, outpath: config.outpath, quiet: false)
526
568
  return if contents.nil?
527
569
 
528
- rbi_name = constant_name.underscore + ".rbi"
570
+ rbi_name = underscore(constant_name) + ".rbi"
529
571
  filename = outpath / rbi_name
530
572
 
531
573
  out = String.new
@@ -598,11 +640,47 @@ module Tapioca
598
640
  def perform_dsl_verification(dir)
599
641
  diff = verify_dsl_rbi(tmp_dir: dir)
600
642
 
643
+ report_diff_and_exit_if_out_of_date(diff, "dsl")
644
+ ensure
645
+ FileUtils.remove_entry(dir)
646
+ end
647
+
648
+ sig { params(files: T::Set[Pathname]).void }
649
+ def purge_stale_dsl_rbi_files(files)
650
+ if files.any?
651
+ say("Removing stale RBI files...")
652
+
653
+ files.sort.each do |filename|
654
+ remove(filename)
655
+ end
656
+ say("")
657
+ end
658
+ end
659
+
660
+ sig { void }
661
+ def perform_sync_verification
662
+ diff = {}
663
+
664
+ removed_rbis.each do |gem_name|
665
+ filename = existing_rbi(gem_name)
666
+ diff[filename] = :removed
667
+ end
668
+
669
+ added_rbis.each do |gem_name|
670
+ filename = expected_rbi(gem_name)
671
+ diff[filename] = gem_rbi_exists?(gem_name) ? :changed : :added
672
+ end
673
+
674
+ report_diff_and_exit_if_out_of_date(diff, "sync")
675
+ end
676
+
677
+ sig { params(diff: T::Hash[String, Symbol], command: String).void }
678
+ def report_diff_and_exit_if_out_of_date(diff, command)
601
679
  if diff.empty?
602
680
  say("Nothing to do, all RBIs are up-to-date.")
603
681
  else
604
682
  say("RBI files are out-of-date. In your development environment, please run:", :green)
605
- say(" `#{Config::DEFAULT_COMMAND} dsl`", [:green, :bold])
683
+ say(" `#{Config::DEFAULT_COMMAND} #{command}`", [:green, :bold])
606
684
  say("Once it is complete, be sure to commit and push any changes", :green)
607
685
 
608
686
  say("")
@@ -614,20 +692,26 @@ module Tapioca
614
692
 
615
693
  exit(1)
616
694
  end
617
- ensure
618
- FileUtils.remove_entry(dir)
619
695
  end
620
696
 
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...")
697
+ sig { void }
698
+ def abort_if_pending_migrations!
699
+ return unless File.exist?("config/application.rb")
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