tapioca 0.4.16 → 0.4.21

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.
@@ -89,7 +89,7 @@ module Tapioca
89
89
  class UrlHelpers < Base
90
90
  extend T::Sig
91
91
 
92
- sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
92
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: Module).void }
93
93
  def decorate(root, constant)
94
94
  case constant
95
95
  when GeneratedPathHelpersModule.singleton_class, GeneratedUrlHelpersModule.singleton_class
@@ -127,7 +127,7 @@ module Tapioca
127
127
 
128
128
  private
129
129
 
130
- sig { params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
130
+ sig { params(root: Parlour::RbiGenerator::Namespace, constant: Module).void }
131
131
  def generate_module_for(root, constant)
132
132
  root.create_module(T.must(constant.name)) do |mod|
133
133
  mod.create_include("::ActionDispatch::Routing::UrlFor")
@@ -143,7 +143,7 @@ module Tapioca
143
143
  end
144
144
  end
145
145
 
146
- sig { params(mod: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module), helper_module: Module).void }
146
+ sig { params(mod: Parlour::RbiGenerator::Namespace, constant: Module, helper_module: Module).void }
147
147
  def create_mixins_for(mod, constant, helper_module)
148
148
  include_helper = constant.ancestors.include?(helper_module) || NON_DISCOVERABLE_INCLUDERS.include?(constant)
149
149
  extend_helper = constant.singleton_class.ancestors.include?(helper_module)
@@ -74,9 +74,15 @@ module Tapioca
74
74
  compile(symbol, constant)
75
75
  end
76
76
 
77
- sig { params(symbol: String, inherit: T::Boolean).returns(BasicObject).checked(:never) }
78
- def resolve_constant(symbol, inherit: false)
79
- Object.const_get(symbol, inherit)
77
+ sig do
78
+ params(
79
+ symbol: String,
80
+ inherit: T::Boolean,
81
+ namespace: Module
82
+ ).returns(BasicObject).checked(:never)
83
+ end
84
+ def resolve_constant(symbol, inherit: false, namespace: Object)
85
+ namespace.const_get(symbol, inherit)
80
86
  rescue NameError, LoadError, RuntimeError, ArgumentError, TypeError
81
87
  nil
82
88
  end
@@ -142,9 +148,11 @@ module Tapioca
142
148
  def compile_object(name, value)
143
149
  return if symbol_ignored?(name)
144
150
  klass = class_of(value)
145
- return if name_of(klass)&.start_with?("T::Types::", "T::Private::")
151
+ klass_name = name_of(klass)
152
+
153
+ return if klass_name&.start_with?("T::Types::", "T::Private::")
146
154
 
147
- type_name = public_module?(klass) && name_of(klass) || "T.untyped"
155
+ type_name = public_module?(klass) && klass_name || "T.untyped"
148
156
  indented("#{name} = T.let(T.unsafe(nil), #{type_name})")
149
157
  end
150
158
 
@@ -181,16 +189,17 @@ module Tapioca
181
189
 
182
190
  [
183
191
  compile_module_helpers(constant),
192
+ compile_type_variables(constant),
184
193
  compile_mixins(constant),
185
194
  compile_mixes_in_class_methods(constant),
186
195
  compile_props(constant),
187
196
  compile_enums(constant),
188
197
  methods,
189
- ].select { |b| b != "" }.join("\n\n")
198
+ ].select { |b| b && !b.empty? }.join("\n\n")
190
199
  end
191
200
  end
192
201
 
193
- sig { params(constant: Module).returns(String) }
202
+ sig { params(constant: Module).returns(T.nilable(String)) }
194
203
  def compile_module_helpers(constant)
195
204
  abstract_type = T::Private::Abstract::Data.get(constant, :abstract_type)
196
205
 
@@ -199,12 +208,14 @@ module Tapioca
199
208
  helpers << indented("final!") if T::Private::Final.final_module?(constant)
200
209
  helpers << indented("sealed!") if T::Private::Sealed.sealed_module?(constant)
201
210
 
211
+ return if helpers.empty?
212
+
202
213
  helpers.join("\n")
203
214
  end
204
215
 
