tapioca 0.3.0 → 0.4.3

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +25 -1
  3. data/README.md +23 -2
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +8 -2
  6. data/lib/tapioca/cli.rb +31 -2
  7. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +129 -0
  8. data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
  9. data/lib/tapioca/compilers/dsl/active_record_associations.rb +285 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +387 -0
  11. data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
  12. data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +213 -0
  13. data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
  14. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +170 -0
  15. data/lib/tapioca/compilers/dsl/active_resource.rb +140 -0
  16. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +126 -0
  17. data/lib/tapioca/compilers/dsl/base.rb +165 -0
  18. data/lib/tapioca/compilers/dsl/frozen_record.rb +96 -0
  19. data/lib/tapioca/compilers/dsl/protobuf.rb +144 -0
  20. data/lib/tapioca/compilers/dsl/smart_properties.rb +173 -0
  21. data/lib/tapioca/compilers/dsl/state_machines.rb +378 -0
  22. data/lib/tapioca/compilers/dsl/url_helpers.rb +92 -0
  23. data/lib/tapioca/compilers/dsl_compiler.rb +121 -0
  24. data/lib/tapioca/compilers/requires_compiler.rb +67 -0
  25. data/lib/tapioca/compilers/sorbet.rb +34 -0
  26. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +223 -35
  27. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +3 -17
  28. data/lib/tapioca/compilers/todos_compiler.rb +32 -0
  29. data/lib/tapioca/config.rb +14 -6
  30. data/lib/tapioca/config_builder.rb +22 -9
  31. data/lib/tapioca/constant_locator.rb +1 -0
  32. data/lib/tapioca/core_ext/class.rb +23 -0
  33. data/lib/tapioca/gemfile.rb +32 -9
  34. data/lib/tapioca/generator.rb +231 -23
  35. data/lib/tapioca/loader.rb +30 -9
  36. data/lib/tapioca/sorbet_config_parser.rb +77 -0
  37. data/lib/tapioca/version.rb +1 -1
  38. metadata +31 -51
