tapioca 0.4.13 → 0.4.18
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|