tapioca 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +25 -1
- data/README.md +12 -0
- data/Rakefile +15 -4
- data/lib/tapioca.rb +2 -0
- data/lib/tapioca/cli.rb +24 -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 +379 -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 +163 -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 +83 -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 +141 -24
- 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/generator.rb +187 -21
- data/lib/tapioca/loader.rb +20 -9
- data/lib/tapioca/sorbet_config_parser.rb +77 -0
- data/lib/tapioca/version.rb +1 -1
- metadata +29 -51
data/lib/tapioca/config.rb
CHANGED
@@ -12,6 +12,7 @@ module Tapioca
|
|
12
12
|
const(:exclude, T::Array[String])
|
13
13
|
const(:typed_overrides, T::Hash[String, String])
|
14
14
|
const(:todos_path, String)
|
15
|
+
const(:generators, T::Array[String])
|
15
16
|
|
16
17
|
sig { returns(Pathname) }
|
17
18
|
def outpath
|
@@ -21,17 +22,21 @@ module Tapioca
|
|
21
22
|
|
22
23
|
private_class_method :new
|
23
24
|
|
24
|
-
|
25
|
-
SORBET_CONFIG = "
|
25
|
+
SORBET_PATH = T.let("sorbet", String)
|
26
|
+
SORBET_CONFIG = T.let("#{SORBET_PATH}/config", String)
|
27
|
+
TAPIOCA_PATH = T.let("#{SORBET_PATH}/tapioca", String)
|
28
|
+
TAPIOCA_CONFIG = T.let("#{TAPIOCA_PATH}/config.yml", String)
|
29
|
+
|
30
|
+
DEFAULT_POSTREQUIRE = T.let("#{TAPIOCA_PATH}/require.rb", String)
|
31
|
+
DEFAULT_RBIDIR = T.let("#{SORBET_PATH}/rbi", String)
|
32
|
+
DEFAULT_DSLDIR = T.let("#{DEFAULT_RBIDIR}/dsl", String)
|
33
|
+
DEFAULT_GEMDIR = T.let("#{DEFAULT_RBIDIR}/gems", String)
|
34
|
+
DEFAULT_TODOSPATH = T.let("#{DEFAULT_RBIDIR}/todo.rbi", String)
|
26
35
|
|
27
|
-
DEFAULT_POSTREQUIRE = "sorbet/tapioca/require.rb"
|
28
|
-
DEFAULT_RBIDIR = "sorbet/rbi"
|
29
|
-
DEFAULT_OUTDIR = T.let("#{DEFAULT_RBIDIR}/gems", String)
|
30
36
|
DEFAULT_OVERRIDES = T.let({
|
31
37
|
# ActiveSupport overrides some core methods with different signatures
|
32
38
|
# so we generate a typed: false RBI for it to suppress errors
|
33
39
|
"activesupport" => "false",
|
34
40
|
}.freeze, T::Hash[String, String])
|
35
|
-
DEFAULT_TODOSPATH = T.let("#{DEFAULT_RBIDIR}/todo.rbi", String)
|
36
41
|
end
|
37
42
|
end
|
@@ -8,10 +8,10 @@ module Tapioca
|
|
8
8
|
class << self
|
9
9
|
extend(T::Sig)
|
10
10
|
|
11
|
-
sig { params(options: T::Hash[String, T.untyped]).returns(Config) }
|
12
|
-
def from_options(options)
|
11
|
+
sig { params(command: Symbol, options: T::Hash[String, T.untyped]).returns(Config) }
|
12
|
+
def from_options(command, options)
|
13
13
|
Config.from_hash(
|
14
|
-
merge_options(default_options, config_options, options)
|
14
|
+
merge_options(default_options(command), config_options, options)
|
15
15
|
)
|
16
16
|
end
|
17
17
|
|
@@ -19,16 +19,25 @@ module Tapioca
|
|
19
19
|
|
20
20
|
sig { returns(T::Hash[String, T.untyped]) }
|
21
21
|
def config_options
|
22
|
-
if File.exist?(Config::
|
23
|
-
YAML.load_file(Config::
|
22
|
+
if File.exist?(Config::TAPIOCA_CONFIG)
|
23
|
+
YAML.load_file(Config::TAPIOCA_CONFIG, fallback: {})
|
24
24
|
else
|
25
25
|
{}
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
-
sig { returns(T::Hash[String, T.untyped]) }
|
30
|
-
def default_options
|
31
|
-
|
29
|
+
sig { params(command: Symbol).returns(T::Hash[String, T.untyped]) }
|
30
|
+
def default_options(command)
|
31
|
+
default_outdir = case command
|
32
|
+
when :sync, :generate
|
33
|
+
Config::DEFAULT_GEMDIR
|
34
|
+
when :dsl
|
35
|
+
Config::DEFAULT_DSLDIR
|
36
|
+
else
|
37
|
+
Config::SORBET_PATH
|
38
|
+
end
|
39
|
+
|
40
|
+
DEFAULT_OPTIONS.merge("outdir" => default_outdir)
|
32
41
|
end
|
33
42
|
|
34
43
|
sig { returns(String) }
|
@@ -55,11 +64,12 @@ module Tapioca
|
|
55
64
|
|
56
65
|
DEFAULT_OPTIONS = T.let({
|
57
66
|
"postrequire" => Config::DEFAULT_POSTREQUIRE,
|
58
|
-
"outdir" =>
|
67
|
+
"outdir" => nil,
|
59
68
|
"generate_command" => default_command,
|
60
69
|
"exclude" => [],
|
61
70
|
"typed_overrides" => Config::DEFAULT_OVERRIDES,
|
62
71
|
"todos_path" => Config::DEFAULT_TODOSPATH,
|
72
|
+
"generators" => [],
|
63
73
|
}.freeze, T::Hash[String, T.untyped])
|
64
74
|
end
|
65
75
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class Class
|
5
|
+
# Returns an array with all classes that are < than its receiver.
|
6
|
+
#
|
7
|
+
# class C; end
|
8
|
+
# C.descendants # => []
|
9
|
+
#
|
10
|
+
# class B < C; end
|
11
|
+
# C.descendants # => [B]
|
12
|
+
#
|
13
|
+
# class A < B; end
|
14
|
+
# C.descendants # => [B, A]
|
15
|
+
#
|
16
|
+
# class D < C; end
|
17
|
+
# C.descendants # => [B, A, D]
|
18
|
+
def descendants
|
19
|
+
ObjectSpace.each_object(singleton_class).reject do |k|
|
20
|
+
k.singleton_class? || k == self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/tapioca/generator.rb
CHANGED
@@ -35,7 +35,7 @@ module Tapioca
|
|
35
35
|
.each do |gem|
|
36
36
|
say("Processing '#{gem.name}' gem:", :green)
|
37
37
|
indent do
|
38
|
-
|
38
|
+
compile_gem_rbi(gem)
|
39
39
|
puts
|
40
40
|
end
|
41
41
|
end
|
@@ -44,6 +44,42 @@ module Tapioca
|
|
44
44
|
say("Please review changes and commit them.", [:green, :bold])
|
45
45
|
end
|
46
46
|
|
47
|
+
sig { void }
|
48
|
+
def build_requires
|
49
|
+
requires_path = Config::DEFAULT_POSTREQUIRE
|
50
|
+
compiler = Compilers::RequiresCompiler.new(Config::SORBET_CONFIG)
|
51
|
+
name = set_color(requires_path, :yellow, :bold)
|
52
|
+
say("Compiling #{name}, this may take a few seconds... ")
|
53
|
+
|
54
|
+
rb_string = compiler.compile
|
55
|
+
if rb_string.empty?
|
56
|
+
say("Nothing to do", :green)
|
57
|
+
return
|
58
|
+
end
|
59
|
+
|
60
|
+
# Clean all existing requires before regenerating the list so we update
|
61
|
+
# it with the new one found in the client code and remove the old ones.
|
62
|
+
File.delete(requires_path) if File.exist?(requires_path)
|
63
|
+
|
64
|
+
content = String.new
|
65
|
+
content << rbi_header(
|
66
|
+
config.generate_command,
|
67
|
+
reason: "explicit gem requires",
|
68
|
+
strictness: "false"
|
69
|
+
)
|
70
|
+
content << rb_string
|
71
|
+
|
72
|
+
outdir = File.dirname(requires_path)
|
73
|
+
FileUtils.mkdir_p(outdir)
|
74
|
+
File.write(requires_path, content)
|
75
|
+
|
76
|
+
say("Done", :green)
|
77
|
+
|
78
|
+
say("All requires from this application have been written to #{name}.", [:green, :bold])
|
79
|
+
cmd = set_color("tapioca sync", :yellow, :bold)
|
80
|
+
say("Please review changes and commit them, then run #{cmd}.", [:green, :bold])
|
81
|
+
end
|
82
|
+
|
47
83
|
sig { void }
|
48
84
|
def build_todos
|
49
85
|
todos_path = config.todos_path
|
@@ -62,7 +98,11 @@ module Tapioca
|
|
62
98
|
end
|
63
99
|
|
64
100
|
content = String.new
|
65
|
-
content << rbi_header(
|
101
|
+
content << rbi_header(
|
102
|
+
config.generate_command,
|
103
|
+
reason: "unresolved constants",
|
104
|
+
strictness: "false"
|
105
|
+
)
|
66
106
|
content << rbi_string
|
67
107
|
content << "\n"
|
68
108
|
|
@@ -76,6 +116,33 @@ module Tapioca
|
|
76
116
|
say("Please review changes and commit them.", [:green, :bold])
|
77
117
|
end
|
78
118
|
|
119
|
+
sig { params(requested_constants: T::Array[String]).void }
|
120
|
+
def build_dsl(requested_constants)
|
121
|
+
load_application(eager_load: requested_constants.empty?)
|
122
|
+
load_dsl_generators
|
123
|
+
|
124
|
+
say("Compiling DSL RBI files...")
|
125
|
+
say("")
|
126
|
+
|
127
|
+
compiler = Compilers::DslCompiler.new(
|
128
|
+
requested_constants: constantize(requested_constants),
|
129
|
+
requested_generators: config.generators,
|
130
|
+
error_handler: ->(error) {
|
131
|
+
say_error(error, :bold, :red)
|
132
|
+
}
|
133
|
+
)
|
134
|
+
|
135
|
+
compiler.run do |constant, contents|
|
136
|
+
compile_dsl_rbi(constant, contents)
|
137
|
+
end
|
138
|
+
|
139
|
+
say("")
|
140
|
+
say("Done", :green)
|
141
|
+
|
142
|
+
say("All operations performed in working directory.", [:green, :bold])
|
143
|
+
say("Please review changes and commit them.", [:green, :bold])
|
144
|
+
end
|
145
|
+
|
79
146
|
sig { void }
|
80
147
|
def sync_rbis_with_gemfile
|
81
148
|
anything_done = [
|
@@ -113,15 +180,83 @@ module Tapioca
|
|
113
180
|
sig { void }
|
114
181
|
def require_gem_file
|
115
182
|
say("Requiring all gems to prepare for compiling... ")
|
116
|
-
|
183
|
+
begin
|
184
|
+
loader.load_bundle(config.prerequire, config.postrequire)
|
185
|
+
rescue LoadError => e
|
186
|
+
explain_failed_require(config.postrequire, e)
|
187
|
+
exit(1)
|
188
|
+
end
|
117
189
|
say(" Done", :green)
|
118
190
|
puts
|
119
191
|
end
|
120
192
|
|
193
|
+
sig { params(file: String, error: LoadError).void }
|
194
|
+
def explain_failed_require(file, error)
|
195
|
+
say_error("\n\nLoadError: #{error}", :bold, :red)
|
196
|
+
say_error("\nTapioca could not load all the gems required by your application.", :yellow)
|
197
|
+
say_error("If you populated ", :yellow)
|
198
|
+
say_error("#{file} ", :bold, :blue)
|
199
|
+
say_error("with ", :yellow)
|
200
|
+
say_error("tapioca require", :bold, :blue)
|
201
|
+
say_error("you should probably review it and remove the faulty line.", :yellow)
|
202
|
+
end
|
203
|
+
|
204
|
+
sig do
|
205
|
+
params(
|
206
|
+
message: String,
|
207
|
+
color: T.any(Symbol, T::Array[Symbol]),
|
208
|
+
).void
|
209
|
+
end
|
210
|
+
def say_error(message = "", *color)
|
211
|
+
force_new_line = (message.to_s !~ /( |\t)\Z/)
|
212
|
+
buffer = prepare_message(*T.unsafe([message, *T.unsafe(color)]))
|
213
|
+
buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
|
214
|
+
|
215
|
+
stderr.print(buffer)
|
216
|
+
stderr.flush
|
217
|
+
end
|
218
|
+
|
219
|
+
sig { params(eager_load: T::Boolean).void }
|
220
|
+
def load_application(eager_load:)
|
221
|
+
say("Loading Rails application... ")
|
222
|
+
|
223
|
+
loader.load_rails(
|
224
|
+
environment_load: true,
|
225
|
+
eager_load: eager_load
|
226
|
+
)
|
227
|
+
|
228
|
+
say("Done", :green)
|
229
|
+
end
|
230
|
+
|
231
|
+
sig { void }
|
232
|
+
def load_dsl_generators
|
233
|
+
say("Loading DSL generator classes... ")
|
234
|
+
|
235
|
+
Dir.glob([
|
236
|
+
"#{__dir__}/compilers/dsl/*.rb",
|
237
|
+
"#{Config::TAPIOCA_PATH}/generators/**/*.rb",
|
238
|
+
]).each do |generator|
|
239
|
+
require File.expand_path(generator)
|
240
|
+
end
|
241
|
+
|
242
|
+
say("Done", :green)
|
243
|
+
end
|
244
|
+
|
245
|
+
sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
|
246
|
+
def constantize(constant_names)
|
247
|
+
constant_names.map do |name|
|
248
|
+
begin
|
249
|
+
name.constantize
|
250
|
+
rescue NameError
|
251
|
+
nil
|
252
|
+
end
|
253
|
+
end.compact
|
254
|
+
end
|
255
|
+
|
121
256
|
sig { returns(T::Hash[String, String]) }
|
122
257
|
def existing_rbis
|
123
258
|
@existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s)
|
124
|
-
.map { |f| f.basename(".*").to_s.split('@') }
|
259
|
+
.map { |f| T.cast(f.basename(".*").to_s.split('@', 2), [String, String]) }
|
125
260
|
.to_h
|
126
261
|
end
|
127
262
|
|
@@ -134,22 +269,22 @@ module Tapioca
|
|
134
269
|
end
|
135
270
|
|
136
271
|
sig { params(gem_name: String, version: String).returns(Pathname) }
|
137
|
-
def
|
272
|
+
def gem_rbi_filename(gem_name, version)
|
138
273
|
config.outpath / "#{gem_name}@#{version}.rbi"
|
139
274
|
end
|
140
275
|
|
141
276
|
sig { params(gem_name: String).returns(Pathname) }
|
142
277
|
def existing_rbi(gem_name)
|
143
|
-
|
278
|
+
gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
|
144
279
|
end
|
145
280
|
|
146
281
|
sig { params(gem_name: String).returns(Pathname) }
|
147
282
|
def expected_rbi(gem_name)
|
148
|
-
|
283
|
+
gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
|
149
284
|
end
|
150
285
|
|
151
286
|
sig { params(gem_name: String).returns(T::Boolean) }
|
152
|
-
def
|
287
|
+
def gem_rbi_exists?(gem_name)
|
153
288
|
existing_rbis.key?(gem_name)
|
154
289
|
end
|
155
290
|
|
@@ -227,13 +362,13 @@ module Tapioca
|
|
227
362
|
gems.each do |gem_name|
|
228
363
|
filename = expected_rbi(gem_name)
|
229
364
|
|
230
|
-
if
|
365
|
+
if gem_rbi_exists?(gem_name)
|
231
366
|
old_filename = existing_rbi(gem_name)
|
232
367
|
move(old_filename, filename) unless old_filename == filename
|
233
368
|
end
|
234
369
|
|
235
370
|
gem = T.must(bundle.gem(gem_name))
|
236
|
-
|
371
|
+
compile_gem_rbi(gem)
|
237
372
|
add(filename)
|
238
373
|
|
239
374
|
puts
|
@@ -265,27 +400,36 @@ module Tapioca
|
|
265
400
|
end
|
266
401
|
end
|
267
402
|
|
268
|
-
sig { params(command: String,
|
269
|
-
def rbi_header(command,
|
270
|
-
<<~HEAD
|
271
|
-
#
|
272
|
-
#
|
403
|
+
sig { params(command: String, reason: T.nilable(String), strictness: T.nilable(String)).returns(String) }
|
404
|
+
def rbi_header(command, reason: nil, strictness: nil)
|
405
|
+
statement = <<~HEAD
|
406
|
+
# DO NOT EDIT MANUALLY
|
407
|
+
# This is an autogenerated file for #{reason}.
|
408
|
+
# Please instead update this file by running `#{command}`.
|
409
|
+
HEAD
|
273
410
|
|
274
|
-
|
411
|
+
sigil = <<~SIGIL if strictness
|
412
|
+
# typed: #{strictness}
|
413
|
+
SIGIL
|
275
414
|
|
276
|
-
|
415
|
+
[statement, sigil].compact.join("\n").strip.concat("\n\n")
|
277
416
|
end
|
278
417
|
|
279
418
|
sig { params(gem: Gemfile::Gem).void }
|
280
|
-
def
|
419
|
+
def compile_gem_rbi(gem)
|
281
420
|
compiler = Compilers::SymbolTableCompiler.new
|
282
421
|
gem_name = set_color(gem.name, :yellow, :bold)
|
283
422
|
say("Compiling #{gem_name}, this may take a few seconds... ")
|
284
423
|
|
285
|
-
|
424
|
+
strictness = config.typed_overrides[gem.name] || "true"
|
286
425
|
|
287
|
-
content =
|
288
|
-
content
|
426
|
+
content = String.new
|
427
|
+
content << rbi_header(
|
428
|
+
config.generate_command,
|
429
|
+
reason: "types exported from the `#{gem.name}` gem",
|
430
|
+
strictness: strictness
|
431
|
+
)
|
432
|
+
content << compiler.compile(gem)
|
289
433
|
|
290
434
|
FileUtils.mkdir_p(config.outdir)
|
291
435
|
filename = config.outpath / gem.rbi_file_name
|
@@ -297,5 +441,27 @@ module Tapioca
|
|
297
441
|
remove(file) unless file.basename.to_s == gem.rbi_file_name
|
298
442
|
end
|
299
443
|
end
|
444
|
+
|
445
|
+
sig { params(constant: Module, contents: String).void }
|
446
|
+
def compile_dsl_rbi(constant, contents)
|
447
|
+
return if contents.nil?
|
448
|
+
|
449
|
+
command = format(config.generate_command, constant.name)
|
450
|
+
constant_name = Module.instance_method(:name).bind(constant).call
|
451
|
+
rbi_name = constant_name.underscore + ".rbi"
|
452
|
+
filename = config.outpath / rbi_name
|
453
|
+
|
454
|
+
out = String.new
|
455
|
+
out << rbi_header(
|
456
|
+
command,
|
457
|
+
reason: "dynamic methods in `#{constant.name}`"
|
458
|
+
)
|
459
|
+
out << contents
|
460
|
+
|
461
|
+
FileUtils.mkdir_p(File.dirname(filename))
|
462
|
+
File.write(filename, out)
|
463
|
+
say("Wrote: ", [:green])
|
464
|
+
say(filename)
|
465
|
+
end
|
300
466
|
end
|
301
467
|
end
|
data/lib/tapioca/loader.rb
CHANGED
@@ -24,6 +24,24 @@ module Tapioca
|
|
24
24
|
load_rails_engines
|
25
25
|
end
|
26
26
|
|
27
|
+
sig { params(environment_load: T::Boolean, eager_load: T::Boolean).void }
|
28
|
+
def load_rails(environment_load: false, eager_load: false)
|
29
|
+
return unless File.exist?("config/application.rb")
|
30
|
+
|
31
|
+
safe_require("rails")
|
32
|
+
|
33
|
+
silence_deprecations
|
34
|
+
|
35
|
+
safe_require("rails/generators/test_case")
|
36
|
+
if environment_load
|
37
|
+
safe_require("./config/environment")
|
38
|
+
else
|
39
|
+
safe_require("./config/application")
|
40
|
+
end
|
41
|
+
|
42
|
+
eager_load_rails_app if eager_load
|
43
|
+
end
|
44
|
+
|
27
45
|
private
|
28
46
|
|
29
47
|
sig { returns(Tapioca::Gemfile) }
|
@@ -80,15 +98,8 @@ module Tapioca
|
|
80
98
|
end
|
81
99
|
|
82
100
|
sig { void }
|
83
|
-
def
|
84
|
-
|
85
|
-
|
86
|
-
safe_require("rails")
|
87
|
-
|
88
|
-
silence_deprecations
|
89
|
-
|
90
|
-
safe_require("rails/generators/test_case")
|
91
|
-
safe_require("./config/application")
|
101
|
+
def eager_load_rails_app
|
102
|
+
Object.const_get("Rails").autoloaders.each(&:eager_load)
|
92
103
|
end
|
93
104
|
|
94
105
|
sig { void }
|