tapioca 0.3.0 → 0.4.3
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 +25 -1
- data/README.md +23 -2
- data/Rakefile +15 -4
- data/lib/tapioca.rb +8 -2
- data/lib/tapioca/cli.rb +31 -2
- data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +129 -0
- data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
- data/lib/tapioca/compilers/dsl/active_record_associations.rb +285 -0
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +387 -0
- data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
- data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +213 -0
- data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
- data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +170 -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 +92 -0
- data/lib/tapioca/compilers/dsl_compiler.rb +121 -0
- data/lib/tapioca/compilers/requires_compiler.rb +67 -0
- data/lib/tapioca/compilers/sorbet.rb +34 -0
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +223 -35
- data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +3 -17
- data/lib/tapioca/compilers/todos_compiler.rb +32 -0
- data/lib/tapioca/config.rb +14 -6
- data/lib/tapioca/config_builder.rb +22 -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 +231 -23
- data/lib/tapioca/loader.rb +30 -9
- data/lib/tapioca/sorbet_config_parser.rb +77 -0
- data/lib/tapioca/version.rb +1 -1
- metadata +31 -51
@@ -0,0 +1,92 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "parlour"
|
5
|
+
|
6
|
+
begin
|
7
|
+
require "rails"
|
8
|
+
require "action_controller"
|
9
|
+
require "action_view"
|
10
|
+
rescue LoadError
|
11
|
+
return
|
12
|
+
end
|
13
|
+
|
14
|
+
module Tapioca
|
15
|
+
module Compilers
|
16
|
+
module Dsl
|
17
|
+
class UrlHelpers < Base
|
18
|
+
extend T::Sig
|
19
|
+
|
20
|
+
sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
|
21
|
+
def decorate(root, constant)
|
22
|
+
case constant
|
23
|
+
when GeneratedPathHelpersModule.singleton_class, GeneratedUrlHelpersModule.singleton_class
|
24
|
+
generate_module_for(root, constant)
|
25
|
+
else
|
26
|
+
root.path(constant) do |mod|
|
27
|
+
create_mixins_for(mod, constant, GeneratedUrlHelpersModule)
|
28
|
+
create_mixins_for(mod, constant, GeneratedPathHelpersModule)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
NON_DISCOVERABLE_INCLUDERS = T.let([
|
34
|
+
ActionDispatch::IntegrationTest,
|
35
|
+
ActionView::Helpers,
|
36
|
+
], T::Array[Module])
|
37
|
+
|
38
|
+
sig { override.returns(T::Enumerable[Module]) }
|
39
|
+
def gather_constants
|
40
|
+
Object.const_set(:GeneratedUrlHelpersModule, Rails.application.routes.named_routes.url_helpers_module)
|
41
|
+
Object.const_set(:GeneratedPathHelpersModule, Rails.application.routes.named_routes.path_helpers_module)
|
42
|
+
|
43
|
+
module_enumerator = T.cast(ObjectSpace.each_object(Module), T::Enumerator[Module])
|
44
|
+
constants = module_enumerator.select do |mod|
|
45
|
+
next unless Module.instance_method(:name).bind(mod).call
|
46
|
+
|
47
|
+
includes_helper?(mod, GeneratedUrlHelpersModule) ||
|
48
|
+
includes_helper?(mod, GeneratedPathHelpersModule) ||
|
49
|
+
includes_helper?(mod.singleton_class, GeneratedUrlHelpersModule) ||
|
50
|
+
includes_helper?(mod.singleton_class, GeneratedPathHelpersModule)
|
51
|
+
end
|
52
|
+
|
53
|
+
constants.concat(NON_DISCOVERABLE_INCLUDERS)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
sig { params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
|
59
|
+
def generate_module_for(root, constant)
|
60
|
+
root.create_module(T.must(constant.name)) do |mod|
|
61
|
+
mod.create_include("ActionDispatch::Routing::UrlFor")
|
62
|
+
mod.create_include("ActionDispatch::Routing::PolymorphicRoutes")
|
63
|
+
|
64
|
+
constant.instance_methods(false).each do |method|
|
65
|
+
mod.create_method(
|
66
|
+
method.to_s,
|
67
|
+
parameters: [Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped")],
|
68
|
+
return_type: "String"
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
sig { params(mod: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module), helper_module: Module).void }
|
75
|
+
def create_mixins_for(mod, constant, helper_module)
|
76
|
+
include_helper = constant.ancestors.include?(helper_module) || NON_DISCOVERABLE_INCLUDERS.include?(constant)
|
77
|
+
extend_helper = constant.singleton_class.ancestors.include?(helper_module)
|
78
|
+
|
79
|
+
mod.create_include(T.must(helper_module.name)) if include_helper
|
80
|
+
mod.create_extend(T.must(helper_module.name)) if extend_helper
|
81
|
+
end
|
82
|
+
|
83
|
+
sig { params(mod: Module, helper: Module).returns(T::Boolean) }
|
84
|
+
def includes_helper?(mod, helper)
|
85
|
+
superclass_ancestors = mod.superclass&.ancestors if Class === mod
|
86
|
+
superclass_ancestors ||= []
|
87
|
+
(mod.ancestors - superclass_ancestors).include?(helper)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
require "tapioca/compilers/dsl/base"
|
5
|
+
|
6
|
+
module Tapioca
|
7
|
+
module Compilers
|
8
|
+
class DslCompiler
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { returns(T::Enumerable[Dsl::Base]) }
|
12
|
+
attr_reader :generators
|
13
|
+
|
14
|
+
sig { returns(T::Array[Module]) }
|
15
|
+
attr_reader :requested_constants
|
16
|
+
|
17
|
+
sig { returns(T.proc.params(error: String).void) }
|
18
|
+
attr_reader :error_handler
|
19
|
+
|
20
|
+
sig do
|
21
|
+
params(
|
22
|
+
requested_constants: T::Array[Module],
|
23
|
+
requested_generators: T::Array[String],
|
24
|
+
error_handler: T.nilable(T.proc.params(error: String).void)
|
25
|
+
).void
|
26
|
+
end
|
27
|
+
def initialize(requested_constants:, requested_generators: [], error_handler: nil)
|
28
|
+
@generators = T.let(
|
29
|
+
gather_generators(requested_generators),
|
30
|
+
T::Enumerable[Dsl::Base]
|
31
|
+
)
|
32
|
+
@requested_constants = requested_constants
|
33
|
+
@error_handler = error_handler || $stderr.method(:puts)
|
34
|
+
end
|
35
|
+
|
36
|
+
sig { params(blk: T.proc.params(constant: Module, rbi: String).void).void }
|
37
|
+
def run(&blk)
|
38
|
+
constants_to_process = gather_constants(requested_constants)
|
39
|
+
|
40
|
+
if constants_to_process.empty?
|
41
|
+
report_error(<<~ERROR)
|
42
|
+
No classes/modules can be matched for RBI generation.
|
43
|
+
Please check that the requested classes/modules include processable DSL methods.
|
44
|
+
ERROR
|
45
|
+
end
|
46
|
+
|
47
|
+
constants_to_process.sort_by { |c| c.name.to_s }.each do |constant|
|
48
|
+
rbi = rbi_for_constant(constant)
|
49
|
+
next if rbi.nil?
|
50
|
+
|
51
|
+
blk.call(constant, rbi)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
sig { params(requested_generators: T::Array[String]).returns(Proc) }
|
58
|
+
def generator_filter(requested_generators)
|
59
|
+
return proc { true } if requested_generators.empty?
|
60
|
+
|
61
|
+
generators = requested_generators.map(&:downcase)
|
62
|
+
|
63
|
+
proc do |klass|
|
64
|
+
generator = klass.name&.sub(/^Tapioca::Compilers::Dsl::/, '')&.downcase
|
65
|
+
generators.include?(generator)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { params(requested_generators: T::Array[String]).returns(T::Enumerable[Dsl::Base]) }
|
70
|
+
def gather_generators(requested_generators)
|
71
|
+
generator_filter = generator_filter(requested_generators)
|
72
|
+
|
73
|
+
Dsl::Base.descendants.select(&generator_filter).map(&:new)
|
74
|
+
end
|
75
|
+
|
76
|
+
sig { params(requested_constants: T::Array[Module]).returns(T::Set[Module]) }
|
77
|
+
def gather_constants(requested_constants)
|
78
|
+
constants = generators.map(&:processable_constants).reduce(Set.new, :union)
|
79
|
+
constants &= requested_constants unless requested_constants.empty?
|
80
|
+
constants
|
81
|
+
end
|
82
|
+
|
83
|
+
sig { params(constant: Module).returns(T.nilable(String)) }
|
84
|
+
def rbi_for_constant(constant)
|
85
|
+
parlour = Parlour::RbiGenerator.new(sort_namespaces: true)
|
86
|
+
|
87
|
+
generators.each do |generator|
|
88
|
+
next unless generator.handles?(constant)
|
89
|
+
generator.decorate(parlour.root, constant)
|
90
|
+
end
|
91
|
+
|
92
|
+
return if parlour.root.children.empty?
|
93
|
+
|
94
|
+
resolve_conflicts(parlour)
|
95
|
+
|
96
|
+
parlour.rbi("true").strip
|
97
|
+
end
|
98
|
+
|
99
|
+
sig { params(parlour: Parlour::RbiGenerator).void }
|
100
|
+
def resolve_conflicts(parlour)
|
101
|
+
Parlour::ConflictResolver.new.resolve_conflicts(parlour.root) do |msg, candidates|
|
102
|
+
error = StringIO.new
|
103
|
+
error.puts "=== Error ==="
|
104
|
+
error.puts msg
|
105
|
+
error.puts "# Candidates"
|
106
|
+
candidates.each_with_index do |candidate, index|
|
107
|
+
error.puts " #{index}. #{candidate.describe}"
|
108
|
+
end
|
109
|
+
report_error(error.string)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
sig { params(error: String).returns(T.noreturn) }
|
114
|
+
def report_error(error)
|
115
|
+
handler = error_handler
|
116
|
+
handler.call(error)
|
117
|
+
exit(1)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require_relative '../sorbet_config_parser'
|
5
|
+
|
6
|
+
module Tapioca
|
7
|
+
module Compilers
|
8
|
+
class RequiresCompiler
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { params(sorbet_path: String).void }
|
12
|
+
def initialize(sorbet_path)
|
13
|
+
@sorbet_path = sorbet_path
|
14
|
+
end
|
15
|
+
|
16
|
+
sig { returns(String) }
|
17
|
+
def compile
|
18
|
+
config = SorbetConfig.parse_file(@sorbet_path)
|
19
|
+
files = collect_files(config)
|
20
|
+
files.flat_map do |file|
|
21
|
+
collect_requires(file).reject do |req|
|
22
|
+
name_in_project?(files, req)
|
23
|
+
end
|
24
|
+
end.sort.uniq.map do |name|
|
25
|
+
"require '#{name}'\n"
|
26
|
+
end.join
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
sig { params(config: SorbetConfig).returns(T::Array[String]) }
|
32
|
+
def collect_files(config)
|
33
|
+
config.paths.flat_map do |path|
|
34
|
+
path = (Pathname.new(@sorbet_path) / "../.." / path).cleanpath
|
35
|
+
if path.directory?
|
36
|
+
Dir.glob("#{path}/**/*.rb", File::FNM_EXTGLOB).reject do |file|
|
37
|
+
file_ignored_by_sorbet?(config, file)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
[path.to_s]
|
41
|
+
end
|
42
|
+
end.sort.uniq
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { params(file_path: String).returns(T::Enumerable[String]) }
|
46
|
+
def collect_requires(file_path)
|
47
|
+
File.read(file_path).lines.map do |line|
|
48
|
+
/^\s*require\s*(\(\s*)?['"](?<name>[^'"]+)['"](\s*\))?/.match(line) { |m| m["name"] }
|
49
|
+
end.compact
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { params(config: SorbetConfig, file: String).returns(T::Boolean) }
|
53
|
+
def file_ignored_by_sorbet?(config, file)
|
54
|
+
config.ignore.any? do |path|
|
55
|
+
Regexp.new(Regexp.escape(path)) =~ file
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { params(files: T::Enumerable[String], name: String).returns(T::Boolean) }
|
60
|
+
def name_in_project?(files, name)
|
61
|
+
files.any? do |file|
|
62
|
+
File.basename(file, '.rb') == name
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
require 'pathname'
|
5
|
+
require 'shellwords'
|
6
|
+
|
7
|
+
module Tapioca
|
8
|
+
module Compilers
|
9
|
+
module Sorbet
|
10
|
+
SORBET = Pathname.new(Gem::Specification.find_by_name("sorbet-static").full_gem_path) / "libexec" / "sorbet"
|
11
|
+
|
12
|
+
class << self
|
13
|
+
extend(T::Sig)
|
14
|
+
|
15
|
+
sig { params(args: String).returns(String) }
|
16
|
+
def run(*args)
|
17
|
+
IO.popen(
|
18
|
+
[
|
19
|
+
sorbet_path,
|
20
|
+
"--quiet",
|
21
|
+
*args,
|
22
|
+
].join(' '),
|
23
|
+
err: "/dev/null"
|
24
|
+
).read
|
25
|
+
end
|
26
|
+
|
27
|
+
sig { returns(String) }
|
28
|
+
def sorbet_path
|
29
|
+
SORBET.to_s.shellescape
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -29,7 +29,7 @@ module Tapioca
|
|
29
29
|
def generate
|
30
30
|
symbols
|
31
31
|
.sort
|
32
|
-
.map(
|
32
|
+
.map { |symbol| generate_from_symbol(symbol) }
|
33
33
|
.compact
|
34
34
|
.join("\n\n")
|
35
35
|
.concat("\n")
|
@@ -176,12 +176,43 @@ module Tapioca
|
|
176
176
|
return if symbol_ignored?(name) && methods.nil?
|
177
177
|
|
178
178
|
[
|
179
|
+
compile_module_helpers(constant),
|
179
180
|
compile_mixins(constant),
|
181
|
+
compile_mixes_in_class_methods(constant),
|
182
|
+
compile_props(constant),
|
180
183
|
methods,
|
181
184
|
].select { |b| b != "" }.join("\n\n")
|
182
185
|
end
|
183
186
|
end
|
184
187
|
|
188
|
+
sig { params(constant: Module).returns(String) }
|
189
|
+
def compile_module_helpers(constant)
|
190
|
+
abstract_type = T::Private::Abstract::Data.get(constant, :abstract_type)
|
191
|
+
|
192
|
+
if abstract_type
|
193
|
+
indented("#{abstract_type}!")
|
194
|
+
elsif T::Private::Final.final_module?(constant)
|
195
|
+
indented("final!")
|
196
|
+
elsif T::Private::Sealed.sealed_module?(constant)
|
197
|
+
indented("sealed!")
|
198
|
+
else
|
199
|
+
""
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
sig { params(constant: Module).returns(String) }
|
204
|
+
def compile_props(constant)
|
205
|
+
return "" unless T::Props::ClassMethods === constant
|
206
|
+
|
207
|
+
constant.props.map do |name, prop|
|
208
|
+
method = "prop"
|
209
|
+
method = "const" if prop.fetch(:immutable, false)
|
210
|
+
type = prop.fetch(:type_object, "T.untyped")
|
211
|
+
|
212
|
+
indented("#{method} :#{name}, #{type}")
|
213
|
+
end.join("\n")
|
214
|
+
end
|
215
|
+
|
185
216
|
sig { params(name: String, constant: Module).returns(T.nilable(String)) }
|
186
217
|
def compile_subconstants(name, constant)
|
187
218
|
output = constants_of(constant).sort.uniq.map do |constant_name|
|
@@ -286,6 +317,7 @@ module Tapioca
|
|
286
317
|
end
|
287
318
|
|
288
319
|
prepends = prepend
|
320
|
+
.reverse
|
289
321
|
.select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
|
290
322
|
.select(&method(:public_module?))
|
291
323
|
.map do |mod|
|
@@ -296,6 +328,7 @@ module Tapioca
|
|
296
328
|
end
|
297
329
|
|
298
330
|
includes = include
|
331
|
+
.reverse
|
299
332
|
.select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
|
300
333
|
.select(&method(:public_module?))
|
301
334
|
.map do |mod|
|
@@ -303,23 +336,68 @@ module Tapioca
|
|
303
336
|
end
|
304
337
|
|
305
338
|
extends = extend
|
339
|
+
.reverse
|
306
340
|
.select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
|
307
341
|
.select(&method(:public_module?))
|
308
342
|
.map do |mod|
|
309
343
|
indented("extend(#{qualified_name_of(mod)})")
|
310
344
|
end
|
311
345
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
346
|
+
(prepends + includes + extends).join("\n")
|
347
|
+
end
|
348
|
+
|
349
|
+
sig { params(constant: Module).returns(String) }
|
350
|
+
def compile_mixes_in_class_methods(constant)
|
351
|
+
return "" if constant.is_a?(Class)
|
352
|
+
|
353
|
+
mixins_from_modules = {}
|
354
|
+
|
355
|
+
Class.new do
|
356
|
+
# rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
|
357
|
+
def method_missing(symbol, *args)
|
316
358
|
end
|
317
|
-
|
318
|
-
|
319
|
-
|
359
|
+
|
360
|
+
define_singleton_method(:include) do |mod|
|
361
|
+
before = singleton_class.ancestors
|
362
|
+
super(mod).tap do
|
363
|
+
mixins_from_modules[mod] = singleton_class.ancestors - before
|
364
|
+
end
|
320
365
|
end
|
321
366
|
|
322
|
-
|
367
|
+
class << self
|
368
|
+
def method_missing(symbol, *args)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
# rubocop:enable Style/MethodMissingSuper, Style/MissingRespondToMissing
|
372
|
+
end.include(constant)
|
373
|
+
|
374
|
+
all_dynamic_extends = mixins_from_modules.delete(constant)
|
375
|
+
all_dynamic_includes = mixins_from_modules.keys
|
376
|
+
dynamic_extends_from_dynamic_includes = mixins_from_modules.values.flatten
|
377
|
+
dynamic_extends = all_dynamic_extends - dynamic_extends_from_dynamic_includes
|
378
|
+
|
379
|
+
result = all_dynamic_includes
|
380
|
+
.select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
|
381
|
+
.select(&method(:public_module?))
|
382
|
+
.map do |mod|
|
383
|
+
indented("include(#{qualified_name_of(mod)})")
|
384
|
+
end.join("\n")
|
385
|
+
|
386
|
+
mixed_in_module = dynamic_extends.find do |mod|
|
387
|
+
mod != constant && public_module?(mod)
|
388
|
+
end
|
389
|
+
|
390
|
+
return result if mixed_in_module.nil?
|
391
|
+
|
392
|
+
qualified_name = qualified_name_of(mixed_in_module)
|
393
|
+
return result if qualified_name == ""
|
394
|
+
|
395
|
+
[
|
396
|
+
result,
|
397
|
+
indented("mixes_in_class_methods(#{qualified_name})"),
|
398
|
+
].select { |b| b != "" }.join("\n\n")
|
399
|
+
rescue
|
400
|
+
""
|
323
401
|
end
|
324
402
|
|
325
403
|
sig { params(name: String, constant: Module).returns(T.nilable(String)) }
|
@@ -331,7 +409,7 @@ module Tapioca
|
|
331
409
|
)
|
332
410
|
|
333
411
|
instance_methods = compile_directly_owned_methods(name, constant)
|
334
|
-
singleton_methods = compile_directly_owned_methods(name, singleton_class_of(constant)
|
412
|
+
singleton_methods = compile_directly_owned_methods(name, singleton_class_of(constant))
|
335
413
|
|
336
414
|
return if symbol_ignored?(name) && instance_methods.empty? && singleton_methods.empty?
|
337
415
|
|
@@ -344,24 +422,44 @@ module Tapioca
|
|
344
422
|
|
345
423
|
sig { params(module_name: String, mod: Module, for_visibility: T::Array[Symbol]).returns(String) }
|
346
424
|
def compile_directly_owned_methods(module_name, mod, for_visibility = [:public, :protected, :private])
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
425
|
+
indent_step = 0
|
426
|
+
preamble = nil
|
427
|
+
postamble = nil
|
428
|
+
|
429
|
+
if mod.singleton_class?
|
430
|
+
indent_step = 1
|
431
|
+
preamble = indented("class << self")
|
432
|
+
postamble = indented("end")
|
433
|
+
end
|
355
434
|
|
356
|
-
|
357
|
-
|
358
|
-
|
435
|
+
methods = with_indentation(indent_step) do
|
436
|
+
method_names_by_visibility(mod)
|
437
|
+
.delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
|
438
|
+
.flat_map do |visibility, method_list|
|
439
|
+
compiled = method_list.sort!.map do |name|
|
440
|
+
next if name == :initialize
|
441
|
+
compile_method(module_name, mod, mod.instance_method(name))
|
442
|
+
end
|
443
|
+
compiled.compact!
|
444
|
+
|
445
|
+
unless compiled.empty? || visibility == :public
|
446
|
+
# add visibility badge
|
447
|
+
compiled.unshift('', indented(visibility.to_s), '')
|
448
|
+
end
|
449
|
+
|
450
|
+
compiled
|
359
451
|
end
|
452
|
+
.compact
|
453
|
+
.join("\n")
|
454
|
+
end
|
360
455
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
456
|
+
return "" if methods.strip == ""
|
457
|
+
|
458
|
+
[
|
459
|
+
preamble,
|
460
|
+
methods,
|
461
|
+
postamble,
|
462
|
+
].compact.join("\n")
|
365
463
|
end
|
366
464
|
|
367
465
|
sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
|
@@ -373,6 +471,16 @@ module Tapioca
|
|
373
471
|
}
|
374
472
|
end
|
375
473
|
|
474
|
+
sig { params(constant: Module, method_name: String).returns(T::Boolean) }
|
475
|
+
def struct_method?(constant, method_name)
|
476
|
+
return false unless T::Props::ClassMethods === constant
|
477
|
+
|
478
|
+
constant
|
479
|
+
.props
|
480
|
+
.keys
|
481
|
+
.include?(method_name.gsub(/=$/, '').to_sym)
|
482
|
+
end
|
483
|
+
|
376
484
|
sig do
|
377
485
|
params(
|
378
486
|
symbol_name: String,
|
@@ -385,24 +493,52 @@ module Tapioca
|
|
385
493
|
return unless method.owner == constant
|
386
494
|
return if symbol_ignored?(symbol_name) && !method_in_gem?(method)
|
387
495
|
|
496
|
+
signature = signature_of(method)
|
497
|
+
method = signature.method if signature
|
498
|
+
|
388
499
|
method_name = method.name.to_s
|
389
500
|
return unless valid_method_name?(method_name)
|
501
|
+
return if struct_method?(constant, method_name)
|
502
|
+
return if method_name.start_with?("__t_props_generated_")
|
390
503
|
|
391
504
|
params = T.let(method.parameters, T::Array[T::Array[Symbol]])
|
392
505
|
parameters = params.map do |(type, name)|
|
393
|
-
name
|
506
|
+
unless name
|
507
|
+
# For attr_writer methods, Sorbet signatures have the name
|
508
|
+
# of the method (without the trailing = sign) as the name of
|
509
|
+
# the only parameter. So, if the parameter does not have a name
|
510
|
+
# then the replacement name should be the name of the method
|
511
|
+
# (minus trailing =) if and only if there is a signature for the
|
512
|
+
# method and the parameter is required and there is a single
|
513
|
+
# parameter and the signature also defines a single parameter and
|
514
|
+
# the name of the method ends with a = character.
|
515
|
+
writer_method_with_sig = signature &&
|
516
|
+
type == :req &&
|
517
|
+
params.size == 1 &&
|
518
|
+
signature.arg_types.size == 1 &&
|
519
|
+
method_name[-1] == "="
|
520
|
+
|
521
|
+
name = if writer_method_with_sig
|
522
|
+
method_name[0...-1].to_sym
|
523
|
+
else
|
524
|
+
:_
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
# Sanitize param names
|
529
|
+
name = name.to_s.gsub(/[^a-zA-Z0-9_]/, '_')
|
394
530
|
|
395
531
|
case type
|
396
532
|
when :req
|
397
|
-
name
|
533
|
+
name
|
398
534
|
when :opt
|
399
|
-
"#{name} =
|
535
|
+
"#{name} = T.unsafe(nil)"
|
400
536
|
when :rest
|
401
537
|
"*#{name}"
|
402
538
|
when :keyreq
|
403
539
|
"#{name}:"
|
404
540
|
when :key
|
405
|
-
"#{name}:
|
541
|
+
"#{name}: T.unsafe(nil)"
|
406
542
|
when :keyrest
|
407
543
|
"**#{name}"
|
408
544
|
when :block
|
@@ -410,10 +546,54 @@ module Tapioca
|
|
410
546
|
end
|
411
547
|
end.join(', ')
|
412
548
|
|
413
|
-
method_name = "#{'self.' if constant.singleton_class?}#{method_name}"
|
414
549
|
parameters = "(#{parameters})" if parameters != ""
|
415
550
|
|
416
|
-
indented(
|
551
|
+
signature_str = indented(compile_signature(signature)) if signature
|
552
|
+
[
|
553
|
+
signature_str,
|
554
|
+
indented("def #{method_name}#{parameters}; end"),
|
555
|
+
].compact.join("\n")
|
556
|
+
end
|
557
|
+
|
558
|
+
TYPE_PARAMETER_MATCHER = /T\.type_parameter\(:?([[:word:]]+)\)/
|
559
|
+
|
560
|
+
sig { params(signature: T.untyped).returns(String) }
|
561
|
+
def compile_signature(signature)
|
562
|
+
params = signature.arg_types
|
563
|
+
params += signature.kwarg_types.to_a
|
564
|
+
params << [signature.rest_name, signature.rest_type] if signature.has_rest
|
565
|
+
params << [signature.block_name, signature.block_type] if signature.block_name
|
566
|
+
|
567
|
+
params = params.compact.map { |name, type| "#{name}: #{type}" }.join(", ")
|
568
|
+
returns = signature.return_type.to_s
|
569
|
+
|
570
|
+
type_parameters = (params + returns).scan(TYPE_PARAMETER_MATCHER).flatten.uniq.map { |p| ":#{p}" }.join(", ")
|
571
|
+
type_parameters = ".type_parameters(#{type_parameters})" unless type_parameters.empty?
|
572
|
+
|
573
|
+
mode = case signature.mode
|
574
|
+
when "abstract"
|
575
|
+
".abstract"
|
576
|
+
when "override"
|
577
|
+
".override"
|
578
|
+
when "overridable_override"
|
579
|
+
".overridable.override"
|
580
|
+
when "overridable"
|
581
|
+
".overridable"
|
582
|
+
else
|
583
|
+
""
|
584
|
+
end
|
585
|
+
|
586
|
+
signature_body = +""
|
587
|
+
signature_body << mode
|
588
|
+
signature_body << type_parameters
|
589
|
+
signature_body << ".params(#{params})" unless params.empty?
|
590
|
+
signature_body << ".returns(#{returns})"
|
591
|
+
signature_body = signature_body
|
592
|
+
.gsub(".returns(<VOID>)", ".void")
|
593
|
+
.gsub("<NOT-TYPED>", "T.untyped")
|
594
|
+
.gsub(TYPE_PARAMETER_MATCHER, "T.type_parameter(:\\1)")[1..-1]
|
595
|
+
|
596
|
+
"sig { #{signature_body} }"
|
417
597
|
end
|
418
598
|
|
419
599
|
sig { params(symbol_name: String).returns(T::Boolean) }
|
@@ -432,16 +612,17 @@ module Tapioca
|
|
432
612
|
sig do
|
433
613
|
type_parameters(:U)
|
434
614
|
.params(
|
615
|
+
step: Integer,
|
435
616
|
_blk: T.proc
|
436
617
|
.returns(T.type_parameter(:U))
|
437
618
|
)
|
438
619
|
.returns(T.type_parameter(:U))
|
439
620
|
end
|
440
|
-
def with_indentation(&_blk)
|
441
|
-
@indent += 2
|
621
|
+
def with_indentation(step = 1, &_blk)
|
622
|
+
@indent += 2 * step
|
442
623
|
yield
|
443
624
|
ensure
|
444
|
-
@indent -= 2
|
625
|
+
@indent -= 2 * step
|
445
626
|
end
|
446
627
|
|
447
628
|
sig { params(str: String).returns(String) }
|
@@ -576,7 +757,7 @@ module Tapioca
|
|
576
757
|
return nil
|
577
758
|
end
|
578
759
|
|
579
|
-
|
760
|
+
raw_name_of(target)
|
580
761
|
end
|
581
762
|
|
582
763
|
sig { params(constant: Module).returns(T.nilable(String)) }
|
@@ -596,6 +777,13 @@ module Tapioca
|
|
596
777
|
Class.instance_method(:superclass).bind(constant).call
|
597
778
|
end
|
598
779
|
|
780
|
+
sig { params(method: T.any(UnboundMethod, Method)).returns(T.untyped) }
|
781
|
+
def signature_of(method)
|
782
|
+
T::Private::Methods.signature_for_method(method)
|
783
|
+
rescue LoadError, StandardError
|
784
|
+
nil
|
785
|
+
end
|
786
|
+
|
599
787
|
sig { params(constant: Module, other: BasicObject).returns(T::Boolean).checked(:never) }
|
600
788
|
def are_equal?(constant, other)
|
601
789
|
BasicObject.instance_method(:equal?).bind(constant).call(other)
|