tapioca 0.4.13 → 0.4.18
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 +0 -1
- data/README.md +4 -2
- data/exe/tapioca +17 -2
- data/lib/tapioca.rb +1 -27
- data/lib/tapioca/cli.rb +1 -108
- data/lib/tapioca/cli/main.rb +136 -0
- data/lib/tapioca/compilers/dsl/active_record_associations.rb +38 -7
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +4 -4
- data/lib/tapioca/compilers/dsl/active_record_scope.rb +1 -1
- data/lib/tapioca/compilers/dsl/base.rb +1 -1
- data/lib/tapioca/compilers/dsl/url_helpers.rb +3 -3
- data/lib/tapioca/compilers/sorbet.rb +4 -1
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +152 -49
- data/lib/tapioca/config.rb +1 -1
- data/lib/tapioca/config_builder.rb +7 -12
- data/lib/tapioca/gemfile.rb +30 -22
- data/lib/tapioca/generator.rb +127 -24
- data/lib/tapioca/generic_type_registry.rb +170 -0
- data/lib/tapioca/internal.rb +21 -0
- data/lib/tapioca/loader.rb +13 -2
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +66 -0
- data/lib/tapioca/sorbet_ext/name_patch.rb +16 -0
- data/lib/tapioca/version.rb +1 -1
- metadata +21 -2
data/lib/tapioca/generator.rb
CHANGED
@@ -63,7 +63,7 @@ module Tapioca
|
|
63
63
|
|
64
64
|
content = String.new
|
65
65
|
content << rbi_header(
|
66
|
-
|
66
|
+
"#{Config::DEFAULT_COMMAND} require",
|
67
67
|
reason: "explicit gem requires",
|
68
68
|
strictness: "false"
|
69
69
|
)
|
@@ -76,8 +76,8 @@ module Tapioca
|
|
76
76
|
say("Done", :green)
|
77
77
|
|
78
78
|
say("All requires from this application have been written to #{name}.", [:green, :bold])
|
79
|
-
cmd = set_color("
|
80
|
-
say("Please review changes and commit them, then run
|
79
|
+
cmd = set_color("#{Config::DEFAULT_COMMAND} sync", :yellow, :bold)
|
80
|
+
say("Please review changes and commit them, then run `#{cmd}`.", [:green, :bold])
|
81
81
|
end
|
82
82
|
|
83
83
|
sig { void }
|
@@ -99,7 +99,7 @@ module Tapioca
|
|
99
99
|
|
100
100
|
content = String.new
|
101
101
|
content << rbi_header(
|
102
|
-
|
102
|
+
"#{Config::DEFAULT_COMMAND} todo",
|
103
103
|
reason: "unresolved constants",
|
104
104
|
strictness: "false"
|
105
105
|
)
|
@@ -116,14 +116,26 @@ module Tapioca
|
|
116
116
|
say("Please review changes and commit them.", [:green, :bold])
|
117
117
|
end
|
118
118
|
|
119
|
-
sig
|
120
|
-
|
119
|
+
sig do
|
120
|
+
params(
|
121
|
+
requested_constants: T::Array[String],
|
122
|
+
should_verify: T::Boolean,
|
123
|
+
).void
|
124
|
+
end
|
125
|
+
def build_dsl(requested_constants, should_verify: false)
|
121
126
|
load_application(eager_load: requested_constants.empty?)
|
122
127
|
load_dsl_generators
|
123
128
|
|
124
|
-
|
129
|
+
if should_verify
|
130
|
+
say("Checking for out-of-date RBIs...")
|
131
|
+
else
|
132
|
+
say("Compiling DSL RBI files...")
|
133
|
+
end
|
125
134
|
say("")
|
126
135
|
|
136
|
+
outpath = should_verify ? Dir.mktmpdir : config.outpath
|
137
|
+
rbi_files_to_purge = existing_rbi_filenames(requested_constants)
|
138
|
+
|
127
139
|
compiler = Compilers::DslCompiler.new(
|
128
140
|
requested_constants: constantize(requested_constants),
|
129
141
|
requested_generators: config.generators,
|
@@ -133,14 +145,21 @@ module Tapioca
|
|
133
145
|
)
|
134
146
|
|
135
147
|
compiler.run do |constant, contents|
|
136
|
-
compile_dsl_rbi(constant, contents)
|
148
|
+
filename = compile_dsl_rbi(constant, contents, outpath: Pathname.new(outpath))
|
149
|
+
rbi_files_to_purge.delete(filename) if filename
|
137
150
|
end
|
138
|
-
|
139
151
|
say("")
|
140
|
-
say("Done", :green)
|
141
152
|
|
142
|
-
|
143
|
-
|
153
|
+
if should_verify
|
154
|
+
perform_dsl_verification(outpath)
|
155
|
+
else
|
156
|
+
purge_stale_dsl_rbi_files(rbi_files_to_purge)
|
157
|
+
|
158
|
+
say("Done", :green)
|
159
|
+
|
160
|
+
say("All operations performed in working directory.", [:green, :bold])
|
161
|
+
say("Please review changes and commit them.", [:green, :bold])
|
162
|
+
end
|
144
163
|
end
|
145
164
|
|
146
165
|
sig { void }
|
@@ -192,6 +211,10 @@ module Tapioca
|
|
192
211
|
exit(1)
|
193
212
|
end
|
194
213
|
say(" Done", :green)
|
214
|
+
unless bundle.missing_specs.empty?
|
215
|
+
say(" completed with missing specs: ")
|
216
|
+
say(bundle.missing_specs.join(', '), :yellow)
|
217
|
+
end
|
195
218
|
puts
|
196
219
|
end
|
197
220
|
|
@@ -202,7 +225,7 @@ module Tapioca
|
|
202
225
|
say_error("If you populated ", :yellow)
|
203
226
|
say_error("#{file} ", :bold, :blue)
|
204
227
|
say_error("with ", :yellow)
|
205
|
-
say_error("
|
228
|
+
say_error("`#{Config::DEFAULT_COMMAND} require`", :bold, :blue)
|
206
229
|
say_error("you should probably review it and remove the faulty line.", :yellow)
|
207
230
|
end
|
208
231
|
|
@@ -249,13 +272,38 @@ module Tapioca
|
|
249
272
|
|
250
273
|
sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
|
251
274
|
def constantize(constant_names)
|
252
|
-
constant_names.map do |name|
|
275
|
+
constant_map = constant_names.map do |name|
|
253
276
|
begin
|
254
|
-
name.constantize
|
277
|
+
[name, name.constantize]
|
255
278
|
rescue NameError
|
256
|
-
nil
|
279
|
+
[name, nil]
|
280
|
+
end
|
281
|
+
end.to_h
|
282
|
+
|
283
|
+
unprocessable_constants = constant_map.select { |_, v| v.nil? }
|
284
|
+
unless unprocessable_constants.empty?
|
285
|
+
unprocessable_constants.each do |name, _|
|
286
|
+
say("Error: Cannot find constant '#{name}'", :red)
|
287
|
+
remove(dsl_rbi_filename(name))
|
288
|
+
end
|
289
|
+
|
290
|
+
exit(1)
|
291
|
+
end
|
292
|
+
|
293
|
+
constant_map.values
|
294
|
+
end
|
295
|
+
|
296
|
+
sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) }
|
297
|
+
def existing_rbi_filenames(requested_constants, path: config.outpath)
|
298
|
+
filenames = if requested_constants.empty?
|
299
|
+
Pathname.glob(path / "**/*.rbi")
|
300
|
+
else
|
301
|
+
requested_constants.map do |constant_name|
|
302
|
+
dsl_rbi_filename(constant_name)
|
257
303
|
end
|
258
|
-
end
|
304
|
+
end
|
305
|
+
|
306
|
+
filenames.to_set
|
259
307
|
end
|
260
308
|
|
261
309
|
sig { returns(T::Hash[String, String]) }
|
@@ -273,6 +321,11 @@ module Tapioca
|
|
273
321
|
.to_h
|
274
322
|
end
|
275
323
|
|
324
|
+
sig { params(constant_name: String).returns(Pathname) }
|
325
|
+
def dsl_rbi_filename(constant_name)
|
326
|
+
config.outpath / "#{constant_name.underscore}.rbi"
|
327
|
+
end
|
328
|
+
|
276
329
|
sig { params(gem_name: String, version: String).returns(Pathname) }
|
277
330
|
def gem_rbi_filename(gem_name, version)
|
278
331
|
config.outpath / "#{gem_name}@#{version}.rbi"
|
@@ -312,6 +365,7 @@ module Tapioca
|
|
312
365
|
|
313
366
|
sig { params(filename: Pathname).void }
|
314
367
|
def remove(filename)
|
368
|
+
return unless filename.exist?
|
315
369
|
say("-- Removing: #{filename}")
|
316
370
|
filename.unlink
|
317
371
|
end
|
@@ -430,7 +484,7 @@ module Tapioca
|
|
430
484
|
rbi_body_content = compiler.compile(gem)
|
431
485
|
content = String.new
|
432
486
|
content << rbi_header(
|
433
|
-
|
487
|
+
"#{Config::DEFAULT_COMMAND} sync",
|
434
488
|
reason: "types exported from the `#{gem.name}` gem",
|
435
489
|
strictness: strictness
|
436
490
|
)
|
@@ -447,23 +501,22 @@ module Tapioca
|
|
447
501
|
end
|
448
502
|
File.write(filename.to_s, content)
|
449
503
|
|
450
|
-
Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
|
504
|
+
T.unsafe(Pathname).glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
|
451
505
|
remove(file) unless file.basename.to_s == gem.rbi_file_name
|
452
506
|
end
|
453
507
|
end
|
454
508
|
|
455
|
-
sig { params(constant: Module, contents: String).
|
456
|
-
def compile_dsl_rbi(constant, contents)
|
509
|
+
sig { params(constant: Module, contents: String, outpath: Pathname).returns(T.nilable(Pathname)) }
|
510
|
+
def compile_dsl_rbi(constant, contents, outpath: config.outpath)
|
457
511
|
return if contents.nil?
|
458
512
|
|
459
|
-
command = format(config.generate_command, constant.name)
|
460
513
|
constant_name = Module.instance_method(:name).bind(constant).call
|
461
514
|
rbi_name = constant_name.underscore + ".rbi"
|
462
|
-
filename =
|
515
|
+
filename = outpath / rbi_name
|
463
516
|
|
464
517
|
out = String.new
|
465
518
|
out << rbi_header(
|
466
|
-
|
519
|
+
"#{Config::DEFAULT_COMMAND} dsl #{constant_name}",
|
467
520
|
reason: "dynamic methods in `#{constant.name}`"
|
468
521
|
)
|
469
522
|
out << contents
|
@@ -472,6 +525,56 @@ module Tapioca
|
|
472
525
|
File.write(filename, out)
|
473
526
|
say("Wrote: ", [:green])
|
474
527
|
say(filename)
|
528
|
+
|
529
|
+
filename
|
530
|
+
end
|
531
|
+
|
532
|
+
sig { params(tmp_dir: Pathname).returns(T.nilable(String)) }
|
533
|
+
def verify_dsl_rbi(tmp_dir:)
|
534
|
+
existing_rbis = existing_rbi_filenames([]).sort
|
535
|
+
new_rbis = existing_rbi_filenames([], path: tmp_dir).grep_v(/gem|shim/).sort
|
536
|
+
|
537
|
+
return "New file(s) introduced." if existing_rbis.length != new_rbis.length
|
538
|
+
|
539
|
+
desynced_files = []
|
540
|
+
|
541
|
+
(0..existing_rbis.length - 1).each do |i|
|
542
|
+
desynced_files << new_rbis[i] unless FileUtils.identical?(existing_rbis[i], new_rbis[i])
|
543
|
+
end
|
544
|
+
|
545
|
+
unless desynced_files.empty?
|
546
|
+
filenames = desynced_files.map { |f| f.to_s.sub!(tmp_dir.to_s, "sorbet/rbi/dsl") }.join("\n - ")
|
547
|
+
|
548
|
+
return "File(s) updated:\n - #{filenames}"
|
549
|
+
end
|
550
|
+
|
551
|
+
nil
|
552
|
+
end
|
553
|
+
|
554
|
+
sig { params(dir: String).void }
|
555
|
+
def perform_dsl_verification(dir)
|
556
|
+
if (error = verify_dsl_rbi(tmp_dir: Pathname.new(dir)))
|
557
|
+
say("RBI files are out-of-date, please run `#{Config::DEFAULT_COMMAND} dsl` to update.")
|
558
|
+
say("Reason: ", [:red])
|
559
|
+
say(error)
|
560
|
+
exit(1)
|
561
|
+
else
|
562
|
+
say("Nothing to do, all RBIs are up-to-date.")
|
563
|
+
end
|
564
|
+
ensure
|
565
|
+
FileUtils.remove_entry(dir)
|
566
|
+
end
|
567
|
+
|
568
|
+
sig { params(files: T::Set[Pathname]).void }
|
569
|
+
def purge_stale_dsl_rbi_files(files)
|
570
|
+
if files.any?
|
571
|
+
say("Removing stale RBI files...")
|
572
|
+
|
573
|
+
files.sort.each do |filename|
|
574
|
+
remove(filename)
|
575
|
+
end
|
576
|
+
say("")
|
577
|
+
end
|
475
578
|
end
|
476
579
|
end
|
477
580
|
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Tapioca
|
5
|
+
# This class is responsible for storing and looking up information related to generic types.
|
6
|
+
#
|
7
|
+
# The class stores 2 different kinds of data, in two separate lookup tables:
|
8
|
+
# 1. a lookup of generic type instances by name: `@generic_instances`
|
9
|
+
# 2. a lookup of type variable serializer by constant and type variable
|
10
|
+
# instance: `@type_variables`
|
11
|
+
#
|
12
|
+
# By storing the above data, we can cheaply query each constant against this registry
|
13
|
+
# to see if it declares any generic type variables. This becomes a simple lookup in the
|
14
|
+
# `@type_variables` hash table with the given constant.
|
15
|
+
#
|
16
|
+
# If there is no entry, then we can cheaply know that we can skip generic type
|
17
|
+
# information generation for this type.
|
18
|
+
#
|
19
|
+
# On the other hand, if we get a result, then the result will be a hash of type
|
20
|
+
# variable to type variable serializers. This allows us to associate type variables
|
21
|
+
# to the constant names that represent them, easily.
|
22
|
+
module GenericTypeRegistry
|
23
|
+
@generic_instances = T.let(
|
24
|
+
{},
|
25
|
+
T::Hash[String, Module]
|
26
|
+
)
|
27
|
+
|
28
|
+
@type_variables = T.let(
|
29
|
+
{},
|
30
|
+
T::Hash[Integer, T::Hash[Integer, String]]
|
31
|
+
)
|
32
|
+
|
33
|
+
class << self
|
34
|
+
extend T::Sig
|
35
|
+
|
36
|
+
# This method is responsible for building the name of the instantiated concrete type
|
37
|
+
# and cloning the given constant so that we can return a type that is the same
|
38
|
+
# as the current type but is a different instance and has a different name method.
|
39
|
+
#
|
40
|
+
# We cache those cloned instances by their name in `@generic_instances`, so that
|
41
|
+
# we don't keep instantiating a new type every single time it is referenced.
|
42
|
+
# For example, `[Foo[Integer], Foo[Integer], Foo[Integer], Foo[String]]` will only
|
43
|
+
# result in 2 clones (1 for `Foo[Integer]` and another for `Foo[String]`) and
|
44
|
+
# 2 hash lookups (for the other two `Foo[Integer]`s).
|
45
|
+
#
|
46
|
+
# This method returns the created or cached clone of the constant.
|
47
|
+
sig { params(constant: T.untyped, types: T.untyped).returns(Module) }
|
48
|
+
def register_type(constant, types)
|
49
|
+
# Build the name of the instantiated generic type,
|
50
|
+
# something like `"Foo[X, Y, Z]"`
|
51
|
+
type_list = types.map { |type| T::Utils.coerce(type).name }.join(", ")
|
52
|
+
name = "#{name_of(constant)}[#{type_list}]"
|
53
|
+
|
54
|
+
# Create a clone of the constant with an overridden `name`
|
55
|
+
# method that returns the name we constructed above.
|
56
|
+
#
|
57
|
+
# Also, we try to memoize the clone based on the name, so that
|
58
|
+
# we don't have to keep recreating clones all the time.
|
59
|
+
@generic_instances[name] ||= constant.clone.tap do |clone|
|
60
|
+
clone.define_singleton_method(:name) { name }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
sig do
|
65
|
+
params(
|
66
|
+
constant: T.untyped,
|
67
|
+
type_member: T::Types::TypeVariable,
|
68
|
+
fixed: T.untyped,
|
69
|
+
lower: T.untyped,
|
70
|
+
upper: T.untyped
|
71
|
+
).void
|
72
|
+
end
|
73
|
+
def register_type_member(constant, type_member, fixed, lower, upper)
|
74
|
+
register_type_variable(constant, :type_member, type_member, fixed, lower, upper)
|
75
|
+
end
|
76
|
+
|
77
|
+
sig do
|
78
|
+
params(
|
79
|
+
constant: T.untyped,
|
80
|
+
type_template: T::Types::TypeVariable,
|
81
|
+
fixed: T.untyped,
|
82
|
+
lower: T.untyped,
|
83
|
+
upper: T.untyped
|
84
|
+
).void
|
85
|
+
end
|
86
|
+
def register_type_template(constant, type_template, fixed, lower, upper)
|
87
|
+
register_type_variable(constant, :type_template, type_template, fixed, lower, upper)
|
88
|
+
end
|
89
|
+
|
90
|
+
sig { params(constant: Module).returns(T.nilable(T::Hash[Integer, String])) }
|
91
|
+
def lookup_type_variables(constant)
|
92
|
+
@type_variables[object_id_of(constant)]
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# This method is called from intercepted calls to `type_member` and `type_template`.
|
98
|
+
# We get passed all the arguments to those methods, as well as the `T::Types::TypeVariable`
|
99
|
+
# instance generated by the Sorbet defined `type_member`/`type_template` call on `T::Generic`.
|
100
|
+
#
|
101
|
+
# This method creates a `String` with that data and stores it in the
|
102
|
+
# `@type_variables` lookup table, keyed by the `constant` and `type_variable`.
|
103
|
+
#
|
104
|
+
# Finally, the original `type_variable` is returned from this method, so that the caller
|
105
|
+
# can return it from the original methods as well.
|
106
|
+
sig do
|
107
|
+
params(
|
108
|
+
constant: T.untyped,
|
109
|
+
type_variable_type: T.enum([:type_member, :type_template]),
|
110
|
+
type_variable: T::Types::TypeVariable,
|
111
|
+
fixed: T.untyped,
|
112
|
+
lower: T.untyped,
|
113
|
+
upper: T.untyped
|
114
|
+
).void
|
115
|
+
end
|
116
|
+
# rubocop:disable Metrics/ParameterLists
|
117
|
+
def register_type_variable(constant, type_variable_type, type_variable, fixed, lower, upper)
|
118
|
+
# rubocop:enable Metrics/ParameterLists
|
119
|
+
type_variables = lookup_or_initialize_type_variables(constant)
|
120
|
+
|
121
|
+
type_variables[object_id_of(type_variable)] = serialize_type_variable(
|
122
|
+
type_variable_type,
|
123
|
+
type_variable.variance,
|
124
|
+
fixed,
|
125
|
+
lower,
|
126
|
+
upper
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
sig { params(constant: Module).returns(T::Hash[Integer, String]) }
|
131
|
+
def lookup_or_initialize_type_variables(constant)
|
132
|
+
@type_variables[object_id_of(constant)] ||= {}
|
133
|
+
end
|
134
|
+
|
135
|
+
sig do
|
136
|
+
params(
|
137
|
+
type_variable_type: Symbol,
|
138
|
+
variance: Symbol,
|
139
|
+
fixed: T.untyped,
|
140
|
+
lower: T.untyped,
|
141
|
+
upper: T.untyped
|
142
|
+
).returns(String)
|
143
|
+
end
|
144
|
+
def serialize_type_variable(type_variable_type, variance, fixed, lower, upper)
|
145
|
+
parts = []
|
146
|
+
parts << ":#{variance}" unless variance == :invariant
|
147
|
+
parts << "fixed: #{fixed}" if fixed
|
148
|
+
parts << "lower: #{lower}" unless lower == T.untyped
|
149
|
+
parts << "upper: #{upper}" unless upper == BasicObject
|
150
|
+
|
151
|
+
parameters = parts.join(", ")
|
152
|
+
|
153
|
+
serialized = T.let(type_variable_type.to_s, String)
|
154
|
+
serialized += "(#{parameters})" unless parameters.empty?
|
155
|
+
|
156
|
+
serialized
|
157
|
+
end
|
158
|
+
|
159
|
+
sig { params(constant: Module).returns(T.nilable(String)) }
|
160
|
+
def name_of(constant)
|
161
|
+
Module.instance_method(:name).bind(constant).call
|
162
|
+
end
|
163
|
+
|
164
|
+
sig { params(object: BasicObject).returns(Integer) }
|
165
|
+
def object_id_of(object)
|
166
|
+
Object.instance_method(:object_id).bind(object).call
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "tapioca"
|
5
|
+
require "tapioca/loader"
|
6
|
+
require "tapioca/constant_locator"
|
7
|
+
require "tapioca/generic_type_registry"
|
8
|
+
require "tapioca/sorbet_ext/generic_name_patch"
|
9
|
+
require "tapioca/config"
|
10
|
+
require "tapioca/config_builder"
|
11
|
+
require "tapioca/generator"
|
12
|
+
require "tapioca/cli"
|
13
|
+
require "tapioca/cli/main"
|
14
|
+
require "tapioca/gemfile"
|
15
|
+
require "tapioca/compilers/sorbet"
|
16
|
+
require "tapioca/compilers/requires_compiler"
|
17
|
+
require "tapioca/compilers/symbol_table_compiler"
|
18
|
+
require "tapioca/compilers/symbol_table/symbol_generator"
|
19
|
+
require "tapioca/compilers/symbol_table/symbol_loader"
|
20
|
+
require "tapioca/compilers/todos_compiler"
|
21
|
+
require "tapioca/compilers/dsl_compiler"
|