tapioca 0.4.0 → 0.4.5

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +26 -1
  3. data/README.md +16 -0
  4. data/Rakefile +16 -4
  5. data/lib/tapioca.rb +6 -2
  6. data/lib/tapioca/cli.rb +25 -3
  7. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +130 -0
  8. data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
  9. data/lib/tapioca/compilers/dsl/active_record_associations.rb +267 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +404 -0
  11. data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
  12. data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +212 -0
  13. data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
  14. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +168 -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 +160 -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/symbol_table/symbol_generator.rb +195 -32
  26. data/lib/tapioca/config.rb +11 -6
  27. data/lib/tapioca/config_builder.rb +19 -9
  28. data/lib/tapioca/constant_locator.rb +1 -0
  29. data/lib/tapioca/core_ext/class.rb +23 -0
  30. data/lib/tapioca/gemfile.rb +32 -9
  31. data/lib/tapioca/generator.rb +200 -24
  32. data/lib/tapioca/loader.rb +30 -9
  33. data/lib/tapioca/version.rb +1 -1
  34. metadata +31 -40
@@ -0,0 +1,160 @@
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
+ # `Tapioca::Compilers::Dsl::UrlHelpers` generates RBI files for classes that include or extend
18
+ # `Rails.application.routes.url_helpers`
19
+ # (see https://api.rubyonrails.org/v5.1.7/classes/ActionDispatch/Routing/UrlFor.html#module-ActionDispatch::Routing::UrlFor-label-URL+generation+for+named+routes).
20
+ #
21
+ # For example, with the following setup:
22
+ #
23
+ # ~~~rb
24
+ # # config/application.rb
25
+ # class Application < Rails::Application
26
+ # routes.draw do
27
+ # resource :index
28
+ # end
29
+ # end
30
+ # ~~~
31
+ #
32
+ # ~~~rb
33
+ # app/models/post.rb
34
+ # class Post
35
+ # include Rails.application.routes.url_helpers
36
+ # end
37
+ # ~~~
38
+ #
39
+ # this generator will produce the following RBI files:
40
+ #
41
+ # ~~~rbi
42
+ # # generated_path_helpers_module.rbi
43
+ # # typed: true
44
+ # module GeneratedPathHelpersModule
45
+ # include ActionDispatch::Routing::PolymorphicRoutes
46
+ # include ActionDispatch::Routing::UrlFor
47
+ #
48
+ # sig { params(args: T.untyped).returns(String) }
49
+ # def edit_index_path(*args); end
50
+ #
51
+ # sig { params(args: T.untyped).returns(String) }
52
+ # def index_path(*args); end
53
+ #
54
+ # sig { params(args: T.untyped).returns(String) }
55
+ # def new_index_path(*args); end
56
+ # end
57
+ # ~~~
58
+ #
59
+ # ~~~rbi
60
+ # # generated_url_helpers_module.rbi
61
+ # # typed: true
62
+ # module GeneratedUrlHelpersModule
63
+ # include ActionDispatch::Routing::PolymorphicRoutes
64
+ # include ActionDispatch::Routing::UrlFor
65
+ #
66
+ # sig { params(args: T.untyped).returns(String) }
67
+ # def edit_index_url(*args); end
68
+ #
69
+ # sig { params(args: T.untyped).returns(String) }
70
+ # def index_url(*args); end
71
+ #
72
+ # sig { params(args: T.untyped).returns(String) }
73
+ # def new_index_url(*args); end
74
+ # end
75
+ # ~~~
76
+ #
77
+ # ~~~rbi
78
+ # # post.rbi
79
+ # # typed: true
80
+ # class Post
81
+ # include GeneratedPathHelpersModule
82
+ # include GeneratedUrlHelpersModule
83
+ # end
84
+ # ~~~
85
+ class UrlHelpers < Base
86
+ extend T::Sig
87
+
88
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
89
+ def decorate(root, constant)
90
+ case constant
91
+ when GeneratedPathHelpersModule.singleton_class, GeneratedUrlHelpersModule.singleton_class
92
+ generate_module_for(root, constant)
93
+ else
94
+ root.path(constant) do |mod|
95
+ create_mixins_for(mod, constant, GeneratedUrlHelpersModule)
96
+ create_mixins_for(mod, constant, GeneratedPathHelpersModule)
97
+ end
98
+ end
99
+ end
100
+
101
+ NON_DISCOVERABLE_INCLUDERS = T.let([
102
+ ActionDispatch::IntegrationTest,
103
+ ActionView::Helpers,
104
+ ], T::Array[Module])
105
+
106
+ sig { override.returns(T::Enumerable[Module]) }
107
+ def gather_constants
108
+ Object.const_set(:GeneratedUrlHelpersModule, Rails.application.routes.named_routes.url_helpers_module)
109
+ Object.const_set(:GeneratedPathHelpersModule, Rails.application.routes.named_routes.path_helpers_module)
110
+
111
+ module_enumerator = T.cast(ObjectSpace.each_object(Module), T::Enumerator[Module])
112
+ constants = module_enumerator.select do |mod|
113
+ next unless Module.instance_method(:name).bind(mod).call
114
+
115
+ includes_helper?(mod, GeneratedUrlHelpersModule) ||
116
+ includes_helper?(mod, GeneratedPathHelpersModule) ||
117
+ includes_helper?(mod.singleton_class, GeneratedUrlHelpersModule) ||
118
+ includes_helper?(mod.singleton_class, GeneratedPathHelpersModule)
119
+ end
120
+
121
+ constants.concat(NON_DISCOVERABLE_INCLUDERS)
122
+ end
123
+
124
+ private
125
+
126
+ sig { params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
127
+ def generate_module_for(root, constant)
128
+ root.create_module(T.must(constant.name)) do |mod|
129
+ mod.create_include("ActionDispatch::Routing::UrlFor")
130
+ mod.create_include("ActionDispatch::Routing::PolymorphicRoutes")
131
+
132
+ constant.instance_methods(false).each do |method|
133
+ mod.create_method(
134
+ method.to_s,
135
+ parameters: [Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped")],
136
+ return_type: "String"
137
+ )
138
+ end
139
+ end
140
+ end
141
+
142
+ sig { params(mod: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module), helper_module: Module).void }
143
+ def create_mixins_for(mod, constant, helper_module)
144
+ include_helper = constant.ancestors.include?(helper_module) || NON_DISCOVERABLE_INCLUDERS.include?(constant)
145
+ extend_helper = constant.singleton_class.ancestors.include?(helper_module)
146
+
147
+ mod.create_include(T.must(helper_module.name)) if include_helper
148
+ mod.create_extend(T.must(helper_module.name)) if extend_helper
149
+ end
150
+
151
+ sig { params(mod: Module, helper: Module).returns(T::Boolean) }
152
+ def includes_helper?(mod, helper)
153
+ superclass_ancestors = mod.superclass&.ancestors if Class === mod
154
+ superclass_ancestors ||= []
155
+ (mod.ancestors - superclass_ancestors).include?(helper)
156
+ end
157
+ end
158
+ end
159
+ end
160
+ 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 'spoom'
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 = Spoom::Sorbet::Config.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: Spoom::Sorbet::Config).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: Spoom::Sorbet::Config, 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
@@ -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,13 +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),
180
181
  compile_mixes_in_class_methods(constant),
