tapioca 0.2.7 → 0.4.1

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 +27 -1
  3. data/README.md +21 -2
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +15 -9
  6. data/lib/tapioca/cli.rb +41 -12
  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 +379 -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 +163 -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 +83 -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 +209 -49
  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 +42 -0
  30. data/lib/tapioca/config_builder.rb +75 -0
  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 +14 -1
  34. data/lib/tapioca/generator.rb +235 -67
  35. data/lib/tapioca/loader.rb +20 -9
  36. data/lib/tapioca/sorbet_config_parser.rb +77 -0
  37. data/lib/tapioca/version.rb +1 -1
  38. metadata +35 -66
@@ -0,0 +1,83 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "rails"
8
+ require "action_controller"
9
+ rescue LoadError
10
+ return
11
+ end
12
+
13
+ module Tapioca
14
+ module Compilers
15
+ module Dsl
16
+ class UrlHelpers < Base
17
+ extend T::Sig
18
+
19
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
20
+ def decorate(root, constant)
21
+ case constant
22
+ when GeneratedPathHelpersModule.singleton_class, GeneratedUrlHelpersModule.singleton_class
23
+ generate_module_for(root, constant)
24
+ else
25
+ root.path(constant) do |mod|
26
+ create_mixins_for(mod, constant, GeneratedUrlHelpersModule)
27
+ create_mixins_for(mod, constant, GeneratedPathHelpersModule)
28
+ end
29
+ end
30
+ end
31
+
32
+ sig { override.returns(T::Enumerable[T.untyped]) }
33
+ def gather_constants
34
+ Object.const_set(:GeneratedUrlHelpersModule, Rails.application.routes.named_routes.url_helpers_module)
35
+ Object.const_set(:GeneratedPathHelpersModule, Rails.application.routes.named_routes.path_helpers_module)
36
+
37
+ constants = ObjectSpace.each_object(Module).select do |mod|
38
+ mod = T.cast(mod, T.class_of(Module))
39
+ next unless Module.instance_method(:name).bind(mod).call
40
+
41
+ includes_helper?(mod, GeneratedUrlHelpersModule) ||
42
+ includes_helper?(mod, GeneratedPathHelpersModule) ||
43
+ includes_helper?(mod.singleton_class, GeneratedUrlHelpersModule) ||
44
+ includes_helper?(mod.singleton_class, GeneratedPathHelpersModule)
45
+ end
46
+
47
+ constants << ActionDispatch::IntegrationTest
48
+ end
49
+
50
+ private
51
+
52
+ sig { params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
53
+ def generate_module_for(root, constant)
54
+ root.create_module(T.must(constant.name)) do |mod|
55
+ mod.create_include("ActionDispatch::Routing::UrlFor")
56
+ mod.create_include("ActionDispatch::Routing::PolymorphicRoutes")
57
+
58
+ constant.instance_methods(false).each do |method|
59
+ mod.create_method(
60
+ method.to_s,
61
+ parameters: [Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped")],
62
+ return_type: "String"
63
+ )
64
+ end
65
+ end
66
+ end
67
+
68
+ sig { params(mod: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module), helper_module: Module).void }
69
+ def create_mixins_for(mod, constant, helper_module)
70
+ mod.create_include(T.must(helper_module.name)) if constant.ancestors.include?(helper_module)
71
+ mod.create_extend(T.must(helper_module.name)) if constant.singleton_class.ancestors.include?(helper_module)
72
+ end
73
+
74
+ sig { params(mod: Module, helper: Module).returns(T::Boolean) }
75
+ def includes_helper?(mod, helper)
76
+ superclass_ancestors = mod.superclass&.ancestors if Class === mod
77
+ superclass_ancestors ||= []
78
+ (mod.ancestors - superclass_ancestors).include?(helper)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ 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
@@ -137,14 +137,11 @@ module Tapioca
137
137
  end
138
138
  def compile_object(name, value)
139
139
  return if symbol_ignored?(name)
140
- indented("#{name} = T.let(T.unsafe(nil), #{type_name_of(value)})")
141
- end
142
-
143
- sig { params(value: BasicObject).returns(String).checked(:never) }
144
- def type_name_of(value)
145
140
  klass = class_of(value)
141
+ return if name_of(klass)&.start_with?("T::Types::", "T::Private::")
146
142
 
147
- public_module?(klass) && name_of(klass) || "T.untyped"
143
+ type_name = public_module?(klass) && name_of(klass) || "T.untyped"
144
+ indented("#{name} = T.let(T.unsafe(nil), #{type_name})")
148
145
  end
149
146
 
150
147
  sig { params(name: String, constant: Module).returns(T.nilable(String)) }
@@ -179,12 +176,43 @@ module Tapioca
179
176
  return if symbol_ignored?(name) && methods.nil?
180
177
 
181
178
  [
179
+ compile_module_helpers(constant),
182
180
  compile_mixins(constant),
181
+ compile_mixes_in_class_methods(constant),
182
+ compile_props(constant),
183
183
  methods,
184
184
  ].select { |b| b != "" }.join("\n\n")
185
185
  end
186
186
  end
187
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
+
188
216
  sig { params(name: String, constant: Module).returns(T.nilable(String)) }
189
217
  def compile_subconstants(name, constant)
190
218
  output = constants_of(constant).sort.uniq.map do |constant_name|
@@ -289,7 +317,8 @@ module Tapioca
289
317
  end
290
318
 
291
319
  prepends = prepend
292
- .select(&method(:name_of))
320
+ .reverse
321
+ .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
293
322
  .select(&method(:public_module?))
294
323
  .map do |mod|
295
324
  # TODO: Sorbet currently does not handle prepend
@@ -299,30 +328,76 @@ module Tapioca
299
328
  end
300
329
 
301
330
  includes = include
302
- .select(&method(:name_of))
331
+ .reverse
332
+ .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
303
333
  .select(&method(:public_module?))
304
334
  .map do |mod|
305
335
  indented("include(#{qualified_name_of(mod)})")
306
336
  end
307
337
 
308
338
  extends = extend
309
- .select(&method(:name_of))
339
+ .reverse
340
+ .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
310
341
  .select(&method(:public_module?))
311
342
  .map do |mod|
312
343
  indented("extend(#{qualified_name_of(mod)})")
313
344
  end
314
345
 
315
- mixes_class_methods = extend
316
- .select do |mod|
317
- qualified_name_of(mod) == "::ActiveSupport::Concern" &&
318
- 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)
358
+ end
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
319
365
  end