205
- sig { params(constant: Module).returns(String) }
216
+ sig { params(constant: Module).returns(T.nilable(String)) }
206
217
  def compile_props(constant)
207
- return "" unless T::Props::ClassMethods === constant
218
+ return unless T::Props::ClassMethods === constant
208
219
 
209
220
  constant.props.map do |name, prop|
210
221
  method = "prop"
@@ -219,11 +230,11 @@ module Tapioca
219
230
  end.join("\n")
220
231
  end
221
232
 
222
- sig { params(constant: Module).returns(String) }
233
+ sig { params(constant: Module).returns(T.nilable(String)) }
223
234
  def compile_enums(constant)
224
- return "" unless constant < T::Enum
235
+ return unless T::Enum > constant
225
236
 
226
- enums = T.cast(constant, T::Enum).values.map do |enum_type|
237
+ enums = T.unsafe(constant).values.map do |enum_type|
227
238
  enum_type.instance_variable_get(:@const_name).to_s
228
239
  end
229
240
 
@@ -250,11 +261,71 @@ module Tapioca
250
261
  compile(symbol, subconstant)
251
262
  end.compact
252
263
 
253
- return "" if output.empty?
264
+ return if output.empty?
254
265
 
255
266
  "\n" + output.join("\n\n")
256
267
  end
257
268
 
269
+ sig { params(constant: Module).returns(T.nilable(String)) }
270
+ def compile_type_variables(constant)
271
+ type_variables = compile_type_variable_declarations(constant)
272
+ singleton_class_type_variables = compile_type_variable_declarations(singleton_class_of(constant))
273
+
274
+ return if !type_variables && !singleton_class_type_variables
275
+
276
+ type_variables += "\n" if type_variables
277
+ singleton_class_type_variables += "\n" if singleton_class_type_variables
278
+
279
+ [
280
+ type_variables,
281
+ singleton_class_type_variables,
282
+ ].compact.join("\n").rstrip
283
+ end
284
+
285
+ sig { params(constant: Module).returns(T.nilable(String)) }
286
+ def compile_type_variable_declarations(constant)
287
+ with_indentation_for_constant(constant) do
288
+ # Try to find the type variables defined on this constant, bail if we can't
289
+ type_variables = GenericTypeRegistry.lookup_type_variables(constant)
290
+ return unless type_variables
291
+
292
+ # Create a map of subconstants (via their object ids) to their names.
293
+ # We need this later when we want to lookup the name of the registered type
294
+ # variable via the value of the type variable constant.
295
+ subconstant_to_name_lookup = constants_of(constant).map do |constant_name|
296
+ [
297
+ object_id_of(resolve_constant(constant_name.to_s, namespace: constant)),
298
+ constant_name,
299
+ ]
300
+ end.to_h
301
+
302
+ # Map each type variable to its string representation.
303
+ #
304
+ # Each entry of `type_variables` maps an object_id to a String,
305
+ # and the order they are inserted into the hash is the order they should be
306
+ # defined in the source code.
307
+ #
308
+ # By looping over these entries and then getting the actual constant name
309
+ # from the `subconstant_to_name_lookup` we defined above, gives us all the
310
+ # information we need to serialize type variable definitions.
311
+ type_variable_declarations = type_variables.map do |type_variable_id, serialized_type_variable|
312
+ constant_name = subconstant_to_name_lookup[type_variable_id]
313
+ # Here, we know that constant_value will be an instance of
314
+ # T::Types::CustomTypeVariable, which knows how to serialize
315
+ # itself to a type_member/type_template
316
+ indented("#{constant_name} = #{serialized_type_variable}")
317
+ end.compact
318
+
319
+ return if type_variable_declarations.empty?
320
+
321
+ [
322
+ indented("extend T::Generic"),
323
+ "",
324
+ *type_variable_declarations,
325
+ ].compact.join("\n")
326
+ end
327
+ end
328
+
258
329
  sig { params(constant: Class).returns(String) }
259
330
  def compile_superclass(constant)
260
331
  superclass = T.let(nil, T.nilable(Class)) # rubocop:disable Lint/UselessAssignment
@@ -431,29 +502,19 @@ module Tapioca
431
502
  instance_methods = compile_directly_owned_methods(name, constant)