182
+ compile_props(constant),
181
183
  methods,
182
184
  ].select { |b| b != "" }.join("\n\n")
183
185
  end
184
186
  end
185
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
+
186
216
  sig { params(name: String, constant: Module).returns(T.nilable(String)) }
187
217
  def compile_subconstants(name, constant)
188
218
  output = constants_of(constant).sort.uniq.map do |constant_name|
@@ -353,8 +383,18 @@ module Tapioca
353
383
  indented("include(#{qualified_name_of(mod)})")
354
384
  end.join("\n")
355
385
 
356
- mixed_in_module = dynamic_extends.find do |mod|
357
- mod != constant && public_module?(mod)
386
+ ancestors = singleton_class_of(constant).ancestors
387
+ extends_as_concern = ancestors.any? do |mod|
388
+ qualified_name_of(mod) == "::ActiveSupport::Concern"
389
+ end
390
+ class_methods_module = resolve_constant("#{name_of(constant)}::ClassMethods")
391
+
392
+ mixed_in_module = if extends_as_concern && Module === class_methods_module
393
+ class_methods_module
394
+ else
395
+ dynamic_extends.find do |mod|
396
+ mod != constant && public_module?(mod)
397
+ end
358
398
  end
359
399
 
360
400
  return result if mixed_in_module.nil?
@@ -379,7 +419,7 @@ module Tapioca
379
419
  )
380
420
 
381
421
  instance_methods = compile_directly_owned_methods(name, constant)
382
- singleton_methods = compile_directly_owned_methods(name, singleton_class_of(constant), [:public])
422
+ singleton_methods = compile_directly_owned_methods(name, singleton_class_of(constant))
383
423
 
384
424
  return if symbol_ignored?(name) && instance_methods.empty? && singleton_methods.empty?
385
425
 
@@ -392,24 +432,44 @@ module Tapioca
392
432
 
393
433
  sig { params(module_name: String, mod: Module, for_visibility: T::Array[Symbol]).returns(String) }
394
434
  def compile_directly_owned_methods(module_name, mod, for_visibility = [:public, :protected, :private])
