tapioca 0.5.3 → 0.6.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +114 -23
  3. data/lib/tapioca/cli.rb +188 -65
  4. data/lib/tapioca/compilers/dsl/active_record_associations.rb +94 -8
  5. data/lib/tapioca/compilers/dsl/active_record_columns.rb +5 -4
  6. data/lib/tapioca/compilers/dsl/active_record_enum.rb +1 -1
  7. data/lib/tapioca/compilers/dsl/active_record_relations.rb +703 -0
  8. data/lib/tapioca/compilers/dsl/active_record_scope.rb +43 -13
  9. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +39 -33
  10. data/lib/tapioca/compilers/dsl/base.rb +26 -42
  11. data/lib/tapioca/compilers/dsl/extensions/frozen_record.rb +29 -0
  12. data/lib/tapioca/compilers/dsl/frozen_record.rb +37 -0
  13. data/lib/tapioca/compilers/dsl/helper/active_record_constants.rb +27 -0
  14. data/lib/tapioca/compilers/dsl/identity_cache.rb +0 -1
  15. data/lib/tapioca/compilers/dsl/param_helper.rb +52 -0
  16. data/lib/tapioca/compilers/dsl/rails_generators.rb +120 -0
  17. data/lib/tapioca/compilers/dsl_compiler.rb +34 -6
  18. data/lib/tapioca/compilers/sorbet.rb +2 -0
  19. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +51 -48
  20. data/lib/tapioca/executor.rb +79 -0
  21. data/lib/tapioca/gemfile.rb +28 -4
  22. data/lib/tapioca/generators/base.rb +11 -18
  23. data/lib/tapioca/generators/dsl.rb +33 -38
  24. data/lib/tapioca/generators/gem.rb +64 -34
  25. data/lib/tapioca/generators/init.rb +41 -16
  26. data/lib/tapioca/generators/todo.rb +6 -6
  27. data/lib/tapioca/helpers/cli_helper.rb +26 -0
  28. data/lib/tapioca/helpers/config_helper.rb +84 -0
  29. data/lib/tapioca/helpers/test/content.rb +51 -0
  30. data/lib/tapioca/helpers/test/isolation.rb +125 -0
  31. data/lib/tapioca/helpers/test/template.rb +34 -0
  32. data/lib/tapioca/internal.rb +3 -2
  33. data/lib/tapioca/rbi_ext/model.rb +13 -10
  34. data/lib/tapioca/reflection.rb +13 -0
  35. data/lib/tapioca/trackers/autoload.rb +70 -0
  36. data/lib/tapioca/trackers/constant_definition.rb +42 -0
  37. data/lib/tapioca/trackers/mixin.rb +78 -0
  38. data/lib/tapioca/trackers.rb +14 -0
  39. data/lib/tapioca/version.rb +1 -1
  40. data/lib/tapioca.rb +28 -2
  41. metadata +37 -13
  42. data/lib/tapioca/config.rb +0 -45
  43. data/lib/tapioca/config_builder.rb +0 -73
  44. data/lib/tapioca/constant_locator.rb +0 -34
@@ -85,26 +85,13 @@ module Tapioca
85
85
 
86
86
  sig { params(tree: RBI::Tree, symbol: String).void }
87
87
  def generate_from_symbol(tree, symbol)
88
- constant = resolve_constant(symbol)
88
+ constant = constantize(symbol)
89
89
 
90
90
  return unless constant
91
91
 
92
92
  compile(tree, symbol, constant)
93
93
  end
94
94
 
95
- sig do
96
- params(
97
- symbol: String,
98
- inherit: T::Boolean,
99
- namespace: Module
100
- ).returns(BasicObject).checked(:never)
101
- end
102
- def resolve_constant(symbol, inherit: false, namespace: Object)
103
- namespace.const_get(symbol, inherit)
104
- rescue NameError, LoadError, RuntimeError, ArgumentError, TypeError
105
- nil
106
- end
107
-
108
95
  sig { params(tree: RBI::Tree, name: T.nilable(String), constant: BasicObject).void.checked(:never) }
109
96
  def compile(tree, name, constant)
110
97
  return unless constant
@@ -168,7 +155,8 @@ module Tapioca
168
155
  comments = documentation_comments(name)
169
156
 
170
157
  if klass_name == "T::Private::Types::TypeAlias"