432
503
  singleton_methods = compile_directly_owned_methods(name, singleton_class_of(constant))
433
504
 
434
- return if symbol_ignored?(name) && instance_methods.empty? && singleton_methods.empty?
505
+ return if symbol_ignored?(name) && !instance_methods && !singleton_methods
435
506
 
436
507
  [
437
- initialize_method || "",
508
+ initialize_method,
438
509
  instance_methods,
439
510
  singleton_methods,
440
- ].select { |b| b.strip != "" }.join("\n\n")
511
+ ].select { |b| b && !b.strip.empty? }.join("\n\n")
441
512
  end
442
513
 
443
- sig { params(module_name: String, mod: Module, for_visibility: T::Array[Symbol]).returns(String) }
514
+ sig { params(module_name: String, mod: Module, for_visibility: T::Array[Symbol]).returns(T.nilable(String)) }
444
515
  def compile_directly_owned_methods(module_name, mod, for_visibility = [:public, :protected, :private])
445
- indent_step = 0
446
- preamble = nil
447
- postamble = nil
448
-
449
- if mod.singleton_class?
450
- indent_step = 1
451
- preamble = indented("class << self")
452
- postamble = indented("end")
453
- end
454
-
455
- methods = with_indentation(indent_step) do
456
- method_names_by_visibility(mod)
516
+ with_indentation_for_constant(mod) do
517
+ methods = method_names_by_visibility(mod)
457
518
  .delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
458
519
  .flat_map do |visibility, method_list|
459
520
  compiled = method_list.sort!.map do |name|
@@ -470,16 +531,11 @@ module Tapioca
470
531
  compiled
471
532
  end
472
533
  .compact
473
- .join("\n")
474
- end
475
534
 
476
- return "" if methods.strip == ""
535
+ return if methods.empty?
477
536
 
478
- [
479
- preamble,
480
- methods,
481
- postamble,
482
- ].compact.join("\n")
537
+ methods.join("\n")
538
+ end
483
539
  end
484
540
 
485
541
  sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
@@ -659,6 +715,33 @@ module Tapioca
659
715
  @indent -= 2 * step
660
716
  end
661
717
 
718
+ sig do
719
+ params(
720
+ constant: Module,
721
+ blk: T.proc
722
+ .returns(T.nilable(String))
723
+ )
724
+ .returns(T.nilable(String))
725
+ end
726
+ def with_indentation_for_constant(constant, &blk)
727
+ step = if constant.singleton_class?
728
+ 1
729
+ else
730
+ 0
731
+ end
732
+
733
+ result = with_indentation(step, &blk)
734
+
735
+ return result unless result
736
+ return result unless constant.singleton_class?
737
+
738
+ [
739
+ indented("class << self"),
740
+ result,
741
+ indented("end"),
742
+ ].compact.join("\n")
743
+ end
744
+
662
745
  sig { params(str: String).returns(String) }
663
746
  def indented(str)
664
747
  " " * @indent + str
@@ -859,12 +942,12 @@ module Tapioca
859
942
  nil
860
943
  end
861
944
 
862
- sig { params(constant: Module).returns(String) }
945
+ sig { params(constant: T::Types::Base).returns(String) }
863
946
  def type_of(constant)
864
947
  constant.to_s.gsub(/\bAttachedClass\b/, "T.attached_class")
865
948
  end
866
949
 
867
- sig { params(object: Object).returns(T::Boolean).checked(:never) }
950
+ sig { params(object: BasicObject).returns(Integer).checked(:never) }
868
951
  def object_id_of(object)
869
952
  Object.instance_method(:object_id).bind(object).call
870
953
  end
@@ -8,7 +8,6 @@ module Tapioca
8
8
  const(:outdir, String)
9
9
  const(:prerequire, T.nilable(String))
10
10
  const(:postrequire, String)
11
- const(:generate_command, String)
12
11
  const(:exclude, T::Array[String])
13
12
  const(:typed_overrides, T::Hash[String, String])
14
13
  const(:todos_path, String)
@@ -27,6 +26,7 @@ module Tapioca
27
26
  TAPIOCA_PATH = T.let("#{SORBET_PATH}/tapioca", String)
