tapioca 0.5.1 → 0.5.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 +1 -1
- data/lib/tapioca/cli.rb +54 -139
- data/lib/tapioca/compilers/dsl/active_model_secure_password.rb +101 -0
- data/lib/tapioca/compilers/dsl/active_record_enum.rb +1 -1
- data/lib/tapioca/compilers/dsl/active_record_fixtures.rb +86 -0
- data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +41 -33
- data/lib/tapioca/compilers/dsl/active_support_concern.rb +0 -2
- data/lib/tapioca/compilers/dsl/base.rb +12 -0
- data/lib/tapioca/compilers/dsl/identity_cache.rb +0 -1
- data/lib/tapioca/compilers/dsl/mixed_in_class_attributes.rb +74 -0
- data/lib/tapioca/compilers/dsl/smart_properties.rb +4 -4
- data/lib/tapioca/compilers/dsl_compiler.rb +7 -6
- data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +198 -0
- data/lib/tapioca/compilers/sorbet.rb +0 -1
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +84 -153
- data/lib/tapioca/compilers/symbol_table_compiler.rb +5 -11
- data/lib/tapioca/config.rb +1 -0
- data/lib/tapioca/config_builder.rb +1 -0
- data/lib/tapioca/constant_locator.rb +7 -1
- data/lib/tapioca/gemfile.rb +11 -5
- data/lib/tapioca/generators/base.rb +61 -0
- data/lib/tapioca/generators/dsl.rb +362 -0
- data/lib/tapioca/generators/gem.rb +345 -0
- data/lib/tapioca/generators/init.rb +79 -0
- data/lib/tapioca/generators/require.rb +52 -0
- data/lib/tapioca/generators/todo.rb +76 -0
- data/lib/tapioca/generators.rb +9 -0
- data/lib/tapioca/internal.rb +1 -2
- data/lib/tapioca/loader.rb +2 -2
- data/lib/tapioca/rbi_ext/model.rb +44 -0
- data/lib/tapioca/reflection.rb +8 -1
- data/lib/tapioca/version.rb +1 -1
- data/lib/tapioca.rb +2 -0
- metadata +34 -12
- data/lib/tapioca/generator.rb +0 -717
data/lib/tapioca/generator.rb
DELETED
@@ -1,717 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
require "pathname"
|
5
|
-
require "thor"
|
6
|
-
|
7
|
-
module Tapioca
|
8
|
-
class Generator < ::Thor::Shell::Color
|
9
|
-
extend(T::Sig)
|
10
|
-
|
11
|
-
sig { returns(Config) }
|
12
|
-
attr_reader :config
|
13
|
-
|
14
|
-
sig do
|
15
|
-
params(
|
16
|
-
config: Config
|
17
|
-
).void
|
18
|
-
end
|
19
|
-
def initialize(config)
|
20
|
-
@config = config
|
21
|
-
@bundle = T.let(nil, T.nilable(Gemfile))
|
22
|
-
@loader = T.let(nil, T.nilable(Loader))
|
23
|
-
@compiler = T.let(nil, T.nilable(Compilers::SymbolTableCompiler))
|
24
|
-
@existing_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
|
25
|
-
@expected_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
|
26
|
-
super()
|
27
|
-
end
|
28
|
-
|
29
|
-
sig { params(gem_names: T::Array[String]).void }
|
30
|
-
def build_gem_rbis(gem_names)
|
31
|
-
require_gem_file
|
32
|
-
|
33
|
-
gems_to_generate(gem_names)
|
34
|
-
.reject { |gem| config.exclude.include?(gem.name) }
|
35
|
-
.each do |gem|
|
36
|
-
say("Processing '#{gem.name}' gem:", :green)
|
37
|
-
indent do
|
38
|
-
compile_gem_rbi(gem)
|
39
|
-
puts
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
say("All operations performed in working directory.", [:green, :bold])
|
44
|
-
say("Please review changes and commit them.", [:green, :bold])
|
45
|
-
end
|
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 << "# typed: true\n"
|
66
|
-
content << "# frozen_string_literal: true\n\n"
|
67
|
-
content << rb_string
|
68
|
-
|
69
|
-
outdir = File.dirname(requires_path)
|
70
|
-
FileUtils.mkdir_p(outdir)
|
71
|
-
File.write(requires_path, content)
|
72
|
-
|
73
|
-
say("Done", :green)
|
74
|
-
|
75
|
-
say("All requires from this application have been written to #{name}.", [:green, :bold])
|
76
|
-
cmd = set_color("#{Config::DEFAULT_COMMAND} sync", :yellow, :bold)
|
77
|
-
say("Please review changes and commit them, then run `#{cmd}`.", [:green, :bold])
|
78
|
-
end
|
79
|
-
|
80
|
-
sig { void }
|
81
|
-
def build_todos
|
82
|
-
todos_path = config.todos_path
|
83
|
-
compiler = Compilers::TodosCompiler.new
|
84
|
-
name = set_color(todos_path, :yellow, :bold)
|
85
|
-
say("Compiling #{name}, this may take a few seconds... ")
|
86
|
-
|
87
|
-
# Clean all existing unresolved constants before regenerating the list
|
88
|
-
# so Sorbet won't grab them as already resolved.
|
89
|
-
File.delete(todos_path) if File.exist?(todos_path)
|
90
|
-
|
91
|
-
rbi_string = compiler.compile
|
92
|
-
if rbi_string.empty?
|
93
|
-
say("Nothing to do", :green)
|
94
|
-
return
|
95
|
-
end
|
96
|
-
|
97
|
-
content = String.new
|
98
|
-
content << rbi_header(
|
99
|
-
"#{Config::DEFAULT_COMMAND} todo",
|
100
|
-
reason: "unresolved constants",
|
101
|
-
strictness: "false"
|
102
|
-
)
|
103
|
-
content << rbi_string
|
104
|
-
content << "\n"
|
105
|
-
|
106
|
-
outdir = File.dirname(todos_path)
|
107
|
-
FileUtils.mkdir_p(outdir)
|
108
|
-
File.write(todos_path, content)
|
109
|
-
|
110
|
-
say("Done", :green)
|
111
|
-
|
112
|
-
say("All unresolved constants have been written to #{name}.", [:green, :bold])
|
113
|
-
say("Please review changes and commit them.", [:green, :bold])
|
114
|
-
end
|
115
|
-
|
116
|
-
sig do
|
117
|
-
params(
|
118
|
-
requested_constants: T::Array[String],
|
119
|
-
should_verify: T::Boolean,
|
120
|
-
quiet: T::Boolean,
|
121
|
-
verbose: T::Boolean
|
122
|
-
).void
|
123
|
-
end
|
124
|
-
def build_dsl(requested_constants, should_verify: false, quiet: false, verbose: false)
|
125
|
-
load_application(eager_load: requested_constants.empty?)
|
126
|
-
abort_if_pending_migrations!
|
127
|
-
load_dsl_generators
|
128
|
-
|
129
|
-
if should_verify
|
130
|
-
say("Checking for out-of-date RBIs...")
|
131
|
-
else
|
132
|
-
say("Compiling DSL RBI files...")
|
133
|
-
end
|
134
|
-
say("")
|
135
|
-
|
136
|
-
outpath = should_verify ? Pathname.new(Dir.mktmpdir) : config.outpath
|
137
|
-
rbi_files_to_purge = existing_rbi_filenames(requested_constants)
|
138
|
-
|
139
|
-
compiler = Compilers::DslCompiler.new(
|
140
|
-
requested_constants: constantize(requested_constants),
|
141
|
-
requested_generators: constantize_generators(config.generators),
|
142
|
-
excluded_generators: constantize_generators(config.exclude_generators),
|
143
|
-
error_handler: ->(error) {
|
144
|
-
say_error(error, :bold, :red)
|
145
|
-
}
|
146
|
-
)
|
147
|
-
|
148
|
-
compiler.run do |constant, contents|
|
149
|
-
constant_name = T.must(Reflection.name_of(constant))
|
150
|
-
|
151
|
-
if verbose && !quiet
|
152
|
-
say("Processing: ", [:yellow])
|
153
|
-
say(constant_name)
|
154
|
-
end
|
155
|
-
|
156
|
-
filename = compile_dsl_rbi(
|
157
|
-
constant_name,
|
158
|
-
contents,
|
159
|
-
outpath: outpath,
|
160
|
-
quiet: should_verify || quiet && !verbose
|
161
|
-
)
|
162
|
-
|
163
|
-
if filename
|
164
|
-
rbi_files_to_purge.delete(filename)
|
165
|
-
end
|
166
|
-
end
|
167
|
-
say("")
|
168
|
-
|
169
|
-
if should_verify
|
170
|
-
perform_dsl_verification(outpath)
|
171
|
-
else
|
172
|
-
purge_stale_dsl_rbi_files(rbi_files_to_purge)
|
173
|
-
|
174
|
-
say("Done", :green)
|
175
|
-
|
176
|
-
say("All operations performed in working directory.", [:green, :bold])
|
177
|
-
say("Please review changes and commit them.", [:green, :bold])
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
sig { params(should_verify: T::Boolean).void }
|
182
|
-
def sync_rbis_with_gemfile(should_verify: false)
|
183
|
-
if should_verify
|
184
|
-
say("Checking for out-of-date RBIs...")
|
185
|
-
say("")
|
186
|
-
perform_sync_verification
|
187
|
-
return
|
188
|
-
end
|
189
|
-
|
190
|
-
anything_done = [
|
191
|
-
perform_removals,
|
192
|
-
perform_additions,
|
193
|
-
].any?
|
194
|
-
|
195
|
-
if anything_done
|
196
|
-
say("All operations performed in working directory.", [:green, :bold])
|
197
|
-
say("Please review changes and commit them.", [:green, :bold])
|
198
|
-
else
|
199
|
-
say("No operations performed, all RBIs are up-to-date.", [:green, :bold])
|
200
|
-
end
|
201
|
-
|
202
|
-
puts
|
203
|
-
end
|
204
|
-
|
205
|
-
private
|
206
|
-
|
207
|
-
EMPTY_RBI_COMMENT = <<~CONTENT
|
208
|
-
# THIS IS AN EMPTY RBI FILE.
|
209
|
-
# see https://github.com/Shopify/tapioca/wiki/Manual-Gem-Requires
|
210
|
-
CONTENT
|
211
|
-
|
212
|
-
sig { returns(Gemfile) }
|
213
|
-
def bundle
|
214
|
-
@bundle ||= Gemfile.new
|
215
|
-
end
|
216
|
-
|
217
|
-
sig { returns(Loader) }
|
218
|
-
def loader
|
219
|
-
@loader ||= Loader.new
|
220
|
-
end
|
221
|
-
|
222
|
-
sig { returns(Compilers::SymbolTableCompiler) }
|
223
|
-
def compiler
|
224
|
-
@compiler ||= Compilers::SymbolTableCompiler.new
|
225
|
-
end
|
226
|
-
|
227
|
-
sig { void }
|
228
|
-
def require_gem_file
|
229
|
-
say("Requiring all gems to prepare for compiling... ")
|
230
|
-
begin
|
231
|
-
loader.load_bundle(bundle, config.prerequire, config.postrequire)
|
232
|
-
rescue LoadError => e
|
233
|
-
explain_failed_require(config.postrequire, e)
|
234
|
-
exit(1)
|
235
|
-
end
|
236
|
-
say(" Done", :green)
|
237
|
-
unless bundle.missing_specs.empty?
|
238
|
-
say(" completed with missing specs: ")
|
239
|
-
say(bundle.missing_specs.join(", "), :yellow)
|
240
|
-
end
|
241
|
-
puts
|
242
|
-
end
|
243
|
-
|
244
|
-
sig { params(file: String, error: LoadError).void }
|
245
|
-
def explain_failed_require(file, error)
|
246
|
-
say_error("\n\nLoadError: #{error}", :bold, :red)
|
247
|
-
say_error("\nTapioca could not load all the gems required by your application.", :yellow)
|
248
|
-
say_error("If you populated ", :yellow)
|
249
|
-
say_error("#{file} ", :bold, :blue)
|
250
|
-
say_error("with ", :yellow)
|
251
|
-
say_error("`#{Config::DEFAULT_COMMAND} require`", :bold, :blue)
|
252
|
-
say_error("you should probably review it and remove the faulty line.", :yellow)
|
253
|
-
end
|
254
|
-
|
255
|
-
sig do
|
256
|
-
params(
|
257
|
-
message: String,
|
258
|
-
color: T.any(Symbol, T::Array[Symbol]),
|
259
|
-
).void
|
260
|
-
end
|
261
|
-
def say_error(message = "", *color)
|
262
|
-
force_new_line = (message.to_s !~ /( |\t)\Z/)
|
263
|
-
buffer = prepare_message(*T.unsafe([message, *T.unsafe(color)]))
|
264
|
-
buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
|
265
|
-
|
266
|
-
stderr.print(buffer)
|
267
|
-
stderr.flush
|
268
|
-
end
|
269
|
-
|
270
|
-
sig { params(eager_load: T::Boolean).void }
|
271
|
-
def load_application(eager_load:)
|
272
|
-
say("Loading Rails application... ")
|
273
|
-
|
274
|
-
loader.load_rails_application(
|
275
|
-
environment_load: true,
|
276
|
-
eager_load: eager_load
|
277
|
-
)
|
278
|
-
|
279
|
-
say("Done", :green)
|
280
|
-
end
|
281
|
-
|
282
|
-
sig { void }
|
283
|
-
def load_dsl_generators
|
284
|
-
say("Loading DSL generator classes... ")
|
285
|
-
|
286
|
-
Dir.glob([
|
287
|
-
"#{__dir__}/compilers/dsl/*.rb",
|
288
|
-
"#{Config::TAPIOCA_PATH}/generators/**/*.rb",
|
289
|
-
]).each do |generator|
|
290
|
-
require File.expand_path(generator)
|
291
|
-
end
|
292
|
-
|
293
|
-
say("Done", :green)
|
294
|
-
end
|
295
|
-
|
296
|
-
sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
|
297
|
-
def constantize(constant_names)
|
298
|
-
constant_map = constant_names.map do |name|
|
299
|
-
[name, Object.const_get(name)]
|
300
|
-
rescue NameError
|
301
|
-
[name, nil]
|
302
|
-
end.to_h
|
303
|
-
|
304
|
-
unprocessable_constants = constant_map.select { |_, v| v.nil? }
|
305
|
-
unless unprocessable_constants.empty?
|
306
|
-
unprocessable_constants.each do |name, _|
|
307
|
-
say("Error: Cannot find constant '#{name}'", :red)
|
308
|
-
remove(dsl_rbi_filename(name))
|
309
|
-
end
|
310
|
-
|
311
|
-
exit(1)
|
312
|
-
end
|
313
|
-
|
314
|
-
constant_map.values
|
315
|
-
end
|
316
|
-
|
317
|
-
sig { params(generator_names: T::Array[String]).returns(T::Array[T.class_of(Compilers::Dsl::Base)]) }
|
318
|
-
def constantize_generators(generator_names)
|
319
|
-
generator_map = generator_names.map do |name|
|
320
|
-
# Try to find built-in tapioca generator first, then globally defined generator. The
|
321
|
-
# explicit `break` ensures the class is returned, not the `potential_name`.
|
322
|
-
generator_klass = ["Tapioca::Compilers::Dsl::#{name}", name].find do |potential_name|
|
323
|
-
break Object.const_get(potential_name)
|
324
|
-
rescue NameError
|
325
|
-
# Skip if we can't find generator by the potential name
|
326
|
-
end
|
327
|
-
|
328
|
-
[name, generator_klass]
|
329
|
-
end.to_h
|
330
|
-
|
331
|
-
unprocessable_generators = generator_map.select { |_, v| v.nil? }
|
332
|
-
unless unprocessable_generators.empty?
|
333
|
-
unprocessable_generators.each do |name, _|
|
334
|
-
say("Error: Cannot find generator '#{name}'", :red)
|
335
|
-
end
|
336
|
-
|
337
|
-
exit(1)
|
338
|
-
end
|
339
|
-
|
340
|
-
generator_map.values
|
341
|
-
end
|
342
|
-
|
343
|
-
sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) }
|
344
|
-
def existing_rbi_filenames(requested_constants, path: config.outpath)
|
345
|
-
filenames = if requested_constants.empty?
|
346
|
-
Pathname.glob(path / "**/*.rbi")
|
347
|
-
else
|
348
|
-
requested_constants.map do |constant_name|
|
349
|
-
dsl_rbi_filename(constant_name)
|
350
|
-
end
|
351
|
-
end
|
352
|
-
|
353
|
-
filenames.to_set
|
354
|
-
end
|
355
|
-
|
356
|
-
sig { returns(T::Hash[String, String]) }
|
357
|
-
def existing_rbis
|
358
|
-
@existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s)
|
359
|
-
.map { |f| T.cast(f.basename(".*").to_s.split("@", 2), [String, String]) }
|
360
|
-
.to_h
|
361
|
-
end
|
362
|
-
|
363
|
-
sig { returns(T::Hash[String, String]) }
|
364
|
-
def expected_rbis
|
365
|
-
@expected_rbis ||= bundle.dependencies
|
366
|
-
.reject { |gem| config.exclude.include?(gem.name) }
|
367
|
-
.map { |gem| [gem.name, gem.version.to_s] }
|
368
|
-
.to_h
|
369
|
-
end
|
370
|
-
|
371
|
-
sig { params(constant_name: String).returns(Pathname) }
|
372
|
-
def dsl_rbi_filename(constant_name)
|
373
|
-
config.outpath / "#{underscore(constant_name)}.rbi"
|
374
|
-
end
|
375
|
-
|
376
|
-
sig { params(gem_name: String, version: String).returns(Pathname) }
|
377
|
-
def gem_rbi_filename(gem_name, version)
|
378
|
-
config.outpath / "#{gem_name}@#{version}.rbi"
|
379
|
-
end
|
380
|
-
|
381
|
-
sig { params(gem_name: String).returns(Pathname) }
|
382
|
-
def existing_rbi(gem_name)
|
383
|
-
gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
|
384
|
-
end
|
385
|
-
|
386
|
-
sig { params(gem_name: String).returns(Pathname) }
|
387
|
-
def expected_rbi(gem_name)
|
388
|
-
gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
|
389
|
-
end
|
390
|
-
|
391
|
-
sig { params(gem_name: String).returns(T::Boolean) }
|
392
|
-
def gem_rbi_exists?(gem_name)
|
393
|
-
existing_rbis.key?(gem_name)
|
394
|
-
end
|
395
|
-
|
396
|
-
sig { returns(T::Array[String]) }
|
397
|
-
def removed_rbis
|
398
|
-
(existing_rbis.keys - expected_rbis.keys).sort
|
399
|
-
end
|
400
|
-
|
401
|
-
sig { returns(T::Array[String]) }
|
402
|
-
def added_rbis
|
403
|
-
expected_rbis.select do |name, value|
|
404
|
-
existing_rbis[name] != value
|
405
|
-
end.keys.sort
|
406
|
-
end
|
407
|
-
|
408
|
-
sig { params(filename: Pathname).void }
|
409
|
-
def add(filename)
|
410
|
-
say("++ Adding: #{filename}")
|
411
|
-
end
|
412
|
-
|
413
|
-
sig { params(filename: Pathname).void }
|
414
|
-
def remove(filename)
|
415
|
-
return unless filename.exist?
|
416
|
-
say("-- Removing: #{filename}")
|
417
|
-
filename.unlink
|
418
|
-
end
|
419
|
-
|
420
|
-
sig { params(old_filename: Pathname, new_filename: Pathname).void }
|
421
|
-
def move(old_filename, new_filename)
|
422
|
-
say("-> Moving: #{old_filename} to #{new_filename}")
|
423
|
-
old_filename.rename(new_filename.to_s)
|
424
|
-
end
|
425
|
-
|
426
|
-
sig { void }
|
427
|
-
def perform_removals
|
428
|
-
say("Removing RBI files of gems that have been removed:", [:blue, :bold])
|
429
|
-
puts
|
430
|
-
|
431
|
-
anything_done = T.let(false, T::Boolean)
|
432
|
-
|
433
|
-
gems = removed_rbis
|
434
|
-
|
435
|
-
indent do
|
436
|
-
if gems.empty?
|
437
|
-
say("Nothing to do.")
|
438
|
-
else
|
439
|
-
gems.each do |removed|
|
440
|
-
filename = existing_rbi(removed)
|
441
|
-
remove(filename)
|
442
|
-
end
|
443
|
-
|
444
|
-
anything_done = true
|
445
|
-
end
|
446
|
-
end
|
447
|
-
|
448
|
-
puts
|
449
|
-
|
450
|
-
anything_done
|
451
|
-
end
|
452
|
-
|
453
|
-
sig { void }
|
454
|
-
def perform_additions
|
455
|
-
say("Generating RBI files of gems that are added or updated:", [:blue, :bold])
|
456
|
-
puts
|
457
|
-
|
458
|
-
anything_done = T.let(false, T::Boolean)
|
459
|
-
|
460
|
-
gems = added_rbis
|
461
|
-
|
462
|
-
indent do
|
463
|
-
if gems.empty?
|
464
|
-
say("Nothing to do.")
|
465
|
-
else
|
466
|
-
require_gem_file
|
467
|
-
|
468
|
-
gems.each do |gem_name|
|
469
|
-
filename = expected_rbi(gem_name)
|
470
|
-
|
471
|
-
if gem_rbi_exists?(gem_name)
|
472
|
-
old_filename = existing_rbi(gem_name)
|
473
|
-
move(old_filename, filename) unless old_filename == filename
|
474
|
-
end
|
475
|
-
|
476
|
-
gem = T.must(bundle.gem(gem_name))
|
477
|
-
compile_gem_rbi(gem)
|
478
|
-
add(filename)
|
479
|
-
|
480
|
-
puts
|
481
|
-
end
|
482
|
-
end
|
483
|
-
|
484
|
-
anything_done = true
|
485
|
-
end
|
486
|
-
|
487
|
-
puts
|
488
|
-
|
489
|
-
anything_done
|
490
|
-
end
|
491
|
-
|
492
|
-
sig do
|
493
|
-
params(gem_names: T::Array[String])
|
494
|
-
.returns(T::Array[Gemfile::GemSpec])
|
495
|
-
end
|
496
|
-
def gems_to_generate(gem_names)
|
497
|
-
return bundle.dependencies if gem_names.empty?
|
498
|
-
|
499
|
-
gem_names.map do |gem_name|
|
500
|
-
gem = bundle.gem(gem_name)
|
501
|
-
if gem.nil?
|
502
|
-
say("Error: Cannot find gem '#{gem_name}'", :red)
|
503
|
-
exit(1)
|
504
|
-
end
|
505
|
-
gem
|
506
|
-
end
|
507
|
-
end
|
508
|
-
|
509
|
-
sig { params(command: String, reason: T.nilable(String), strictness: T.nilable(String)).returns(String) }
|
510
|
-
def rbi_header(command, reason: nil, strictness: nil)
|
511
|
-
statement = <<~HEAD
|
512
|
-
# DO NOT EDIT MANUALLY
|
513
|
-
# This is an autogenerated file for #{reason}.
|
514
|
-
# Please instead update this file by running `#{command}`.
|
515
|
-
HEAD
|
516
|
-
|
517
|
-
sigil = <<~SIGIL if strictness
|
518
|
-
# typed: #{strictness}
|
519
|
-
SIGIL
|
520
|
-
|
521
|
-
if config.file_header
|
522
|
-
[statement, sigil].compact.join("\n").strip.concat("\n\n")
|
523
|
-
elsif sigil
|
524
|
-
sigil.strip.concat("\n\n")
|
525
|
-
else
|
526
|
-
""
|
527
|
-
end
|
528
|
-
end
|
529
|
-
|
530
|
-
sig { params(gem: Gemfile::GemSpec).void }
|
531
|
-
def compile_gem_rbi(gem)
|
532
|
-
compiler = Compilers::SymbolTableCompiler.new
|
533
|
-
gem_name = set_color(gem.name, :yellow, :bold)
|
534
|
-
say("Compiling #{gem_name}, this may take a few seconds... ")
|
535
|
-
|
536
|
-
strictness = config.typed_overrides[gem.name] || "true"
|
537
|
-
rbi_body_content = compiler.compile(gem)
|
538
|
-
content = String.new
|
539
|
-
content << rbi_header(
|
540
|
-
"#{Config::DEFAULT_COMMAND} sync",
|
541
|
-
reason: "types exported from the `#{gem.name}` gem",
|
542
|
-
strictness: strictness
|
543
|
-
)
|
544
|
-
|
545
|
-
FileUtils.mkdir_p(config.outdir)
|
546
|
-
filename = config.outpath / gem.rbi_file_name
|
547
|
-
|
548
|
-
if rbi_body_content.strip.empty?
|
549
|
-
content << EMPTY_RBI_COMMENT
|
550
|
-
say("Done (empty output)", :yellow)
|
551
|
-
else
|
552
|
-
content << rbi_body_content
|
553
|
-
say("Done", :green)
|
554
|
-
end
|
555
|
-
File.write(filename.to_s, content)
|
556
|
-
|
557
|
-
T.unsafe(Pathname).glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
|
558
|
-
remove(file) unless file.basename.to_s == gem.rbi_file_name
|
559
|
-
end
|
560
|
-
end
|
561
|
-
|
562
|
-
sig do
|
563
|
-
params(constant_name: String, contents: String, outpath: Pathname, quiet: T::Boolean)
|
564
|
-
.returns(T.nilable(Pathname))
|
565
|
-
end
|
566
|
-
def compile_dsl_rbi(constant_name, contents, outpath: config.outpath, quiet: false)
|
567
|
-
return if contents.nil?
|
568
|
-
|
569
|
-
rbi_name = underscore(constant_name) + ".rbi"
|
570
|
-
filename = outpath / rbi_name
|
571
|
-
|
572
|
-
out = String.new
|
573
|
-
out << rbi_header(
|
574
|
-
"#{Config::DEFAULT_COMMAND} dsl #{constant_name}",
|
575
|
-
reason: "dynamic methods in `#{constant_name}`"
|
576
|
-
)
|
577
|
-
out << contents
|
578
|
-
|
579
|
-
FileUtils.mkdir_p(File.dirname(filename))
|
580
|
-
File.write(filename, out)
|
581
|
-
|
582
|
-
unless quiet
|
583
|
-
say("Wrote: ", [:green])
|
584
|
-
say(filename)
|
585
|
-
end
|
586
|
-
|
587
|
-
filename
|
588
|
-
end
|
589
|
-
|
590
|
-
sig { params(tmp_dir: Pathname).returns(T::Hash[String, Symbol]) }
|
591
|
-
def verify_dsl_rbi(tmp_dir:)
|
592
|
-
diff = {}
|
593
|
-
|
594
|
-
existing_rbis = rbi_files_in(config.outpath)
|
595
|
-
new_rbis = rbi_files_in(tmp_dir)
|
596
|
-
|
597
|
-
added_files = (new_rbis - existing_rbis)
|
598
|
-
|
599
|
-
added_files.each do |file|
|
600
|
-
diff[file] = :added
|
601
|
-
end
|
602
|
-
|
603
|
-
removed_files = (existing_rbis - new_rbis)
|
604
|
-
|
605
|
-
removed_files.each do |file|
|
606
|
-
diff[file] = :removed
|
607
|
-
end
|
608
|
-
|
609
|
-
common_files = (existing_rbis & new_rbis)
|
610
|
-
|
611
|
-
changed_files = common_files.map do |filename|
|
612
|
-
filename unless FileUtils.identical?(config.outpath / filename, tmp_dir / filename)
|
613
|
-
end.compact
|
614
|
-
|
615
|
-
changed_files.each do |file|
|
616
|
-
diff[file] = :changed
|
617
|
-
end
|
618
|
-
|
619
|
-
diff
|
620
|
-
end
|
621
|
-
|
622
|
-
sig { params(cause: Symbol, files: T::Array[String]).returns(String) }
|
623
|
-
def build_error_for_files(cause, files)
|
624
|
-
filenames = files.map do |file|
|
625
|
-
config.outpath / file
|
626
|
-
end.join("\n - ")
|
627
|
-
|
628
|
-
" File(s) #{cause}:\n - #{filenames}"
|
629
|
-
end
|
630
|
-
|
631
|
-
sig { params(path: Pathname).returns(T::Array[Pathname]) }
|
632
|
-
def rbi_files_in(path)
|
633
|
-
Pathname.glob(path / "**/*.rbi").map do |file|
|
634
|
-
file.relative_path_from(path)
|
635
|
-
end.sort
|
636
|
-
end
|
637
|
-
|
638
|
-
sig { params(dir: Pathname).void }
|
639
|
-
def perform_dsl_verification(dir)
|
640
|
-
diff = verify_dsl_rbi(tmp_dir: dir)
|
641
|
-
|
642
|
-
report_diff_and_exit_if_out_of_date(diff, "dsl")
|
643
|
-
ensure
|
644
|
-
FileUtils.remove_entry(dir)
|
645
|
-
end
|
646
|
-
|
647
|
-
sig { params(files: T::Set[Pathname]).void }
|
648
|
-
def purge_stale_dsl_rbi_files(files)
|
649
|
-
if files.any?
|
650
|
-
say("Removing stale RBI files...")
|
651
|
-
|
652
|
-
files.sort.each do |filename|
|
653
|
-
remove(filename)
|
654
|
-
end
|
655
|
-
say("")
|
656
|
-
end
|
657
|
-
end
|
658
|
-
|
659
|
-
sig { void }
|
660
|
-
def perform_sync_verification
|
661
|
-
diff = {}
|
662
|
-
|
663
|
-
removed_rbis.each do |gem_name|
|
664
|
-
filename = existing_rbi(gem_name)
|
665
|
-
diff[filename] = :removed
|
666
|
-
end
|
667
|
-
|
668
|
-
added_rbis.each do |gem_name|
|
669
|
-
filename = expected_rbi(gem_name)
|
670
|
-
diff[filename] = gem_rbi_exists?(gem_name) ? :changed : :added
|
671
|
-
end
|
672
|
-
|
673
|
-
report_diff_and_exit_if_out_of_date(diff, "sync")
|
674
|
-
end
|
675
|
-
|
676
|
-
sig { params(diff: T::Hash[String, Symbol], command: String).void }
|
677
|
-
def report_diff_and_exit_if_out_of_date(diff, command)
|
678
|
-
if diff.empty?
|
679
|
-
say("Nothing to do, all RBIs are up-to-date.")
|
680
|
-
else
|
681
|
-
say("RBI files are out-of-date. In your development environment, please run:", :green)
|
682
|
-
say(" `#{Config::DEFAULT_COMMAND} #{command}`", [:green, :bold])
|
683
|
-
say("Once it is complete, be sure to commit and push any changes", :green)
|
684
|
-
|
685
|
-
say("")
|
686
|
-
|
687
|
-
say("Reason:", [:red])
|
688
|
-
diff.group_by(&:last).sort.each do |cause, diff_for_cause|
|
689
|
-
say(build_error_for_files(cause, diff_for_cause.map(&:first)))
|
690
|
-
end
|
691
|
-
|
692
|
-
exit(1)
|
693
|
-
end
|
694
|
-
end
|
695
|
-
|
696
|
-
sig { void }
|
697
|
-
def abort_if_pending_migrations!
|
698
|
-
return unless File.exist?("config/application.rb")
|
699
|
-
return unless defined?(::Rake)
|
700
|
-
|
701
|
-
Rails.application.load_tasks
|
702
|
-
Rake::Task["db:abort_if_pending_migrations"].invoke if Rake::Task.task_defined?("db:abort_if_pending_migrations")
|
703
|
-
end
|
704
|
-
|
705
|
-
sig { params(class_name: String).returns(String) }
|
706
|
-
def underscore(class_name)
|
707
|
-
return class_name unless /[A-Z-]|::/.match?(class_name)
|
708
|
-
|
709
|
-
word = class_name.to_s.gsub("::", "/")
|
710
|
-
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
711
|
-
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
712
|
-
word.tr!("-", "_")
|
713
|
-
word.downcase!
|
714
|
-
word
|
715
|
-
end
|
716
|
-
end
|
717
|
-
end
|