171
- constant = RBI::Const.new(name, "T.type_alias { #{T.unsafe(value).aliased_type} }", comments: comments)
158
+ type_alias = sanitize_signature_types(T.unsafe(value).aliased_type.to_s)
159
+ constant = RBI::Const.new(name, "T.type_alias { #{type_alias} }", comments: comments)
172
160
  tree << constant
173
161
  return
174
162
  end
@@ -229,7 +217,8 @@ module Tapioca
229
217
 
230
218
  sig { params(tree: RBI::Tree, constant: Module).void }
231
219
  def compile_module_helpers(tree, constant)
232
- abstract_type = T::Private::Abstract::Data.get(constant, :abstract_type)
220
+ abstract_type = T::Private::Abstract::Data.get(constant, :abstract_type) ||
221
+ T::Private::Abstract::Data.get(singleton_class_of(constant), :abstract_type)
233
222
 
234
223
  tree << RBI::Helper.new(abstract_type.to_s) if abstract_type
235
224
  tree << RBI::Helper.new("final") if T::Private::Final.final_module?(constant)
@@ -267,7 +256,7 @@ module Tapioca
267
256
  def compile_subconstants(tree, name, constant)
268
257
  constants_of(constant).sort.uniq.map do |constant_name|
269
258
  symbol = (name == "Object" ? "" : name) + "::#{constant_name}"
270
- subconstant = resolve_constant(symbol)
259
+ subconstant = constantize(symbol)
271
260
 
272
261
  # Don't compile modules of Object because Object::Foo == Foo
273
262
  # Don't compile modules of BasicObject because BasicObject::BasicObject == BasicObject
@@ -298,7 +287,7 @@ module Tapioca
298
287
  # variable via the value of the type variable constant.
299
288
  subconstant_to_name_lookup = constants_of(constant)
300
289
  .each_with_object({}.compare_by_identity) do |constant_name, table|
301
- table[resolve_constant(constant_name.to_s, namespace: constant)] = constant_name.to_s
290
+ table[constantize(constant_name.to_s, namespace: constant)] = constant_name.to_s
302
291
  end
303
292
 
304
293
  # Map each type variable to its string representation.
@@ -354,7 +343,7 @@ module Tapioca
354
343
  superclass_name = name_of(superclass)
355
344
  next unless superclass_name
356
345
 
357
- resolved_superclass = resolve_constant(superclass_name)
346
+ resolved_superclass = constantize(superclass_name)
358
347
  next unless Module === resolved_superclass
359
348
  next if name_of(resolved_superclass) == constant_name
360
349
 
@@ -380,43 +369,44 @@ module Tapioca
380
369
  interesting_ancestors = interesting_ancestors_of(constant)
381
370
  interesting_singleton_class_ancestors = interesting_ancestors_of(singleton_class)
382
371
 
383
- prepend = interesting_ancestors.take_while { |c| !are_equal?(constant, c) }
384
- include = interesting_ancestors.drop(prepend.size + 1)
385
- extend = interesting_singleton_class_ancestors.reject do |mod|
372
+ prepends = interesting_ancestors.take_while { |c| !are_equal?(constant, c) }
373
+ includes = interesting_ancestors.drop(prepends.size + 1)
374
+ extends = interesting_singleton_class_ancestors.reject do |mod|
386
375
  Module != class_of(mod) || are_equal?(mod, singleton_class)
387
376
  end
388
377
 
389
- prepend
390
- .reverse
391
- .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
392
- .map do |mod|
393
- add_to_symbol_queue(name_of(mod))
394
-
395
- # TODO: Sorbet currently does not handle prepend
396
- # properly for method resolution, so we generate an
397
- # include statement instead
398
- qname = qualified_name_of(mod)
399
- tree << RBI::Include.new(T.must(qname))
400
- end
378
+ add_mixins(tree, prepends.reverse, Trackers::Mixin::Type::Prepend)
379
+ add_mixins(tree, includes.reverse, Trackers::Mixin::Type::Include)
380
+ add_mixins(tree, extends.reverse, Trackers::Mixin::Type::Extend)
381
+ end
401
382
 
402
- include
403
- .reverse
404
- .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
405
- .map do |mod|
406
- add_to_symbol_queue(name_of(mod))
383
+ sig do
384
+ params(
385
+ tree: RBI::Tree,
386
+ mods: T::Array[Module],
387
+ mixin_type: Trackers::Mixin::Type
388
+ ).void
389
+ end
390
+ def add_mixins(tree, mods, mixin_type)
391
+ mods
392
+ .select do |mod|
393
+ name = name_of(mod)
407
394
 