@@ -0,0 +1,92 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "rails"
8
+ require "action_controller"
9
+ require "action_view"
10
+ rescue LoadError
11
+ return
12
+ end
13
+
14
+ module Tapioca
15
+ module Compilers
16
+ module Dsl
17
+ class UrlHelpers < Base
18
+ extend T::Sig
19
+
20
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
21
+ def decorate(root, constant)
22
+ case constant
23
+ when GeneratedPathHelpersModule.singleton_class, GeneratedUrlHelpersModule.singleton_class
24
+ generate_module_for(root, constant)
25
+ else
26
+ root.path(constant) do |mod|
27
+ create_mixins_for(mod, constant, GeneratedUrlHelpersModule)
28
+ create_mixins_for(mod, constant, GeneratedPathHelpersModule)
29
+ end
30
+ end
31
+ end
32
+
33
+ NON_DISCOVERABLE_INCLUDERS = T.let([
34
+ ActionDispatch::IntegrationTest,
35
+ ActionView::Helpers,
36
+ ], T::Array[Module])
37
+
38
+ sig { override.returns(T::Enumerable[Module]) }
39
+ def gather_constants
40
+ Object.const_set(:GeneratedUrlHelpersModule, Rails.application.routes.named_routes.url_helpers_module)
41
+ Object.const_set(:GeneratedPathHelpersModule, Rails.application.routes.named_routes.path_helpers_module)
42
+
43
+ module_enumerator = T.cast(ObjectSpace.each_object(Module), T::Enumerator[Module])
44
+ constants = module_enumerator.select do |mod|
45
+ next unless Module.instance_method(:name).bind(mod).call
46
+
47
+ includes_helper?(mod, GeneratedUrlHelpersModule) ||
48
+ includes_helper?(mod, GeneratedPathHelpersModule) ||
49
+ includes_helper?(mod.singleton_class, GeneratedUrlHelpersModule) ||
50
+ includes_helper?(mod.singleton_class, GeneratedPathHelpersModule)
51
+ end
52
+
53
+ constants.concat(NON_DISCOVERABLE_INCLUDERS)
54
+ end
55
+
56
+ private
57
+
58
+ sig { params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
59
+ def generate_module_for(root, constant)
60
+ root.create_module(T.must(constant.name)) do |mod|
61
+ mod.create_include("ActionDispatch::Routing::UrlFor")
62
+ mod.create_include("ActionDispatch::Routing::PolymorphicRoutes")
63
+
64
+ constant.instance_methods(false).each do |method|
65
+ mod.create_method(
66
+ method.to_s,
67
+ parameters: [Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped")],
68
+ return_type: "String"
69
+ )
70
+ end
71
+ end
72
+ end
73
+
74
+ sig { params(mod: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module), helper_module: Module).void }
75
+ def create_mixins_for(mod, constant, helper_module)
76
+ include_helper = constant.ancestors.include?(helper_module) || NON_DISCOVERABLE_INCLUDERS.include?(constant)
77
+ extend_helper = constant.singleton_class.ancestors.include?(helper_module)
78
+
79
+ mod.create_include(T.must(helper_module.name)) if include_helper
80
+ mod.create_extend(T.must(helper_module.name)) if extend_helper
81
+ end
82
+
83
+ sig { params(mod: Module, helper: Module).returns(T::Boolean) }
84
+ def includes_helper?(mod, helper)
85
+ superclass_ancestors = mod.superclass&.ancestors if Class === mod
86
+ superclass_ancestors ||= []
87
+ (mod.ancestors - superclass_ancestors).include?(helper)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ require "tapioca/compilers/dsl/base"
5
+
6
+ module Tapioca
7
+ module Compilers
8
+ class DslCompiler
9
+ extend T::Sig
10
+
11
+ sig { returns(T::Enumerable[Dsl::Base]) }
12
+ attr_reader :generators
13
+
14
+ sig { returns(T::Array[Module]) }
15
+ attr_reader :requested_constants
16
+
17
+ sig { returns(T.proc.params(error: String).void) }
18
+ attr_reader :error_handler
19
+
20
+ sig do
21
+ params(
22
+ requested_constants: T::Array[Module],
23
+ requested_generators: T::Array[String],
24
+ error_handler: T.nilable(T.proc.params(error: String).void)
25
+ ).void
26
+ end
27
+ def initialize(requested_constants:, requested_generators: [], error_handler: nil)
28
+ @generators = T.let(
29
+ gather_generators(requested_generators),
30
+ T::Enumerable[Dsl::Base]
31
+ )
32
+ @requested_constants = requested_constants
33
+ @error_handler = error_handler || $stderr.method(:puts)
34
+ end
35
+
36
+ sig { params(blk: T.proc.params(constant: Module, rbi: String).void).void }
37
+ def run(&blk)
38
+ constants_to_process = gather_constants(requested_constants)
39
+
40
+ if constants_to_process.empty?
41
+ report_error(<<~ERROR)
42
+ No classes/modules can be matched for RBI generation.
43
+ Please check that the requested classes/modules include processable DSL methods.
44
+ ERROR
45
+ end
46
+
47
+ constants_to_process.sort_by { |c| c.name.to_s }.each do |constant|
48
+ rbi = rbi_for_constant(constant)
49
+ next if rbi.nil?
50
+
51
+ blk.call(constant, rbi)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ sig { params(requested_generators: T::Array[String]).returns(Proc) }
58
+ def generator_filter(requested_generators)
59
+ return proc { true } if requested_generators.empty?
60
+
61
+ generators = requested_generators.map(&:downcase)
62
+
63
+ proc do |klass|
64
+ generator = klass.name&.sub(/^Tapioca::Compilers::Dsl::/, '')&.downcase
65
+ generators.include?(generator)
66
+ end
67
+ end
68
+
69
+ sig { params(requested_generators: T::Array[String]).returns(T::Enumerable[Dsl::Base]) }
70
+ def gather_generators(requested_generators)
71
+ generator_filter = generator_filter(requested_generators)
72
+
73
+ Dsl::Base.descendants.select(&generator_filter).map(&:new)
74
+ end
75
+
76
+ sig { params(requested_constants: T::Array[Module]).returns(T::Set[Module]) }
77
+ def gather_constants(requested_constants)
78
+ constants = generators.map(&:processable_constants).reduce(Set.new, :union)
79
+ constants &= requested_constants unless requested_constants.empty?
80
+ constants
81
+ end
82
+
83
+ sig { params(constant: Module).returns(T.nilable(String)) }
84
+ def rbi_for_constant(constant)
85
+ parlour = Parlour::RbiGenerator.new(sort_namespaces: true)
86
+
87
+ generators.each do |generator|
88
+ next unless generator.handles?(constant)
89
+ generator.decorate(parlour.root, constant)
90
+ end
91
+
92
+ return if parlour.root.children.empty?
93
+
94
+ resolve_conflicts(parlour)
95
+
96
+ parlour.rbi("true").strip
97
+ end
98
+
99
+ sig { params(parlour: Parlour::RbiGenerator).void }
100
+ def resolve_conflicts(parlour)
101
+ Parlour::ConflictResolver.new.resolve_conflicts(parlour.root) do |msg, candidates|
102
+ error = StringIO.new
103
+ error.puts "=== Error ==="
104
+ error.puts msg
105
+ error.puts "# Candidates"
106
+ candidates.each_with_index do |candidate, index|
107
+ error.puts " #{index}. #{candidate.describe}"
108
+ end
109
+ report_error(error.string)
110
+ end
111
+ end
112
+
113
+ sig { params(error: String).returns(T.noreturn) }
114
+ def report_error(error)
115
+ handler = error_handler
116
+ handler.call(error)
117
+ exit(1)
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require_relative '../sorbet_config_parser'
5
+
6
+ module Tapioca
7
+ module Compilers
8
+ class RequiresCompiler
9
+ extend T::Sig
10
+
11
+ sig { params(sorbet_path: String).void }
12
+ def initialize(sorbet_path)
13
+ @sorbet_path = sorbet_path
14
+ end
15
+
16
+ sig { returns(String) }
17
+ def compile
18
+ config = SorbetConfig.parse_file(@sorbet_path)
19
+ files = collect_files(config)
20
+ files.flat_map do |file|
21
+ collect_requires(file).reject do |req|
22
+ name_in_project?(files, req)
23
+ end
24
+ end.sort.uniq.map do |name|
25
+ "require '#{name}'\n"
26
+ end.join
27
+ end
28
+
29
+ private
30
+
31
+ sig { params(config: SorbetConfig).returns(T::Array[String]) }
32
+ def collect_files(config)
33
+ config.paths.flat_map do |path|
34
+ path = (Pathname.new(@sorbet_path) / "../.." / path).cleanpath
35
+ if path.directory?
36
+ Dir.glob("#{path}/**/*.rb", File::FNM_EXTGLOB).reject do |file|
37
+ file_ignored_by_sorbet?(config, file)
38
+ end
39
+ else
40
+ [path.to_s]
41
+ end
42
+ end.sort.uniq
43
+ end
44
+
45
+ sig { params(file_path: String).returns(T::Enumerable[String]) }
46
+ def collect_requires(file_path)
47
+ File.read(file_path).lines.map do |line|
48
+ /^\s*require\s*(\(\s*)?['"](?<name>[^'"]+)['"](\s*\))?/.match(line) { |m| m["name"] }
49
+ end.compact
50
+ end
51
+
52
+ sig { params(config: SorbetConfig, file: String).returns(T::Boolean) }
53
+ def file_ignored_by_sorbet?(config, file)
54
+ config.ignore.any? do |path|
55
+ Regexp.new(Regexp.escape(path)) =~ file
56
+ end
57
+ end
58
+
59
+ sig { params(files: T::Enumerable[String], name: String).returns(T::Boolean) }
60
+ def name_in_project?(files, name)
61
+ files.any? do |file|
62
+ File.basename(file, '.rb') == name
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ require 'pathname'
5
+ require 'shellwords'
6
+
7
+ module Tapioca
8
+ module Compilers
9
+ module Sorbet
10
+ SORBET = Pathname.new(Gem::Specification.find_by_name("sorbet-static").full_gem_path) / "libexec" / "sorbet"
11
+
12
+ class << self
13
+ extend(T::Sig)
14
+
15
+ sig { params(args: String).returns(String) }
16
+ def run(*args)
17
+ IO.popen(
18
+ [
19
+ sorbet_path,
20
+ "--quiet",
21
+ *args,
22
+ ].join(' '),
23
+ err: "/dev/null"
24
+ ).read
25
+ end
26
+
27
+ sig { returns(String) }
28
+ def sorbet_path
29
+ SORBET.to_s.shellescape
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -29,7 +29,7 @@ module Tapioca
29
29
  def generate
30
30
  symbols
31
31
  .sort
32
- .map(&method(:generate_from_symbol))
32
+ .map { |symbol| generate_from_symbol(symbol) }
33
33
  .compact
34
34
  .join("\n\n")
35
35
  .concat("\n")
@@ -176,12 +176,43 @@ module Tapioca
176
176
  return if symbol_ignored?(name) && methods.nil?
177
177
 
178
178
  [
179
+ compile_module_helpers(constant),
179
180
  compile_mixins(constant),
181
+ compile_mixes_in_class_methods(constant),
182
+ compile_props(constant),
180
183
  methods,
181
184
  ].select { |b| b != "" }.join("\n\n")
182
185
  end
183
186
  end
184
187
 
188
+ sig { params(constant: Module).returns(String) }
189
+ def compile_module_helpers(constant)
190
+ abstract_type = T::Private::Abstract::Data.get(constant, :abstract_type)
191
+
192
+ if abstract_type
193
+ indented("#{abstract_type}!")
194
+ elsif T::Private::Final.final_module?(constant)
195
+ indented("final!")
196
+ elsif T::Private::Sealed.sealed_module?(constant)
197
+ indented("sealed!")
198
+ else
199
+ ""
200
+ end
201
+ end
202
+
203
+ sig { params(constant: Module).returns(String) }
204
+ def compile_props(constant)
205
+ return "" unless T::Props::ClassMethods === constant
206
+
207
+ constant.props.map do |name, prop|
208
+ method = "prop"
209
+ method = "const" if prop.fetch(:immutable, false)
210
+ type = prop.fetch(:type_object, "T.untyped")
211
+
212
+ indented("#{method} :#{name}, #{type}")
213
+ end.join("\n")
214
+ end
215
+
185
216
  sig { params(name: String, constant: Module).returns(T.nilable(String)) }
186
217
  def compile_subconstants(name, constant)
187
218
  output = constants_of(constant).sort.uniq.map do |constant_name|
@@ -286,6 +317,7 @@ module Tapioca
286
317
  end
287
318
 
288
319
  prepends = prepend
320
+ .reverse
289
321
  .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
290
322
  .select(&method(:public_module?))
291
323
  .map do |mod|
@@ -296,6 +328,7 @@ module Tapioca
296
328
  end
297
329
 
298
330
  includes = include
331
+ .reverse
299
332
  .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
300
333
  .select(&method(:public_module?))
301
334
  .map do |mod|
@@ -303,23 +336,68 @@ module Tapioca
303
336
  end
304
337
 
305
338
  extends = extend
339
+ .reverse
306
340
  .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
307
341
  .select(&method(:public_module?))
308
342
  .map do |mod|
309
343
  indented("extend(#{qualified_name_of(mod)})")
310
344
  end
311
345
 
312
- mixes_class_methods = extend
313
- .select do |mod|
314
- qualified_name_of(mod) == "::ActiveSupport::Concern" &&
315
- Module === resolve_constant("#{name_of(constant)}::ClassMethods")
346
+ (prepends + includes + extends).join("\n")
347
+ end
348
+
349
+ sig { params(constant: Module).returns(String) }
350
+ def compile_mixes_in_class_methods(constant)
351
+ return "" if constant.is_a?(Class)
352
+
353
+ mixins_from_modules = {}
354
+
355
+ Class.new do
356
+ # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
357
+ def method_missing(symbol, *args)
316
358
  end
317
- .first(1)
318
- .flat_map do
319
- ["", indented("mixes_in_class_methods(ClassMethods)")]
359
+
360
+ define_singleton_method(:include) do |mod|
361
+ before = singleton_class.ancestors
362
+ super(mod).tap do
363
+ mixins_from_modules[mod] = singleton_class.ancestors - before
364
+ end
320
365
  end
321
366
 
322
- (prepends + includes + extends + mixes_class_methods).join("\n")
367
+ class << self
368
+ def method_missing(symbol, *args)
369
+ end
370
+ end
371
+ # rubocop:enable Style/MethodMissingSuper, Style/MissingRespondToMissing
372
+ end.include(constant)
373
+
374
+ all_dynamic_extends = mixins_from_modules.delete(constant)
375
+ all_dynamic_includes = mixins_from_modules.keys
376
+ dynamic_extends_from_dynamic_includes = mixins_from_modules.values.flatten
377
+ dynamic_extends = all_dynamic_extends - dynamic_extends_from_dynamic_includes
378
+
379
+ result = all_dynamic_includes
380
+ .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
381
+ .select(&method(:public_module?))
382
+ .map do |mod|
383
+ indented("include(#{qualified_name_of(mod)})")
384
+ end.join("\n")
385
+
386
+ mixed_in_module = dynamic_extends.find do |mod|
387
+ mod != constant && public_module?(mod)
388
+ end
389
+
390
+ return result if mixed_in_module.nil?
391
+
392
+ qualified_name = qualified_name_of(mixed_in_module)
393
+ return result if qualified_name == ""
394
+
395
+ [
396
+ result,
397
+ indented("mixes_in_class_methods(#{qualified_name})"),
398
+ ].select { |b| b != "" }.join("\n\n")
399
+ rescue
400
+ ""
323
401
  end
324
402
 
325
403
  sig { params(name: String, constant: Module).returns(T.nilable(String)) }
@@ -331,7 +409,7 @@ module Tapioca
331
409
  )