28
27
  TAPIOCA_CONFIG = T.let("#{TAPIOCA_PATH}/config.yml", String)
29
28
 
29
+ DEFAULT_COMMAND = T.let("bin/tapioca", String)
30
30
  DEFAULT_POSTREQUIRE = T.let("#{TAPIOCA_PATH}/require.rb", String)
31
31
  DEFAULT_RBIDIR = T.let("#{SORBET_PATH}/rbi", String)
32
32
  DEFAULT_DSLDIR = T.let("#{DEFAULT_RBIDIR}/dsl", String)
@@ -10,9 +10,13 @@ module Tapioca
10
10
 
11
11
  sig { params(command: Symbol, options: T::Hash[String, T.untyped]).returns(Config) }
12
12
  def from_options(command, options)
13
- Config.from_hash(
14
- merge_options(default_options(command), config_options, options)
15
- )
13
+ merged_options = merge_options(default_options(command), config_options, options)
14
+
15
+ puts(<<~MSG) if merged_options.include?("generate_command")
16
+ DEPRECATION: The `-c` and `--cmd` flags will be removed in a future release.
17
+ MSG
18
+
19
+ Config.from_hash(merged_options)
16
20
  end
17
21
 
18
22
  private
@@ -40,14 +44,6 @@ module Tapioca
40
44
  DEFAULT_OPTIONS.merge("outdir" => default_outdir)
41
45
  end
42
46
 