408
- qname = qualified_name_of(mod)
409
- tree << RBI::Include.new(T.must(qname))
395
+ name && !name.start_with?("T::")
410
396
  end
411
-
412
- extend
413
- .reverse
414
- .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
415
397
  .map do |mod|
416
398
  add_to_symbol_queue(name_of(mod))
417
399
 
418
400
  qname = qualified_name_of(mod)
419
- tree << RBI::Extend.new(T.must(qname))
401
+ case mixin_type
402
+ # TODO: Sorbet currently does not handle prepend
403
+ # properly for method resolution, so we generate an
404
+ # include statement instead
405
+ when Trackers::Mixin::Type::Include, Trackers::Mixin::Type::Prepend
406
+ tree << RBI::Include.new(T.must(qname))
407
+ when Trackers::Mixin::Type::Extend
408
+ tree << RBI::Extend.new(T.must(qname))
409
+ end
420
410
  end
421
411
  end
422
412
 
@@ -645,7 +635,7 @@ module Tapioca
645
635
  sig { params(constant: Module, strict: T::Boolean).returns(T::Boolean) }
646
636
  def defined_in_gem?(constant, strict: true)
647
637
  files = Set.new(get_file_candidates(constant))
648
- .merge(Tapioca::ConstantLocator.files_for(constant))
638
+ .merge(Tapioca::Trackers::ConstantDefinition.files_for(constant))
649
639
 
650
640
  return !strict if files.empty?
651
641
 
@@ -654,6 +644,19 @@ module Tapioca
654
644
  end
655
645
  end
656
646
 
647
+ sig do
648
+ params(
649
+ mod: Module,
650
+ mixin_type: Trackers::Mixin::Type,
651
+ mixin_locations: T::Hash[Trackers::Mixin::Type, T::Hash[Module, T::Array[String]]]
652
+ ).returns(T::Boolean)
653
+ end
654
+ def mixed_in_by_gem?(mod, mixin_type, mixin_locations)
655
+ locations = mixin_locations.dig(mixin_type, mod)
656
+ return true unless locations
657
+ locations.any? { |location| gem.contains_path?(location) }
658
+ end
659
+
657
660
  sig { params(constant: Module).returns(T::Array[String]) }
658
661
  def get_file_candidates(constant)
659
662
  wrapped_module = Pry::WrappedModule.new(constant)
@@ -725,7 +728,7 @@ module Tapioca
725
728
  return name if name
726
729
  name = super(constant)
727
730
  return if name.nil?
728
- return unless are_equal?(constant, resolve_constant(name, inherit: true))
731
+ return unless are_equal?(constant, constantize(name, inherit: true))
729
732
  name = "Struct" if name =~ /^(::)?Struct::[^:]+$/
730
733
  name
731
734
  end
