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.
- checksums.yaml +4 -4
- data/Gemfile +26 -1
- data/README.md +16 -0
- data/Rakefile +16 -4
- data/lib/tapioca.rb +6 -2
- data/lib/tapioca/cli.rb +25 -3
- data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +130 -0
- data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
- data/lib/tapioca/compilers/dsl/active_record_associations.rb +267 -0
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +404 -0
- data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
- data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +212 -0
- data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
- data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +168 -0
- data/lib/tapioca/compilers/dsl/active_resource.rb +140 -0
- data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +126 -0
- data/lib/tapioca/compilers/dsl/base.rb +165 -0
- data/lib/tapioca/compilers/dsl/frozen_record.rb +96 -0
- data/lib/tapioca/compilers/dsl/protobuf.rb +144 -0
- data/lib/tapioca/compilers/dsl/smart_properties.rb +173 -0
- data/lib/tapioca/compilers/dsl/state_machines.rb +378 -0
- data/lib/tapioca/compilers/dsl/url_helpers.rb +160 -0
- data/lib/tapioca/compilers/dsl_compiler.rb +121 -0
- data/lib/tapioca/compilers/requires_compiler.rb +67 -0
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +195 -32
- data/lib/tapioca/config.rb +11 -6
- data/lib/tapioca/config_builder.rb +19 -9
- data/lib/tapioca/constant_locator.rb +1 -0
- data/lib/tapioca/core_ext/class.rb +23 -0
- data/lib/tapioca/gemfile.rb +32 -9
- data/lib/tapioca/generator.rb +200 -24
- data/lib/tapioca/loader.rb +30 -9
- data/lib/tapioca/version.rb +1 -1
- 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(
|
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
|
-
|
357
|
-
|
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)
|
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
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
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
|
-
|
405
|
-
|
406
|
-
|
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
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
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
|
-
|
440
|
-
|
441
|
-
|
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
|
-
|
465
|
-
|
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
|
-
|
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
|
-
|
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)
|