395
- method_names_by_visibility(mod)
396
- .delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
397
- .flat_map do |visibility, method_list|
398
- compiled = method_list.sort!.map do |name|
399
- next if name == :initialize
400
- compile_method(module_name, mod, mod.instance_method(name))
401
- end
402
- compiled.compact!
435
+ indent_step = 0
436
+ preamble = nil
437
+ postamble = nil
438
+
439
+ if mod.singleton_class?
440
+ indent_step = 1
441
+ preamble = indented("class << self")
442
+ postamble = indented("end")
443
+ end
403
444
 
404
- unless compiled.empty? || visibility == :public
405
- # add visibility badge
406
- compiled.unshift('', indented(visibility.to_s), '')
445
+ methods = with_indentation(indent_step) do
446
+ method_names_by_visibility(mod)
447
+ .delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
448
+ .flat_map do |visibility, method_list|
449
+ compiled = method_list.sort!.map do |name|
450
+ next if name == :initialize
451
+ compile_method(module_name, mod, mod.instance_method(name))
452
+ end
453
+ compiled.compact!
454
+
455
+ unless compiled.empty? || visibility == :public
456
+ # add visibility badge
457
+ compiled.unshift('', indented(visibility.to_s), '')
458
+ end
459
+
460
+ compiled
407
461
  end
462
+ .compact
463
+ .join("\n")
464
+ end
408
465
 
409
- compiled
410
- end
411
- .compact
412
- .join("\n")
466
+ return "" if methods.strip == ""
467
+
468
+ [
469
+ preamble,
470
+ methods,
471
+ postamble,
472
+ ].compact.join("\n")
413
473
  end
414
474
 
415
475
  sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
@@ -421,6 +481,16 @@ module Tapioca
421
481
  }
422
482
  end
423
483
 
484
+ sig { params(constant: Module, method_name: String).returns(T::Boolean) }
485
+ def struct_method?(constant, method_name)
486
+ return false unless T::Props::ClassMethods === constant
487
+
488
+ constant
489
+ .props
490
+ .keys
491
+ .include?(method_name.gsub(/=$/, '').to_sym)
492
+ end
493
+
424
494
  sig do