@@ -0,0 +1,79 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "etc"
5
+
6
+ module Tapioca
7
+ class Executor
8
+ extend T::Sig
9
+
10
+ MINIMUM_ITEMS_PER_WORKER = T.let(2, Integer)
11
+
12
+ sig { params(queue: T::Array[T.untyped], number_of_workers: T.nilable(Integer)).void }
13
+ def initialize(queue, number_of_workers: nil)
14
+ @queue = queue
15
+
16
+ # Forking workers is expensive and not worth it for a low number of gems. Here we assign the number of workers to
17
+ # be the minimum between the number of available processors (max) or the number of workers to make sure that each
18
+ # one has at least 4 items to process
19
+ @number_of_workers = T.let(
20
+ number_of_workers || [Etc.nprocessors, (queue.length.to_f / MINIMUM_ITEMS_PER_WORKER).ceil].min,
21
+ Integer
22
+ )
23
+
24
+ # The number of items that will be processed per worker, so that we can split the queue into groups and assign
25
+ # them to each one of the workers
26
+ @items_per_worker = T.let((queue.length.to_f / @number_of_workers).ceil, Integer)
27
+ end
28
+
29
+ sig do
30
+ type_parameters(:T).params(
31
+ block: T.proc.params(item: T.untyped).returns(T.type_parameter(:T))
32
+ ).returns(T::Array[T.type_parameter(:T)])
33
+ end
34
+ def run_in_parallel(&block)
35
+ # If we only have one worker selected, it's not worth forking, just run sequentially
36
+ return @queue.map { |item| block.call(item) } if @number_of_workers == 1
37
+
38
+ read_pipes = []
39
+ write_pipes = []
40
+
41
+ # If we have more than one worker, fork the pool by shifting the expected number of items per worker from the
42
+ # queue
43
+ workers = (0...@number_of_workers).map do
44
+ items = @queue.shift(@items_per_worker)
45
+
46
+ # Each worker has their own pair of pipes, so that we can read the result from each worker separately
47
+ read, write = IO.pipe
48
+ read_pipes << read
49
+ write_pipes << write
50
+
51
+ fork do
52
+ read.close
53
+ result = items.map { |item| block.call(item) }
54
+
55
+ # Pack the result as a Base64 string of the Marshal dump of the array of values returned by the block that we
56
+ # ran in parallel
57
+ packed = [Marshal.dump(result)].pack("m")
58
+ write.puts(packed)
59
+ write.close
60
+ end
61
+ end
62
+
63
+ # Close all the write pipes, then read and close from all the read pipes
64
+ write_pipes.each(&:close)
65
+ result = read_pipes.map do |pipe|
66
+ content = pipe.read
67
+ pipe.close
68
+ content
69
+ end
70
+
71
+ # Wait until all the workers finish. Notice that waiting for the PIDs can only happen after we read and close the
72
+ # pipe or else we may end up in a condition where writing to the pipe hangs indefinitely
73
+ workers.each { |pid| Process.waitpid(pid) }
74
+
75
+ # Decode the value back into the Ruby objects by doing the inverse of what each worker does
76
+ result.flat_map { |item| T.unsafe(Marshal.load(item.unpack1("m"))) }
77
+ end
78
+ end
79
+ end
@@ -105,6 +105,7 @@ module Tapioca
105
105
  real_gem_path = to_realpath(@spec.full_gem_path)
106
106
  @full_gem_path = T.let(real_gem_path, String)
107
107
  @version = T.let(version_string, String)
108
+ @exported_rbi_files = T.let(nil, T.nilable(T::Array[String]))
108
109
  end
109
110
 
110
111
  sig { params(gemfile_dir: String).returns(T::Boolean) }
@@ -114,13 +115,14 @@ module Tapioca
114
115
 
115
116
  sig { returns(T::Array[Pathname]) }
116
117
  def files
117
- spec = @spec
118
- if default_gem? && spec.is_a?(::Gem::Specification)
119
- spec.files.map do |file|
118
+ if default_gem?
119
+ # `Bundler::RemoteSpecification` delegates missing methods to
120
+ # `Gem::Specification`, so `files` actually always exists on spec.
121
+ T.unsafe(@spec).files.map do |file|
120
122
  ruby_lib_dir.join(file)
121
123
  end
122
124
  else
123
- spec.full_require_paths.flat_map do |path|
125
+ @spec.full_require_paths.flat_map do |path|
124
126
  Pathname.glob((Pathname.new(path) / "**/*.rb").to_s)
125
127
  end
126
128
  end
@@ -150,6 +152,28 @@ module Tapioca
150
152
  files.each { |path| YARD.parse(path.to_s, [], Logger::Severity::FATAL) }
151
153
  end
152
154
 
155
+ sig { returns(T::Array[String]) }
156
+ def exported_rbi_files
157
+ @exported_rbi_files ||= Dir.glob("#{full_gem_path}/rbi/**/*.rbi")
158
+ end
159
+
160
+ sig { returns(T::Boolean) }
161
+ def export_rbi_files?
162
+ exported_rbi_files.any?
163
+ end
164
+
165
+ sig { returns(RBI::MergeTree) }
166
+ def exported_rbi_tree
167
+ rewriter = RBI::Rewriters::Merge.new(keep: RBI::Rewriters::Merge::Keep::NONE)
168
+
169
+ exported_rbi_files.each do |file|
170
+ rbi = RBI::Parser.parse_file(file)
171
+ rewriter.merge(rbi)
172
+ end
173
+
174
+ rewriter.tree
175
+ end
176
+
153
177
  private
154
178
 
155
179
  sig { returns(T::Boolean) }
