tapioca 0.3.0 → 0.4.3

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