tapioca 0.5.0 → 0.5.4
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 +55 -139
- data/lib/tapioca/compilers/dsl/active_model_secure_password.rb +101 -0
- data/lib/tapioca/compilers/dsl/active_record_fixtures.rb +86 -0
- 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/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/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 +16 -6
- data/lib/tapioca/generator.rb +0 -717
@@ -0,0 +1,362 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Tapioca
|
5
|
+
module Generators
|
6
|
+
class Dsl < Base
|
7
|
+
sig do
|
8
|
+
params(
|
9
|
+
requested_constants: T::Array[String],
|
10
|
+
outpath: Pathname,
|
11
|
+
generators: T::Array[String],
|
12
|
+
exclude_generators: T::Array[String],
|
13
|
+
file_header: T::Boolean,
|
14
|
+
compiler_path: String,
|
15
|
+
tapioca_path: String,
|
16
|
+
default_command: String,
|
17
|
+
file_writer: Thor::Actions,
|
18
|
+
should_verify: T::Boolean,
|
19
|
+
quiet: T::Boolean,
|
20
|
+
verbose: T::Boolean
|
21
|
+
).void
|
22
|
+
end
|
23
|
+
def initialize(
|
24
|
+
requested_constants:,
|
25
|
+
outpath:,
|
26
|
+
generators:,
|
27
|
+
exclude_generators:,
|
28
|
+
file_header:,
|
29
|
+
compiler_path:,
|
30
|
+
tapioca_path:,
|
31
|
+
default_command:,
|
32
|
+
file_writer: FileWriter.new,
|
33
|
+
should_verify: false,
|
34
|
+
quiet: false,
|
35
|
+
verbose: false
|
36
|
+
)
|
37
|
+
@requested_constants = requested_constants
|
38
|
+
@outpath = outpath
|
39
|
+
@generators = generators
|
40
|
+
@exclude_generators = exclude_generators
|
41
|
+
@file_header = file_header
|
42
|
+
@compiler_path = compiler_path
|
43
|
+
@tapioca_path = tapioca_path
|
44
|
+
@should_verify = should_verify
|
45
|
+
@quiet = quiet
|
46
|
+
@verbose = verbose
|
47
|
+
|
48
|
+
super(default_command: default_command, file_writer: file_writer)
|
49
|
+
|
50
|
+
@loader = T.let(nil, T.nilable(Loader))
|
51
|
+
end
|
52
|
+
|
53
|
+
sig { override.void }
|
54
|
+
def generate
|
55
|
+
load_application(eager_load: @requested_constants.empty?)
|
56
|
+
abort_if_pending_migrations!
|
57
|
+
load_dsl_generators
|
58
|
+
|
59
|
+
if @should_verify
|
60
|
+
say("Checking for out-of-date RBIs...")
|
61
|
+
else
|
62
|
+
say("Compiling DSL RBI files...")
|
63
|
+
end
|
64
|
+
say("")
|
65
|
+
|
66
|
+
outpath = @should_verify ? Pathname.new(Dir.mktmpdir) : @outpath
|
67
|
+
rbi_files_to_purge = existing_rbi_filenames(@requested_constants)
|
68
|
+
|
69
|
+
compiler = Compilers::DslCompiler.new(
|
70
|
+
requested_constants: constantize(@requested_constants),
|
71
|
+
requested_generators: constantize_generators(@generators),
|
72
|
+
excluded_generators: constantize_generators(@exclude_generators),
|
73
|
+
error_handler: ->(error) {
|
74
|
+
say_error(error, :bold, :red)
|
75
|
+
}
|
76
|
+
)
|
77
|
+
|
78
|
+
compiler.run do |constant, contents|
|
79
|
+
constant_name = T.must(Reflection.name_of(constant))
|
80
|
+
|
81
|
+
if @verbose && !@quiet
|
82
|
+
say_status(:processing, constant_name, :yellow)
|
83
|
+
end
|
84
|
+
|
85
|
+
filename = compile_dsl_rbi(
|
86
|
+
constant_name,
|
87
|
+
contents,
|
88
|
+
outpath: outpath,
|
89
|
+
quiet: @should_verify || @quiet && !@verbose
|
90
|
+
)
|
91
|
+
|
92
|
+
if filename
|
93
|
+
rbi_files_to_purge.delete(filename)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
say("")
|
97
|
+
|
98
|
+
if @should_verify
|
99
|
+
perform_dsl_verification(outpath)
|
100
|
+
else
|
101
|
+
purge_stale_dsl_rbi_files(rbi_files_to_purge)
|
102
|
+
|
103
|
+
say("Done", :green)
|
104
|
+
|
105
|
+
say("All operations performed in working directory.", [:green, :bold])
|
106
|
+
say("Please review changes and commit them.", [:green, :bold])
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
sig { params(eager_load: T::Boolean).void }
|
113
|
+
def load_application(eager_load:)
|
114
|
+
say("Loading Rails application... ")
|
115
|
+
|
116
|
+
loader.load_rails_application(
|
117
|
+
environment_load: true,
|
118
|
+
eager_load: eager_load
|
119
|
+
)
|
120
|
+
|
121
|
+
say("Done", :green)
|
122
|
+
end
|
123
|
+
|
124
|
+
sig { void }
|
125
|
+
def abort_if_pending_migrations!
|
126
|
+
return unless File.exist?("config/application.rb")
|
127
|
+
return unless defined?(::Rake)
|
128
|
+
|
129
|
+
Rails.application.load_tasks
|
130
|
+
if Rake::Task.task_defined?("db:abort_if_pending_migrations")
|
131
|
+
Rake::Task["db:abort_if_pending_migrations"].invoke
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
sig { void }
|
136
|
+
def load_dsl_generators
|
137
|
+
say("Loading DSL generator classes... ")
|
138
|
+
|
139
|
+
Dir.glob([
|
140
|
+
"#{@compiler_path}/*.rb",
|
141
|
+
"#{@tapioca_path}/generators/**/*.rb",
|
142
|
+
]).each do |generator|
|
143
|
+
require File.expand_path(generator)
|
144
|
+
end
|
145
|
+
|
146
|
+
say("Done", :green)
|
147
|
+
end
|
148
|
+
|
149
|
+
sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) }
|
150
|
+
def existing_rbi_filenames(requested_constants, path: @outpath)
|
151
|
+
filenames = if requested_constants.empty?
|
152
|
+
Pathname.glob(path / "**/*.rbi")
|
153
|
+
else
|
154
|
+
requested_constants.map do |constant_name|
|
155
|
+
dsl_rbi_filename(constant_name)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
filenames.to_set
|
160
|
+
end
|
161
|
+
|
162
|
+
sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
|
163
|
+
def constantize(constant_names)
|
164
|
+
constant_map = constant_names.map do |name|
|
165
|
+
[name, Object.const_get(name)]
|
166
|
+
rescue NameError
|
167
|
+
[name, nil]
|
168
|
+
end.to_h
|
169
|
+
|
170
|
+
unprocessable_constants = constant_map.select { |_, v| v.nil? }
|
171
|
+
unless unprocessable_constants.empty?
|
172
|
+
unprocessable_constants.each do |name, _|
|
173
|
+
say("Error: Cannot find constant '#{name}'", :red)
|
174
|
+
remove(dsl_rbi_filename(name))
|
175
|
+
end
|
176
|
+
|
177
|
+
exit(1)
|
178
|
+
end
|
179
|
+
|
180
|
+
constant_map.values
|
181
|
+
end
|
182
|
+
|
183
|
+
sig { params(generator_names: T::Array[String]).returns(T::Array[T.class_of(Compilers::Dsl::Base)]) }
|
184
|
+
def constantize_generators(generator_names)
|
185
|
+
generator_map = generator_names.map do |name|
|
186
|
+
# Try to find built-in tapioca generator first, then globally defined generator. The
|
187
|
+
# explicit `break` ensures the class is returned, not the `potential_name`.
|
188
|
+
generator_klass = ["Tapioca::Compilers::Dsl::#{name}", name].find do |potential_name|
|
189
|
+
break Object.const_get(potential_name)
|
190
|
+
rescue NameError
|
191
|
+
# Skip if we can't find generator by the potential name
|
192
|
+
end
|
193
|
+
|
194
|
+
[name, generator_klass]
|
195
|
+
end.to_h
|
196
|
+
|
197
|
+
unprocessable_generators = generator_map.select { |_, v| v.nil? }
|
198
|
+
unless unprocessable_generators.empty?
|
199
|
+
unprocessable_generators.each do |name, _|
|
200
|
+
say("Error: Cannot find generator '#{name}'", :red)
|
201
|
+
end
|
202
|
+
|
203
|
+
exit(1)
|
204
|
+
end
|
205
|
+
|
206
|
+
generator_map.values
|
207
|
+
end
|
208
|
+
|
209
|
+
sig do
|
210
|
+
params(
|
211
|
+
constant_name: String,
|
212
|
+
rbi: RBI::File,
|
213
|
+
outpath: Pathname,
|
214
|
+
quiet: T::Boolean
|
215
|
+
).returns(T.nilable(Pathname))
|
216
|
+
end
|
217
|
+
def compile_dsl_rbi(constant_name, rbi, outpath: @outpath, quiet: false)
|
218
|
+
return if rbi.empty?
|
219
|
+
|
220
|
+
filename = outpath / rbi_filename_for(constant_name)
|
221
|
+
|
222
|
+
rbi.set_file_header(
|
223
|
+
generate_command_for(constant_name),
|
224
|
+
reason: "dynamic methods in `#{constant_name}`",
|
225
|
+
display_heading: @file_header
|
226
|
+
)
|
227
|
+
|
228
|
+
create_file(filename, rbi.transformed_string, verbose: !quiet)
|
229
|
+
|
230
|
+
filename
|
231
|
+
end
|
232
|
+
|
233
|
+
sig { params(dir: Pathname).void }
|
234
|
+
def perform_dsl_verification(dir)
|
235
|
+
diff = verify_dsl_rbi(tmp_dir: dir)
|
236
|
+
|
237
|
+
report_diff_and_exit_if_out_of_date(diff, "dsl")
|
238
|
+
ensure
|
239
|
+
FileUtils.remove_entry(dir)
|
240
|
+
end
|
241
|
+
|
242
|
+
sig { params(files: T::Set[Pathname]).void }
|
243
|
+
def purge_stale_dsl_rbi_files(files)
|
244
|
+
if files.any?
|
245
|
+
say("Removing stale RBI files...")
|
246
|
+
|
247
|
+
files.sort.each do |filename|
|
248
|
+
remove(filename)
|
249
|
+
end
|
250
|
+
say("")
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
sig { params(constant_name: String).returns(Pathname) }
|
255
|
+
def dsl_rbi_filename(constant_name)
|
256
|
+
@outpath / "#{underscore(constant_name)}.rbi"
|
257
|
+
end
|
258
|
+
|
259
|
+
sig { params(filename: Pathname).void }
|
260
|
+
def remove(filename)
|
261
|
+
return unless filename.exist?
|
262
|
+
say("-- Removing: #{filename}")
|
263
|
+
filename.unlink
|
264
|
+
end
|
265
|
+
|
266
|
+
sig { params(tmp_dir: Pathname).returns(T::Hash[String, Symbol]) }
|
267
|
+
def verify_dsl_rbi(tmp_dir:)
|
268
|
+
diff = {}
|
269
|
+
|
270
|
+
existing_rbis = rbi_files_in(@outpath)
|
271
|
+
new_rbis = rbi_files_in(tmp_dir)
|
272
|
+
|
273
|
+
added_files = (new_rbis - existing_rbis)
|
274
|
+
|
275
|
+
added_files.each do |file|
|
276
|
+
diff[file] = :added
|
277
|
+
end
|
278
|
+
|
279
|
+
removed_files = (existing_rbis - new_rbis)
|
280
|
+
|
281
|
+
removed_files.each do |file|
|
282
|
+
diff[file] = :removed
|
283
|
+
end
|
284
|
+
|
285
|
+
common_files = (existing_rbis & new_rbis)
|
286
|
+
|
287
|
+
changed_files = common_files.map do |filename|
|
288
|
+
filename unless FileUtils.identical?(@outpath / filename, tmp_dir / filename)
|
289
|
+
end.compact
|
290
|
+
|
291
|
+
changed_files.each do |file|
|
292
|
+
diff[file] = :changed
|
293
|
+
end
|
294
|
+
|
295
|
+
diff
|
296
|
+
end
|
297
|
+
|
298
|
+
sig { params(cause: Symbol, files: T::Array[String]).returns(String) }
|
299
|
+
def build_error_for_files(cause, files)
|
300
|
+
filenames = files.map do |file|
|
301
|
+
@outpath / file
|
302
|
+
end.join("\n - ")
|
303
|
+
|
304
|
+
" File(s) #{cause}:\n - #{filenames}"
|
305
|
+
end
|
306
|
+
|
307
|
+
sig { params(diff: T::Hash[String, Symbol], command: String).void }
|
308
|
+
def report_diff_and_exit_if_out_of_date(diff, command)
|
309
|
+
if diff.empty?
|
310
|
+
say("Nothing to do, all RBIs are up-to-date.")
|
311
|
+
else
|
312
|
+
say("RBI files are out-of-date. In your development environment, please run:", :green)
|
313
|
+
say(" `#{@default_command} #{command}`", [:green, :bold])
|
314
|
+
say("Once it is complete, be sure to commit and push any changes", :green)
|
315
|
+
|
316
|
+
say("")
|
317
|
+
|
318
|
+
say("Reason:", [:red])
|
319
|
+
diff.group_by(&:last).sort.each do |cause, diff_for_cause|
|
320
|
+
say(build_error_for_files(cause, diff_for_cause.map(&:first)))
|
321
|
+
end
|
322
|
+
|
323
|
+
exit(1)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
sig { params(path: Pathname).returns(T::Array[Pathname]) }
|
328
|
+
def rbi_files_in(path)
|
329
|
+
Pathname.glob(path / "**/*.rbi").map do |file|
|
330
|
+
file.relative_path_from(path)
|
331
|
+
end.sort
|
332
|
+
end
|
333
|
+
|
334
|
+
sig { returns(Loader) }
|
335
|
+
def loader
|
336
|
+
@loader ||= Loader.new
|
337
|
+
end
|
338
|
+
|
339
|
+
sig { params(class_name: String).returns(String) }
|
340
|
+
def underscore(class_name)
|
341
|
+
return class_name unless /[A-Z-]|::/.match?(class_name)
|
342
|
+
|
343
|
+
word = class_name.to_s.gsub("::", "/")
|
344
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
345
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
346
|
+
word.tr!("-", "_")
|
347
|
+
word.downcase!
|
348
|
+
word
|
349
|
+
end
|
350
|
+
|
351
|
+
sig { params(constant: String).returns(String) }
|
352
|
+
def rbi_filename_for(constant)
|
353
|
+
underscore(constant) + ".rbi"
|
354
|
+
end
|
355
|
+
|
356
|
+
sig { params(constant: String).returns(String) }
|
357
|
+
def generate_command_for(constant)
|
358
|
+
"#{@default_command} dsl #{constant}"
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
@@ -0,0 +1,345 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Tapioca
|
5
|
+
module Generators
|
6
|
+
class Gem < Base
|
7
|
+
sig do
|
8
|
+
params(
|
9
|
+
gem_names: T::Array[String],
|
10
|
+
gem_excludes: T::Array[String],
|
11
|
+
prerequire: T.nilable(String),
|
12
|
+
postrequire: String,
|
13
|
+
typed_overrides: T::Hash[String, String],
|
14
|
+
default_command: String,
|
15
|
+
outpath: Pathname,
|
16
|
+
file_header: T::Boolean,
|
17
|
+
doc: T::Boolean,
|
18
|
+
file_writer: Thor::Actions
|
19
|
+
).void
|
20
|
+
end
|
21
|
+
def initialize(
|
22
|
+
gem_names:,
|
23
|
+
gem_excludes:,
|
24
|
+
prerequire:,
|
25
|
+
postrequire:,
|
26
|
+
typed_overrides:,
|
27
|
+
default_command:,
|
28
|
+
outpath:,
|
29
|
+
file_header:,
|
30
|
+
doc:,
|
31
|
+
file_writer: FileWriter.new
|
32
|
+
)
|
33
|
+
@gem_names = gem_names
|
34
|
+
@gem_excludes = gem_excludes
|
35
|
+
@prerequire = prerequire
|
36
|
+
@postrequire = postrequire
|
37
|
+
@typed_overrides = typed_overrides
|
38
|
+
@outpath = outpath
|
39
|
+
@file_header = file_header
|
40
|
+
|
41
|
+
super(default_command: default_command, file_writer: file_writer)
|
42
|
+
|
43
|
+
@loader = T.let(nil, T.nilable(Loader))
|
44
|
+
@bundle = T.let(nil, T.nilable(Gemfile))
|
45
|
+
@existing_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
|
46
|
+
@expected_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
|
47
|
+
@doc = T.let(doc, T::Boolean)
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { override.void }
|
51
|
+
def generate
|
52
|
+
require_gem_file
|
53
|
+
|
54
|
+
gems_to_generate(@gem_names)
|
55
|
+
.reject { |gem| @gem_excludes.include?(gem.name) }
|
56
|
+
.each do |gem|
|
57
|
+
say("Processing '#{gem.name}' gem:", :green)
|
58
|
+
shell.indent do
|
59
|
+
compile_gem_rbi(gem)
|
60
|
+
puts
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
say("All operations performed in working directory.", [:green, :bold])
|
65
|
+
say("Please review changes and commit them.", [:green, :bold])
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { params(should_verify: T::Boolean).void }
|
69
|
+
def sync(should_verify: false)
|
70
|
+
if should_verify
|
71
|
+
say("Checking for out-of-date RBIs...")
|
72
|
+
say("")
|
73
|
+
perform_sync_verification
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
77
|
+
anything_done = [
|
78
|
+
perform_removals,
|
79
|
+
perform_additions,
|
80
|
+
].any?
|
81
|
+
|
82
|
+
if anything_done
|
83
|
+
say("All operations performed in working directory.", [:green, :bold])
|
84
|
+
say("Please review changes and commit them.", [:green, :bold])
|
85
|
+
else
|
86
|
+
say("No operations performed, all RBIs are up-to-date.", [:green, :bold])
|
87
|
+
end
|
88
|
+
|
89
|
+
puts
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
sig { returns(Loader) }
|
95
|
+
def loader
|
96
|
+
@loader ||= Loader.new
|
97
|
+
end
|
98
|
+
|
99
|
+
sig { returns(Gemfile) }
|
100
|
+
def bundle
|
101
|
+
@bundle ||= Gemfile.new
|
102
|
+
end
|
103
|
+
|
104
|
+
sig { void }
|
105
|
+
def require_gem_file
|
106
|
+
say("Requiring all gems to prepare for compiling... ")
|
107
|
+
begin
|
108
|
+
loader.load_bundle(bundle, @prerequire, @postrequire)
|
109
|
+
rescue LoadError => e
|
110
|
+
explain_failed_require(@postrequire, e)
|
111
|
+
exit(1)
|
112
|
+
end
|
113
|
+
say(" Done", :green)
|
114
|
+
unless bundle.missing_specs.empty?
|
115
|
+
say(" completed with missing specs: ")
|
116
|
+
say(bundle.missing_specs.join(", "), :yellow)
|
117
|
+
end
|
118
|
+
puts
|
119
|
+
end
|
120
|
+
|
121
|
+
sig { params(gem_names: T::Array[String]).returns(T::Array[Gemfile::GemSpec]) }
|
122
|
+
def gems_to_generate(gem_names)
|
123
|
+
return bundle.dependencies if gem_names.empty?
|
124
|
+
|
125
|
+
gem_names.map do |gem_name|
|
126
|
+
gem = bundle.gem(gem_name)
|
127
|
+
if gem.nil?
|
128
|
+
say("Error: Cannot find gem '#{gem_name}'", :red)
|
129
|
+
exit(1)
|
130
|
+
end
|
131
|
+
gem
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
sig { params(gem: Gemfile::GemSpec).void }
|
136
|
+
def compile_gem_rbi(gem)
|
137
|
+
gem_name = set_color(gem.name, :yellow, :bold)
|
138
|
+
say("Compiling #{gem_name}, this may take a few seconds... ")
|
139
|
+
|
140
|
+
rbi = RBI::File.new(strictness: @typed_overrides[gem.name] || "true")
|
141
|
+
rbi.set_file_header(
|
142
|
+
"#{@default_command} gem #{gem.name}",
|
143
|
+
reason: "types exported from the `#{gem.name}` gem",
|
144
|
+
display_heading: @file_header
|
145
|
+
)
|
146
|
+
|
147
|
+
Compilers::SymbolTableCompiler.new.compile(gem, rbi, 0, @doc)
|
148
|
+
|
149
|
+
if rbi.empty?
|
150
|
+
rbi.set_empty_body_content
|
151
|
+
say("Done (empty output)", :yellow)
|
152
|
+
else
|
153
|
+
say("Done", :green)
|
154
|
+
end
|
155
|
+
|
156
|
+
create_file(@outpath / gem.rbi_file_name, rbi.transformed_string)
|
157
|
+
|
158
|
+
T.unsafe(Pathname).glob((@outpath / "#{gem.name}@*.rbi").to_s) do |file|
|
159
|
+
remove(file) unless file.basename.to_s == gem.rbi_file_name
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
sig { void }
|
164
|
+
def perform_sync_verification
|
165
|
+
diff = {}
|
166
|
+
|
167
|
+
removed_rbis.each do |gem_name|
|
168
|
+
filename = existing_rbi(gem_name)
|
169
|
+
diff[filename] = :removed
|
170
|
+
end
|
171
|
+
|
172
|
+
added_rbis.each do |gem_name|
|
173
|
+
filename = expected_rbi(gem_name)
|
174
|
+
diff[filename] = gem_rbi_exists?(gem_name) ? :changed : :added
|
175
|
+
end
|
176
|
+
|
177
|
+
report_diff_and_exit_if_out_of_date(diff, "gem")
|
178
|
+
end
|
179
|
+
|
180
|
+
sig { void }
|
181
|
+
def perform_removals
|
182
|
+
say("Removing RBI files of gems that have been removed:", [:blue, :bold])
|
183
|
+
puts
|
184
|
+
|
185
|
+
anything_done = T.let(false, T::Boolean)
|
186
|
+
|
187
|
+
gems = removed_rbis
|
188
|
+
|
189
|
+
shell.indent do
|
190
|
+
if gems.empty?
|
191
|
+
say("Nothing to do.")
|
192
|
+
else
|
193
|
+
gems.each do |removed|
|
194
|
+
filename = existing_rbi(removed)
|
195
|
+
remove(filename)
|
196
|
+
end
|
197
|
+
|
198
|
+
anything_done = true
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
puts
|
203
|
+
|
204
|
+
anything_done
|
205
|
+
end
|
206
|
+
|
207
|
+
sig { void }
|
208
|
+
def perform_additions
|
209
|
+
say("Generating RBI files of gems that are added or updated:", [:blue, :bold])
|
210
|
+
puts
|
211
|
+
|
212
|
+
anything_done = T.let(false, T::Boolean)
|
213
|
+
|
214
|
+
gems = added_rbis
|
215
|
+
|
216
|
+
shell.indent do
|
217
|
+
if gems.empty?
|
218
|
+
say("Nothing to do.")
|
219
|
+
else
|
220
|
+
require_gem_file
|
221
|
+
|
222
|
+
gems.each do |gem_name|
|
223
|
+
filename = expected_rbi(gem_name)
|
224
|
+
|
225
|
+
if gem_rbi_exists?(gem_name)
|
226
|
+
old_filename = existing_rbi(gem_name)
|
227
|
+
move(old_filename, filename) unless old_filename == filename
|
228
|
+
end
|
229
|
+
|
230
|
+
gem = T.must(bundle.gem(gem_name))
|
231
|
+
compile_gem_rbi(gem)
|
232
|
+
puts
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
anything_done = true
|
237
|
+
end
|
238
|
+
|
239
|
+
puts
|
240
|
+
|
241
|
+
anything_done
|
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("`#{@default_command} require`", :bold, :blue)
|
252
|
+
say_error("you should probably review it and remove the faulty line.", :yellow)
|
253
|
+
end
|
254
|
+
|
255
|
+
sig { params(filename: Pathname).void }
|
256
|
+
def remove(filename)
|
257
|
+
return unless filename.exist?
|
258
|
+
say("-- Removing: #{filename}")
|
259
|
+
filename.unlink
|
260
|
+
end
|
261
|
+
|
262
|
+
sig { returns(T::Array[String]) }
|
263
|
+
def removed_rbis
|
264
|
+
(existing_rbis.keys - expected_rbis.keys).sort
|
265
|
+
end
|
266
|
+
|
267
|
+
sig { params(gem_name: String).returns(Pathname) }
|
268
|
+
def existing_rbi(gem_name)
|
269
|
+
gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
|
270
|
+
end
|
271
|
+
|
272
|
+
sig { returns(T::Array[String]) }
|
273
|
+
def added_rbis
|
274
|
+
expected_rbis.select do |name, value|
|
275
|
+
existing_rbis[name] != value
|
276
|
+
end.keys.sort
|
277
|
+
end
|
278
|
+
|
279
|
+
sig { params(gem_name: String).returns(Pathname) }
|
280
|
+
def expected_rbi(gem_name)
|
281
|
+
gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
|
282
|
+
end
|
283
|
+
|
284
|
+
sig { params(gem_name: String).returns(T::Boolean) }
|
285
|
+
def gem_rbi_exists?(gem_name)
|
286
|
+
existing_rbis.key?(gem_name)
|
287
|
+
end
|
288
|
+
|
289
|
+
sig { params(diff: T::Hash[String, Symbol], command: String).void }
|
290
|
+
def report_diff_and_exit_if_out_of_date(diff, command)
|
291
|
+
if diff.empty?
|
292
|
+
say("Nothing to do, all RBIs are up-to-date.")
|
293
|
+
else
|
294
|
+
say("RBI files are out-of-date. In your development environment, please run:", :green)
|
295
|
+
say(" `#{Config::DEFAULT_COMMAND} #{command}`", [:green, :bold])
|
296
|
+
say("Once it is complete, be sure to commit and push any changes", :green)
|
297
|
+
|
298
|
+
say("")
|
299
|
+
|
300
|
+
say("Reason:", [:red])
|
301
|
+
diff.group_by(&:last).sort.each do |cause, diff_for_cause|
|
302
|
+
say(build_error_for_files(cause, diff_for_cause.map(&:first)))
|
303
|
+
end
|
304
|
+
|
305
|
+
exit(1)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
sig { params(old_filename: Pathname, new_filename: Pathname).void }
|
310
|
+
def move(old_filename, new_filename)
|
311
|
+
say("-> Moving: #{old_filename} to #{new_filename}")
|
312
|
+
old_filename.rename(new_filename.to_s)
|
313
|
+
end
|
314
|
+
|
315
|
+
sig { returns(T::Hash[String, String]) }
|
316
|
+
def existing_rbis
|
317
|
+
@existing_rbis ||= Pathname.glob((@outpath / "*@*.rbi").to_s)
|
318
|
+
.map { |f| T.cast(f.basename(".*").to_s.split("@", 2), [String, String]) }
|
319
|
+
.to_h
|
320
|
+
end
|
321
|
+
|
322
|
+
sig { returns(T::Hash[String, String]) }
|
323
|
+
def expected_rbis
|
324
|
+
@expected_rbis ||= bundle.dependencies
|
325
|
+
.reject { |gem| @gem_excludes.include?(gem.name) }
|
326
|
+
.map { |gem| [gem.name, gem.version.to_s] }
|
327
|
+
.to_h
|
328
|
+
end
|
329
|
+
|
330
|
+
sig { params(gem_name: String, version: String).returns(Pathname) }
|
331
|
+
def gem_rbi_filename(gem_name, version)
|
332
|
+
@outpath / "#{gem_name}@#{version}.rbi"
|
333
|
+
end
|
334
|
+
|
335
|
+
sig { params(cause: Symbol, files: T::Array[String]).returns(String) }
|
336
|
+
def build_error_for_files(cause, files)
|
337
|
+
filenames = files.map do |file|
|
338
|
+
@outpath / file
|
339
|
+
end.join("\n - ")
|
340
|
+
|
341
|
+
" File(s) #{cause}:\n - #{filenames}"
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|