@@ -1,9 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- # TODO: Remove me when logging logic has been abstracted.
5
- require "thor"
6
-
7
4
  module Tapioca
8
5
  module Generators
9
6
  class Base
@@ -14,7 +11,7 @@ module Tapioca
14
11
  include Thor::Actions
15
12
  end
16
13
 
17
- # TODO: Remove me when logging logic has been abstracted
14
+ include CliHelper
18
15
  include Thor::Base
19
16
 
20
17
  abstract!
@@ -30,20 +27,6 @@ module Tapioca
30
27
 
31
28
  private
32
29
 
33
- # TODO: Remove me when logging logic has been abstracted
34
- sig { params(message: String, color: T.any(Symbol, T::Array[Symbol])).void }
35
- def say_error(message = "", *color)
36
- force_new_line = (message.to_s !~ /( |\t)\Z/)
37
- # NOTE: This is a hack. We're no longer subclassing from Thor::Shell::Color
38
- # so we no longer have access to the prepare_message call.
39
- # We should update this to remove this.
40
- buffer = shell.send(:prepare_message, *T.unsafe([message, *T.unsafe(color)]))
41
- buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
42
-
43
- $stderr.print(buffer)
44
- $stderr.flush
45
- end
46
-
47
30
  sig do
48
31
  params(
49
32
  path: T.any(String, Pathname),
@@ -56,6 +39,16 @@ module Tapioca
56
39
  def create_file(path, content, force: true, skip: false, verbose: true)
57
40
  @file_writer.create_file(path, force: force, skip: skip, verbose: verbose) { content }
58
41
  end
42
+
43
+ sig do
44
+ params(
45
+ path: T.any(String, Pathname),
46
+ verbose: T::Boolean
47
+ ).void
48
+ end
49
+ def remove_file(path, verbose: true)
50
+ @file_writer.remove_file(path, verbose: verbose)
51
+ end
59
52
  end
60
53
  end
61
54
  end
@@ -8,8 +8,8 @@ module Tapioca
8
8
  params(
9
9
  requested_constants: T::Array[String],
10
10
  outpath: Pathname,
11
- generators: T::Array[String],
12
- exclude_generators: T::Array[String],
11
+ only: T::Array[String],
12
+ exclude: T::Array[String],
13
13
  file_header: T::Boolean,
14
14
  compiler_path: String,
15
15
  tapioca_path: String,
@@ -17,14 +17,15 @@ module Tapioca
17
17
  file_writer: Thor::Actions,
18
18
  should_verify: T::Boolean,
19
19
  quiet: T::Boolean,
20
- verbose: T::Boolean
20
+ verbose: T::Boolean,
21
+ number_of_workers: T.nilable(Integer),
21
22
  ).void
22
23
  end
23
24
  def initialize(
24
25
  requested_constants:,
25
26
  outpath:,
26
- generators:,
27
- exclude_generators:,
27
+ only:,
28
+ exclude:,
28
29
  file_header:,
29
30
  compiler_path:,
30
31
  tapioca_path:,
@@ -32,18 +33,20 @@ module Tapioca
32
33
  file_writer: FileWriter.new,
33
34
  should_verify: false,
34
35
  quiet: false,
35
- verbose: false
36
+ verbose: false,
37
+ number_of_workers: nil
36
38
  )
37
39
  @requested_constants = requested_constants
38
40
  @outpath = outpath
39
- @generators = generators
40
- @exclude_generators = exclude_generators
41
+ @only = only
42
+ @exclude = exclude
41
43
  @file_header = file_header
42
44
  @compiler_path = compiler_path
43
45
  @tapioca_path = tapioca_path
44
46
  @should_verify = should_verify
45
47
  @quiet = quiet
46
48
  @verbose = verbose
49
+ @number_of_workers = number_of_workers
47
50
 
48
51
  super(default_command: default_command, file_writer: file_writer)
49
52
 
@@ -52,6 +55,7 @@ module Tapioca
52
55
 
53
56
  sig { override.void }
54
57
  def generate
58
+ load_dsl_extensions
55
59
  load_application(eager_load: @requested_constants.empty?)
56
60
  abort_if_pending_migrations!
57
61
  load_dsl_generators
@@ -68,31 +72,31 @@ module Tapioca
68
72
 
69
73
  compiler = Compilers::DslCompiler.new(
70
74
  requested_constants: constantize(@requested_constants),
71
- requested_generators: constantize_generators(@generators),
72
- excluded_generators: constantize_generators(@exclude_generators),
75
+ requested_generators: constantize_generators(@only),
76
+ excluded_generators: constantize_generators(@exclude),
73
77
  error_handler: ->(error) {
74
78
  say_error(error, :bold, :red)
75
- }
79
+ },
80
+ number_of_workers: @number_of_workers
76
81
  )
