tapioca 0.4.0 → 0.4.5

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