43
- sig { returns(String) }
44
- def default_command
45
- command = File.basename($PROGRAM_NAME)
46
- args = ARGV.join(" ")
47
-
48
- "#{command} #{args}".strip
49
- end
50
-
51
47
  sig { params(options: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
52
48
  def merge_options(*options)
53
49
  options.each_with_object({}) do |option, result|
@@ -65,7 +61,6 @@ module Tapioca
65
61
  DEFAULT_OPTIONS = T.let({
66
62
  "postrequire" => Config::DEFAULT_POSTREQUIRE,
67
63
  "outdir" => nil,
68
- "generate_command" => default_command,
69
64
  "exclude" => [],
70
65
  "typed_overrides" => Config::DEFAULT_OVERRIDES,
71
66
  "todos_path" => Config::DEFAULT_TODOSPATH,
@@ -63,7 +63,7 @@ module Tapioca
63
63
 
64
64
  content = String.new
65
65
  content << rbi_header(
66
- config.generate_command,
66
+ "#{Config::DEFAULT_COMMAND} require",
67
67
  reason: "explicit gem requires",
68
68
  strictness: "false"
69
69
  )
@@ -76,8 +76,8 @@ module Tapioca
76
76
  say("Done", :green)
77
77
 
78
78
  say("All requires from this application have been written to #{name}.", [:green, :bold])
79
- cmd = set_color("tapioca sync", :yellow, :bold)
80
- say("Please review changes and commit them, then run #{cmd}.", [:green, :bold])
79
+ cmd = set_color("#{Config::DEFAULT_COMMAND} sync", :yellow, :bold)
80
+ say("Please review changes and commit them, then run `#{cmd}`.", [:green, :bold])
81
81
  end
82
82
 
83
83
  sig { void }
@@ -99,7 +99,7 @@ module Tapioca
99
99
 
100
100
  content = String.new
101
101
  content << rbi_header(
102
- config.generate_command,
102
+ "#{Config::DEFAULT_COMMAND} todo",
103
103
  reason: "unresolved constants",
104
104
  strictness: "false"
105
105
  )
@@ -116,16 +116,27 @@ module Tapioca
116
116
  say("Please review changes and commit them.", [:green, :bold])
117
117
  end
118
118
 
119
- sig { params(requested_constants: T::Array[String]).void }
120
- def build_dsl(requested_constants)
119
+ sig do
120
+ params(
121
+ requested_constants: T::Array[String],
122
+ should_verify: T::Boolean,
123
+ quiet: T::Boolean
124
+ ).void
125
+ end
126
+ def build_dsl(requested_constants, should_verify: false, quiet: false)
121
127
  load_application(eager_load: requested_constants.empty?)
122
128
  load_dsl_generators
123
129
 
124
- rbi_files_to_purge = existing_rbi_filenames(requested_constants)
125
-
126
- say("Compiling DSL RBI files...")
130
+ if should_verify
131
+ say("Checking for out-of-date RBIs...")
132
+ else
133
+ say("Compiling DSL RBI files...")
134
+ end
127
135
  say("")
128
136
 
137
+ outpath = should_verify ? Pathname.new(Dir.mktmpdir) : config.outpath
138
+ rbi_files_to_purge = existing_rbi_filenames(requested_constants)
139
+
129
140
  compiler = Compilers::DslCompiler.new(
130
141
  requested_constants: constantize(requested_constants),
131
142
  requested_generators: config.generators,
@@ -135,24 +146,31 @@ module Tapioca
135
146
  )
136
147
 
137
148
  compiler.run do |constant, contents|
138
- filename = compile_dsl_rbi(constant, contents)
139
- rbi_files_to_purge.delete(filename) if filename
140
- end
149
+ constant_name = Module.instance_method(:name).bind(constant).call
141
150
 
142
- unless rbi_files_to_purge.empty?
143
- say("")
144
- say("Removing stale RBI files...")
151
+ filename = compile_dsl_rbi(
152
+ constant_name,
153
+ contents,
154
+ outpath: outpath,
155
+ quiet: should_verify || quiet
156
+ )
145
157
 
146
- rbi_files_to_purge.sort.each do |filename|
147
- remove(filename)
158
+ if filename
159
+ rbi_files_to_purge.delete(filename)
148
160
  end
149
161
  end
150
-
151
162
  say("")
152
- say("Done", :green)
153
163
 
154
- say("All operations performed in working directory.", [:green, :bold])
155
- say("Please review changes and commit them.", [:green, :bold])
164
+ if should_verify
165
+ perform_dsl_verification(outpath)
166
+ else
167
+ purge_stale_dsl_rbi_files(rbi_files_to_purge)
168
+
169
+ say("Done", :green)
170
+
171
+ say("All operations performed in working directory.", [:green, :bold])
172
+ say("Please review changes and commit them.", [:green, :bold])
173
+ end
156
174
  end
157
175
 
158
176
  sig { void }
@@ -218,7 +236,7 @@ module Tapioca
218
236
  say_error("If you populated ", :yellow)
219
237
  say_error("#{file} ", :bold, :blue)
220
238
  say_error("with ", :yellow)
221
- say_error("tapioca require", :bold, :blue)
239
+ say_error("`#{Config::DEFAULT_COMMAND} require`", :bold, :blue)
222
240
  say_error("you should probably review it and remove the faulty line.", :yellow)
223
241
  end
224
242
 
@@ -286,10 +304,10 @@ module Tapioca
286
304
  constant_map.values
287
305
  end
288
306
 
289
- sig { params(requested_constants: T::Array[String]).returns(T::Set[Pathname]) }
290
- def existing_rbi_filenames(requested_constants)
307
+ sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) }
308
+ def existing_rbi_filenames(requested_constants, path: config.outpath)
291
309
  filenames = if requested_constants.empty?
292
- Pathname.glob(config.outpath / "**/*.rbi")
310
+ Pathname.glob(path / "**/*.rbi")
293
311
  else
294
312
  requested_constants.map do |constant_name|
295
313
  dsl_rbi_filename(constant_name)
@@ -477,7 +495,7 @@ module Tapioca
477
495
  rbi_body_content = compiler.compile(gem)
478
496
  content = String.new
479
497
  content << rbi_header(
480
- config.generate_command,
498
+ "#{Config::DEFAULT_COMMAND} sync",
481
499
  reason: "types exported from the `#{gem.name}` gem",
482
500
  strictness: strictness
483
501
  )
@@ -494,33 +512,120 @@ module Tapioca
494
512
  end
495
513
  File.write(filename.to_s, content)
496
514
 
497
- Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
515
+ T.unsafe(Pathname).glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
498
516
  remove(file) unless file.basename.to_s == gem.rbi_file_name
499
517
  end
500
518
  end
501
519
 
502
- sig { params(constant: Module, contents: String).returns(T.nilable(Pathname)) }
503
- def compile_dsl_rbi(constant, contents)
520
+ sig do
521
+ params(constant_name: String, contents: String, outpath: Pathname, quiet: T::Boolean)
522
+ .returns(T.nilable(Pathname))
523
+ end
524
+ def compile_dsl_rbi(constant_name, contents, outpath: config.outpath, quiet: false)
504
525
  return if contents.nil?
505
526
 
506
- command = format(config.generate_command, constant.name)
507
- constant_name = Module.instance_method(:name).bind(constant).call
508
527
  rbi_name = constant_name.underscore + ".rbi"
509
- filename = config.outpath / rbi_name
528
+ filename = outpath / rbi_name
510
529
 
511
530
  out = String.new
512
531
  out << rbi_header(
513
- command,
514
- reason: "dynamic methods in `#{constant.name}`"
532
+ "#{Config::DEFAULT_COMMAND} dsl #{constant_name}",
533
+ reason: "dynamic methods in `#{constant_name}`"
515
534
  )
516
535
  out << contents
517
536
 
518
537
  FileUtils.mkdir_p(File.dirname(filename))
519
538
  File.write(filename, out)
520
- say("Wrote: ", [:green])
521
- say(filename)
539
+
540
+ unless quiet
541
+ say("Wrote: ", [:green])
542
+ say(filename)
543
+ end
522
544
 
523
545
  filename
524
546
  end
547
+
548
+ sig { params(tmp_dir: Pathname).returns(T::Hash[String, Symbol]) }
549
+ def verify_dsl_rbi(tmp_dir:)
550
+ diff = {}
551
+
552
+ existing_rbis = rbi_files_in(config.outpath)
553
+ new_rbis = rbi_files_in(tmp_dir)
554
+
555
+ added_files = (new_rbis - existing_rbis)
556
+
557
+ added_files.each do |file|
558
+ diff[file] = :added
559
+ end
560
+
561
+ removed_files = (existing_rbis - new_rbis)
562
+
563
+ removed_files.each do |file|
564
+ diff[file] = :removed
565
+ end
566
+
567
+ common_files = (existing_rbis & new_rbis)
568
+
569
+ changed_files = common_files.map do |filename|
570
+ filename unless FileUtils.identical?(config.outpath / filename, tmp_dir / filename)
571
+ end.compact
572
+
573
+ changed_files.each do |file|
574
+ diff[file] = :changed
575
+ end
576
+
577
+ diff
578
+ end
579
+
580
+ sig { params(cause: Symbol, files: T::Array[String]).returns(String) }
581
+ def build_error_for_files(cause, files)
582
+ filenames = files.map do |file|
583
+ config.outpath / file
584
+ end.join("\n - ")
585
+
586
+ " File(s) #{cause}:\n - #{filenames}"
587
+ end
588
+
589
+ sig { params(path: Pathname).returns(T::Array[Pathname]) }
590
+ def rbi_files_in(path)
591
+ Pathname.glob(path / "**/*.rbi").map do |file|
592
+ file.relative_path_from(path)
593
+ end.sort
594
+ end
595
+
596
+ sig { params(dir: Pathname).void }
597
+ def perform_dsl_verification(dir)
598
+ diff = verify_dsl_rbi(tmp_dir: dir)
599
+
600
+ if diff.empty?
601
+ say("Nothing to do, all RBIs are up-to-date.")
602
+ else
603
+ say("RBI files are out-of-date, please run:")
604
+ say(" `#{Config::DEFAULT_COMMAND} dsl`")
605
+
606
+ say("")
607
+
608
+ say("Reason:", [:red])
609
+ diff.group_by(&:last).sort.each do |cause, diff_for_cause|
610
+ say(build_error_for_files(cause, diff_for_cause.map(&:first)))
611
+ end
612
+
613
+ exit(1)
614
+ end
615
+ ensure
616
+ FileUtils.remove_entry(dir)
617
+ end
618
+
619
+ sig { params(files: T::Set[Pathname]).void }
620
+ def purge_stale_dsl_rbi_files(files)
621
+ if files.any?
622
+ say("Removing stale RBI files...")
623
+
624
+ files.sort.each do |filename|
625
+ remove(filename)
626
+ end
627
+ say("")
628
+ end
629
+ end
525
630
  end
526
631
  end