332
410
 
333
411
  instance_methods = compile_directly_owned_methods(name, constant)
334
- singleton_methods = compile_directly_owned_methods(name, singleton_class_of(constant), [:public])
412
+ singleton_methods = compile_directly_owned_methods(name, singleton_class_of(constant))
335
413
 
336
414
  return if symbol_ignored?(name) && instance_methods.empty? && singleton_methods.empty?
337
415
 
@@ -344,24 +422,44 @@ module Tapioca
344
422
 
345
423
  sig { params(module_name: String, mod: Module, for_visibility: T::Array[Symbol]).returns(String) }
346
424
  def compile_directly_owned_methods(module_name, mod, for_visibility = [:public, :protected, :private])
347
- method_names_by_visibility(mod)
348
- .delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
349
- .flat_map do |visibility, method_list|
350
- compiled = method_list.sort!.map do |name|
351
- next if name == :initialize
352
- compile_method(module_name, mod, mod.instance_method(name))
353
- end
354
- compiled.compact!
425
+ indent_step = 0
426
+ preamble = nil
427
+ postamble = nil
428
+
429
+ if mod.singleton_class?
430
+ indent_step = 1
431
+ preamble = indented("class << self")
432
+ postamble = indented("end")
433
+ end
355
434
 
356
- unless compiled.empty? || visibility == :public
357
- # add visibility badge
358
- compiled.unshift('', indented(visibility.to_s), '')
435
+ methods = with_indentation(indent_step) do
436
+ method_names_by_visibility(mod)
437
+ .delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
438
+ .flat_map do |visibility, method_list|
439
+ compiled = method_list.sort!.map do |name|
440
+ next if name == :initialize
441
+ compile_method(module_name, mod, mod.instance_method(name))
442
+ end
443
+ compiled.compact!
444
+
445
+ unless compiled.empty? || visibility == :public
446
+ # add visibility badge
447
+ compiled.unshift('', indented(visibility.to_s), '')
448
+ end
449
+
450
+ compiled
359
451
  end