320
- .first(1)
321
- .flat_map do
322
- ["", indented("mixes_in_class_methods(ClassMethods)")]
366
+
367
+ class << self
368
+ def method_missing(symbol, *args)
369
+ end
323
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
324
378
 
325
- (prepends + includes + extends + mixes_class_methods).join("\n")
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
+ ""
326
401
  end
327
402
 
328
403
  sig { params(name: String, constant: Module).returns(T.nilable(String)) }
@@ -334,7 +409,7 @@ module Tapioca
334
409
  )
335
410
 
336
411
  instance_methods = compile_directly_owned_methods(name, constant)
337
- singleton_methods = compile_directly_owned_methods(name, singleton_class_of(constant), [:public])
412
+ singleton_methods = compile_directly_owned_methods(name, singleton_class_of(constant))
338
413
 
339
414
  return if symbol_ignored?(name) && instance_methods.empty? && singleton_methods.empty?
340
415
 
@@ -347,24 +422,44 @@ module Tapioca
347
422
 
348
423
  sig { params(module_name: String, mod: Module, for_visibility: T::Array[Symbol]).returns(String) }
349
424
  def compile_directly_owned_methods(module_name, mod, for_visibility = [:public, :protected, :private])
350
- method_names_by_visibility(mod)
351
- .delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
352
- .flat_map do |visibility, method_list|
353
- compiled = method_list.sort!.map do |name|
354
- next if name == :initialize
355
- compile_method(module_name, mod, mod.instance_method(name))
356
- end
357
- 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
358
434
 
359
- unless compiled.empty? || visibility == :public
360
- # add visibility badge
361
- 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
362
451
  end
452
+ .compact
453
+ .join("\n")
454
+ end
363
455
 
364
- compiled
365
- end
366
- .compact
367
- .join("\n")
456
+ return "" if methods.strip == ""
457
+
458
+ [
459
+ preamble,
460
+ methods,
461
+ postamble,
462
+ ].compact.join("\n")
368
463
  end
369
464
 
370
465
  sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
@@ -376,6 +471,16 @@ module Tapioca
376
471
  }
377
472
  end
378
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
+
379
484
  sig do
