tapioca 0.4.16 → 0.4.21

Sign up to get free protection for your applications and to get access to all the features.
@@ -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