77
82
 
78
- compiler.run do |constant, contents|
83
+ processed_files = compiler.run do |constant, contents|
79
84
  constant_name = T.must(Reflection.name_of(constant))
80
85
 
81
86
  if @verbose && !@quiet
82
87
  say_status(:processing, constant_name, :yellow)
83
88
  end
84
89
 
85
- filename = compile_dsl_rbi(
90
+ compile_dsl_rbi(
86
91
  constant_name,
87
92
  contents,
88
93
  outpath: outpath,
89
94
  quiet: @should_verify || @quiet && !@verbose
90
95
  )
91
-
92
- if filename
93
- rbi_files_to_purge.delete(filename)
94
- end
95
96
  end
97
+
98
+ processed_files.each { |filename| rbi_files_to_purge.delete(T.must(filename)) }
99
+
96
100
  say("")
97
101
 
98
102
  if @should_verify
@@ -171,7 +175,8 @@ module Tapioca
171
175
  unless unprocessable_constants.empty?
172
176
  unprocessable_constants.each do |name, _|
173
177
  say("Error: Cannot find constant '#{name}'", :red)
174
- remove(dsl_rbi_filename(name))
178
+ filename = dsl_rbi_filename(name)
179
+ remove_file(filename) if File.file?(filename)
175
180
  end
176
181
 
177
182
  exit(1)
@@ -182,17 +187,9 @@ module Tapioca
182
187
 
183
188
  sig { params(generator_names: T::Array[String]).returns(T::Array[T.class_of(Compilers::Dsl::Base)]) }
184
189
  def constantize_generators(generator_names)
185
- generator_map = generator_names.map do |name|
186
- # Try to find built-in tapioca generator first, then globally defined generator. The
187
- # explicit `break` ensures the class is returned, not the `potential_name`.
188
- generator_klass = ["Tapioca::Compilers::Dsl::#{name}", name].find do |potential_name|
189
- break Object.const_get(potential_name)
190
- rescue NameError
191
- # Skip if we can't find generator by the potential name
192
- end
193
-
194
- [name, generator_klass]
195
- end.to_h
190
+ generator_map = generator_names.to_h do |name|
191
+ [name, Compilers::Dsl::Base.resolve(name)]
192
+ end
196
193
 
197
194
  unprocessable_generators = generator_map.select { |_, v| v.nil? }
198
195
  unless unprocessable_generators.empty?
@@ -203,7 +200,7 @@ module Tapioca
203
200
  exit(1)
204
201
  end
205
202
 
206
- generator_map.values
203
+ T.cast(generator_map.values, T::Array[T.class_of(Compilers::Dsl::Base)])
207
204
  end
208
205
 
209
206
  sig do
@@ -245,7 +242,7 @@ module Tapioca
245
242
  say("Removing stale RBI files...")
246
243
 
247
244
  files.sort.each do |filename|
248
- remove(filename)
245
+ remove_file(filename)
249
246
  end
250
247
  say("")
251
248
  end
@@ -256,13 +253,6 @@ module Tapioca
256
253
  @outpath / "#{underscore(constant_name)}.rbi"
257
254
  end
258
255
 
259
- sig { params(filename: Pathname).void }
260
- def remove(filename)
261
- return unless filename.exist?
262
- say("-- Removing: #{filename}")
263
- filename.unlink
264
- end
265
-
266
256
  sig { params(tmp_dir: Pathname).returns(T::Hash[String, Symbol]) }
267
257
  def verify_dsl_rbi(tmp_dir:)
268
258
  diff = {}
@@ -357,6 +347,11 @@ module Tapioca
357
347
  def generate_command_for(constant)
358
348
  "#{@default_command} dsl #{constant}"
359
349
  end
350
+
351
+ sig { void }
352
+ def load_dsl_extensions
353
+ Dir["#{__dir__}/../compilers/dsl/extensions/*.rb"].sort.each { |f| require(f) }
354
+ end
360
355
  end
361
356
  end
362
357
  end