425
495
  params(
426
496
  symbol_name: String,
@@ -433,27 +503,58 @@ module Tapioca
433
503
  return unless method.owner == constant
434
504
  return if symbol_ignored?(symbol_name) && !method_in_gem?(method)
435
505
 
506
+ signature = signature_of(method)
507
+ method = T.let(signature.method, UnboundMethod) if signature
508
+
436
509
  method_name = method.name.to_s
437
510
  return unless valid_method_name?(method_name)
438
-
439
- params = T.let(method.parameters, T::Array[T::Array[Symbol]])
440
- parameters = params.map do |(type, name)|
441
- name ||= :_
511
+ return if struct_method?(constant, method_name)
512
+ return if method_name.start_with?("__t_props_generated_")
513
+
514
+ parameters = T.let(method.parameters, T::Array[[Symbol, T.nilable(Symbol)]])
515
+
516
+ sanitized_parameters = parameters.map do |type, name|
517
+ unless name
518
+ # For attr_writer methods, Sorbet signatures have the name
519
+ # of the method (without the trailing = sign) as the name of
520
+ # the only parameter. So, if the parameter does not have a name
521
+ # then the replacement name should be the name of the method
522
+ # (minus trailing =) if and only if there is a signature for the
523
+ # method and the parameter is required and there is a single
524
+ # parameter and the signature also defines a single parameter and
525
+ # the name of the method ends with a = character.
526
+ writer_method_with_sig = (
527
+ signature && type == :req &&
528
+ parameters.size == 1 &&
529
+ signature.arg_types.size == 1 &&
530
+ method_name[-1] == "="
531
+ )
532
+
533
+ name = if writer_method_with_sig
534
+ T.must(method_name[0...-1]).to_sym
535
+ else
536
+ :_
537
+ end
538
+ end
442
539
 
443
540
  # Sanitize param names
444
541
  name = name.to_s.gsub(/[^a-zA-Z0-9_]/, '_')
445
542
 
543
+ [type, name]
544
+ end
545
+
546
+ parameter_list = sanitized_parameters.map do |type, name|
446
547
  case type
447
548
  when :req
448
549
  name
449
550
  when :opt
450
- "#{name} = _"
551
+ "#{name} = T.unsafe(nil)"
451
552
  when :rest
452
553
  "*#{name}"
453
554
  when :keyreq
454
555
  "#{name}:"
455
556
  when :key
456
- "#{name}: _"
557
+ "#{name}: T.unsafe(nil)"
457
558
  when :keyrest
458
559
  "**#{name}"
459
560
  when :block
@@ -461,10 +562,59 @@ module Tapioca
461
562
  end
462
563
  end.join(', ')
463
564
 
464
- method_name = "#{'self.' if constant.singleton_class?}#{method_name}"
465
- parameters = "(#{parameters})" if parameters != ""
565
+ parameter_list = "(#{parameter_list})" if parameter_list != ""
566
+ signature_str = indented(compile_signature(signature, sanitized_parameters)) if signature
567
+
568
+ [
569
+ signature_str,
570
+ indented("def #{method_name}#{parameter_list}; end"),
571
+ ].compact.join("\n")
572
+ end
573
+
574
+ TYPE_PARAMETER_MATCHER = /T\.type_parameter\(:?([[:word:]]+)\)/
575
+
576
+ sig { params(signature: T.untyped, parameters: T::Array[[Symbol, String]]).returns(String) }
577
+ def compile_signature(signature, parameters)
578
+ parameter_types = T.let(signature.arg_types.to_h, T::Hash[Symbol, T::Types::Base])
579
+ parameter_types.merge!(signature.kwarg_types)
580
+ parameter_types[signature.rest_name] = signature.rest_type if signature.has_rest
581
+ parameter_types[signature.keyrest_name] = signature.keyrest_type if signature.has_keyrest
582
+ parameter_types[signature.block_name] = signature.block_type if signature.block_name
583
+
584
+ params = parameters.map do |_, name|
585
+ type = parameter_types[name.to_sym]
586
+ "#{name}: #{type}"
587
+ end.join(", ")
588
+
589
+ returns = type_of(signature.return_type)
590
+
591
+ type_parameters = (params + returns).scan(TYPE_PARAMETER_MATCHER).flatten.uniq.map { |p| ":#{p}" }.join(", ")
592
+ type_parameters = ".type_parameters(#{type_parameters})" unless type_parameters.empty?
593
+
594
+ mode = case signature.mode
595
+ when "abstract"
596
+ ".abstract"
597
+ when "override"
598
+ ".override"
599
+ when "overridable_override"
600
+ ".overridable.override"
601
+ when "overridable"
602
+ ".overridable"
603
+ else
604
+ ""
605
+ end
466
606
 
467
- indented("def #{method_name}#{parameters}; end")
607
+ signature_body = +""
608
+ signature_body << mode
609
+ signature_body << type_parameters
610
+ signature_body << ".params(#{params})" unless params.empty?
611
+ signature_body << ".returns(#{returns})"
612
+ signature_body = signature_body
613
+ .gsub(".returns(<VOID>)", ".void")
614
+ .gsub("<NOT-TYPED>", "T.untyped")
615
+ .gsub(TYPE_PARAMETER_MATCHER, "T.type_parameter(:\\1)")[1..-1]
616
+
617
+ "sig { #{signature_body} }"
468
618
  end
469
619
 
470
620
  sig { params(symbol_name: String).returns(T::Boolean) }
@@ -483,16 +633,17 @@ module Tapioca
483
633
  sig do
484
634
  type_parameters(:U)
485
635
  .params(
636
+ step: Integer,
486
637
  _blk: T.proc
487
638
  .returns(T.type_parameter(:U))
488
639
  )
489
640
  .returns(T.type_parameter(:U))
490
641
  end
491
- def with_indentation(&_blk)
492
- @indent += 2
642
+ def with_indentation(step = 1, &_blk)
643
+ @indent += 2 * step
493
644
  yield
494
645
  ensure
495
- @indent -= 2
646
+ @indent -= 2 * step
496
647
  end
497
648
 
498
649
  sig { params(str: String).returns(String) }
@@ -627,7 +778,7 @@ module Tapioca
627
778
  return nil
628
779
  end
629
780
 
630
- name_of(target)
781
+ raw_name_of(target)
631
782
  end
632
783
 
633
784
  sig { params(constant: Module).returns(T.nilable(String)) }
@@ -647,6 +798,18 @@ module Tapioca
647
798
  Class.instance_method(:superclass).bind(constant).call
648
799
  end
649
800
 
801
+ sig { params(method: T.any(UnboundMethod, Method)).returns(T.untyped) }
802
+ def signature_of(method)
803
+ T::Private::Methods.signature_for_method(method)
804
+ rescue LoadError, StandardError
805
+ nil
806
+ end
807
+
808
+ sig { params(constant: Module).returns(String) }
809
+ def type_of(constant)
810
+ constant.to_s.gsub(/\bAttachedClass\b/, "T.attached_class")
811
+ end
812
+
650
813
  sig { params(constant: Module, other: BasicObject).returns(T::Boolean).checked(:never) }
651
814
  def are_equal?(constant, other)
652
815
  BasicObject.instance_method(:equal?).bind(constant).call(other)