380
485
  params(
381
486
  symbol_name: String,
@@ -388,24 +493,32 @@ module Tapioca
388
493
  return unless method.owner == constant
389
494
  return if symbol_ignored?(symbol_name) && !method_in_gem?(method)
390
495
 
496
+ signature = signature_of(method)
497
+ method = signature.method if signature
498
+
391
499
  method_name = method.name.to_s
392
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_")
393
503
 
394
504
  params = T.let(method.parameters, T::Array[T::Array[Symbol]])
395
505
  parameters = params.map do |(type, name)|
396
506
  name ||= :_
397
507
 
508
+ # Sanitize param names
509
+ name = name.to_s.gsub(/[^a-zA-Z0-9_]/, '_')
510
+
398
511
  case type
399
512
  when :req
400
- name.to_s
513
+ name
401
514
  when :opt
402
- "#{name} = _"
515
+ "#{name} = T.unsafe(nil)"
403
516
  when :rest
404
517
  "*#{name}"
405
518
  when :keyreq
406
519
  "#{name}:"
407
520
  when :key
408
- "#{name}: _"
521
+ "#{name}: T.unsafe(nil)"
409
522
  when :keyrest
410
523
  "**#{name}"
411
524
  when :block
@@ -413,10 +526,54 @@ module Tapioca
413
526
  end
414
527
  end.join(', ')
415
528
 
416
- method_name = "#{'self.' if constant.singleton_class?}#{method_name}"
417
529
  parameters = "(#{parameters})" if parameters != ""
418
530
 
419
- indented("def #{method_name}#{parameters}; end")
531
+ signature_str = indented(compile_signature(signature)) if signature
532
+ [
533
+ signature_str,
534
+ indented("def #{method_name}#{parameters}; end"),
535
+ ].compact.join("\n")
536
+ end
537
+
538
+ TYPE_PARAMETER_MATCHER = /T\.type_parameter\(:?([[:word:]]+)\)/
539
+
540
+ sig { params(signature: T.untyped).returns(String) }
541
+ def compile_signature(signature)
542
+ params = signature.arg_types
543
+ params += signature.kwarg_types.to_a
544
+ params << [signature.rest_name, signature.rest_type] if signature.has_rest
545
+ params << [signature.block_name, signature.block_type] if signature.block_name
546
+
547
+ params = params.compact.map { |name, type| "#{name}: #{type}" }.join(", ")
548
+ returns = signature.return_type.to_s
549
+
550
+ type_parameters = (params + returns).scan(TYPE_PARAMETER_MATCHER).flatten.uniq.map { |p| ":#{p}" }.join(", ")
551
+ type_parameters = ".type_parameters(#{type_parameters})" unless type_parameters.empty?
552
+
553
+ mode = case signature.mode
554
+ when "abstract"
555
+ ".abstract"
556
+ when "override"
557
+ ".override"
558
+ when "overridable_override"
559
+ ".overridable.override"
560
+ when "overridable"
561
+ ".overridable"
562
+ else
563
+ ""
564
+ end
565
+
566
+ signature_body = +""
567
+ signature_body << mode
568
+ signature_body << type_parameters
569
+ signature_body << ".params(#{params})" unless params.empty?
570
+ signature_body << ".returns(#{returns})"
571
+ signature_body = signature_body
572
+ .gsub(".returns(<VOID>)", ".void")
573
+ .gsub("<NOT-TYPED>", "T.untyped")
574
+ .gsub(TYPE_PARAMETER_MATCHER, "T.type_parameter(:\\1)")[1..-1]
575
+
576
+ "sig { #{signature_body} }"
420
577
  end
421
578
 
422
579
  sig { params(symbol_name: String).returns(T::Boolean) }
@@ -424,11 +581,6 @@ module Tapioca
424
581
  SymbolLoader.ignore_symbol?(symbol_name)
425
582
  end
426
583
 
427
- sig { params(path: String).returns(T::Boolean) }
428
- def path_in_gem?(path)
429
- path.start_with?(gem.full_gem_path)
430
- end
431
-
432
584
  SPECIAL_METHOD_NAMES = %w[! ~ +@ ** -@ * / % + - << >> & | ^ < <= => > >= == === != =~ !~ <=> [] []= `]
433
585
 
434
586
  sig { params(name: String).returns(T::Boolean) }
@@ -440,16 +592,17 @@ module Tapioca
440
592
  sig do
441
593
  type_parameters(:U)
442
594
  .params(
595
+ step: Integer,
443
596
  _blk: T.proc
444
597
  .returns(T.type_parameter(:U))
445
598
  )
446
599
  .returns(T.type_parameter(:U))
447
600
  end
448
- def with_indentation(&_blk)
449
- @indent += 2
601
+ def with_indentation(step = 1, &_blk)
602
+ @indent += 2 * step
450
603
  yield
451
604
  ensure
452
- @indent -= 2
605
+ @indent -= 2 * step
453
606
  end
454
607
 
455
608
  sig { params(str: String).returns(String) }
@@ -462,7 +615,7 @@ module Tapioca
462
615
  source_location = method.source_location&.first
463
616
  return false if source_location.nil?
464
617
 
465
- path_in_gem?(source_location)
618
+ gem.contains_path?(source_location)
466
619
  end
467
620
 
468
621
  sig { params(constant: Module, strict: T::Boolean).returns(T::Boolean) }
@@ -473,7 +626,7 @@ module Tapioca
473
626
  return !strict if files.empty?
474
627
 
475
628
  files.any? do |file|
476
- path_in_gem?(file)
629
+ gem.contains_path?(file)
477
630
  end
478
631
  end
479
632
 
@@ -584,7 +737,7 @@ module Tapioca
584
737
  return nil
585
738
  end
586
739
 
587
- name_of(target)
740
+ raw_name_of(target)
588
741
  end
589
742
 
590
743
  sig { params(constant: Module).returns(T.nilable(String)) }
@@ -604,6 +757,13 @@ module Tapioca
604
757
  Class.instance_method(:superclass).bind(constant).call
605
758
  end
606
759
 
760
+ sig { params(method: T.any(UnboundMethod, Method)).returns(T.untyped) }
761
+ def signature_of(method)
762
+ T::Private::Methods.signature_for_method(method)
763
+ rescue LoadError, StandardError
764
+ nil
765
+ end
766
+
607
767
  sig { params(constant: Module, other: BasicObject).returns(T::Boolean).checked(:never) }
608
768
  def are_equal?(constant, other)
609
769
  BasicObject.instance_method(:equal?).bind(constant).call(other)