452
+ .compact
453
+ .join("\n")
454
+ end
360
455
 
361
- compiled
362
- end
363
- .compact
364
- .join("\n")
456
+ return "" if methods.strip == ""
457
+
458
+ [
459
+ preamble,
460
+ methods,
461
+ postamble,
462
+ ].compact.join("\n")
365
463
  end
366
464
 
367
465
  sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
@@ -373,6 +471,16 @@ module Tapioca
373
471
  }
374
472
  end
375
473
 
474
+ sig { params(constant: Module, method_name: String).returns(T::Boolean) }
475
+ def struct_method?(constant, method_name)
476
+ return false unless T::Props::ClassMethods === constant
477
+
478
+ constant
479
+ .props
480
+ .keys
481
+ .include?(method_name.gsub(/=$/, '').to_sym)
482
+ end
483
+
376
484
  sig do
377
485
  params(
378
486
  symbol_name: String,
@@ -385,24 +493,52 @@ module Tapioca
385
493
  return unless method.owner == constant
386
494
  return if symbol_ignored?(symbol_name) && !method_in_gem?(method)
387
495
 
496
+ signature = signature_of(method)
497
+ method = signature.method if signature
498
+
388
499
  method_name = method.name.to_s
389
500
  return unless valid_method_name?(method_name)
501
+ return if struct_method?(constant, method_name)
502
+ return if method_name.start_with?("__t_props_generated_")
390
503
 
391
504
  params = T.let(method.parameters, T::Array[T::Array[Symbol]])
392
505
  parameters = params.map do |(type, name)|
393
- name ||= :_
506
+ unless name
507
+ # For attr_writer methods, Sorbet signatures have the name
508
+ # of the method (without the trailing = sign) as the name of
509
+ # the only parameter. So, if the parameter does not have a name
510
+ # then the replacement name should be the name of the method
511
+ # (minus trailing =) if and only if there is a signature for the
512
+ # method and the parameter is required and there is a single
513
+ # parameter and the signature also defines a single parameter and
514
+ # the name of the method ends with a = character.
515
+ writer_method_with_sig = signature &&
516
+ type == :req &&
517
+ params.size == 1 &&
518
+ signature.arg_types.size == 1 &&
519
+ method_name[-1] == "="
520
+
521
+ name = if writer_method_with_sig
522
+ method_name[0...-1].to_sym
523
+ else
524
+ :_
525
+ end
526
+ end
527
+
528
+ # Sanitize param names
529
+ name = name.to_s.gsub(/[^a-zA-Z0-9_]/, '_')
394
530
 
395
531
  case type
396
532
  when :req
397
- name.to_s
533
+ name
398
534
  when :opt
399
- "#{name} = _"
535
+ "#{name} = T.unsafe(nil)"
400
536
  when :rest
401
537
  "*#{name}"
402
538
  when :keyreq
403
539
  "#{name}:"
404
540
  when :key
405
- "#{name}: _"
541
+ "#{name}: T.unsafe(nil)"
406
542
  when :keyrest
407
543
  "**#{name}"
408
544
  when :block
@@ -410,10 +546,54 @@ module Tapioca
410
546
  end
411
547
  end.join(', ')
412
548
 
413
- method_name = "#{'self.' if constant.singleton_class?}#{method_name}"
414
549
  parameters = "(#{parameters})" if parameters != ""
415
550
 
416
- indented("def #{method_name}#{parameters}; end")
551
+ signature_str = indented(compile_signature(signature)) if signature
552
+ [
553
+ signature_str,
554
+ indented("def #{method_name}#{parameters}; end"),
555
+ ].compact.join("\n")
556
+ end
557
+
558
+ TYPE_PARAMETER_MATCHER = /T\.type_parameter\(:?([[:word:]]+)\)/
559
+
560
+ sig { params(signature: T.untyped).returns(String) }
561
+ def compile_signature(signature)
562
+ params = signature.arg_types
563
+ params += signature.kwarg_types.to_a
564
+ params << [signature.rest_name, signature.rest_type] if signature.has_rest
565
+ params << [signature.block_name, signature.block_type] if signature.block_name
566
+
567
+ params = params.compact.map { |name, type| "#{name}: #{type}" }.join(", ")
568
+ returns = signature.return_type.to_s
569
+
570
+ type_parameters = (params + returns).scan(TYPE_PARAMETER_MATCHER).flatten.uniq.map { |p| ":#{p}" }.join(", ")
571
+ type_parameters = ".type_parameters(#{type_parameters})" unless type_parameters.empty?
572
+
573
+ mode = case signature.mode
574
+ when "abstract"
575
+ ".abstract"
576
+ when "override"
577
+ ".override"
578
+ when "overridable_override"
579
+ ".overridable.override"
580
+ when "overridable"
581
+ ".overridable"
582
+ else
583
+ ""
584
+ end
585
+
586
+ signature_body = +""
587
+ signature_body << mode
588
+ signature_body << type_parameters
589
+ signature_body << ".params(#{params})" unless params.empty?
590
+ signature_body << ".returns(#{returns})"
591
+ signature_body = signature_body
592
+ .gsub(".returns(<VOID>)", ".void")
593
+ .gsub("<NOT-TYPED>", "T.untyped")
594
+ .gsub(TYPE_PARAMETER_MATCHER, "T.type_parameter(:\\1)")[1..-1]
595
+
596
+ "sig { #{signature_body} }"
417
597
  end
418
598
 
419
599
  sig { params(symbol_name: String).returns(T::Boolean) }
@@ -432,16 +612,17 @@ module Tapioca
432
612
  sig do
433
613
  type_parameters(:U)
434
614
  .params(
615
+ step: Integer,
435
616
  _blk: T.proc
436
617
  .returns(T.type_parameter(:U))
437
618
  )
438
619
  .returns(T.type_parameter(:U))
439
620
  end
440
- def with_indentation(&_blk)
441
- @indent += 2
621
+ def with_indentation(step = 1, &_blk)
622
+ @indent += 2 * step
442
623
  yield
443
624
  ensure
444
- @indent -= 2
625
+ @indent -= 2 * step
445
626
  end
446
627
 
447
628
  sig { params(str: String).returns(String) }
@@ -576,7 +757,7 @@ module Tapioca
576
757
  return nil
577
758
  end
578
759
 
579
- name_of(target)
760
+ raw_name_of(target)
580
761
  end
581
762
 
582
763
  sig { params(constant: Module).returns(T.nilable(String)) }
@@ -596,6 +777,13 @@ module Tapioca
596
777
  Class.instance_method(:superclass).bind(constant).call
597
778
  end
598
779
 
780
+ sig { params(method: T.any(UnboundMethod, Method)).returns(T.untyped) }
781
+ def signature_of(method)
782
+ T::Private::Methods.signature_for_method(method)
783
+ rescue LoadError, StandardError
784
+ nil
785
+ end
786
+
599
787
  sig { params(constant: Module, other: BasicObject).returns(T::Boolean).checked(:never) }
600
788
  def are_equal?(constant, other)
601
789
  BasicObject.instance_method(:equal?).bind(constant).call(other)