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/config.rb
CHANGED
@@ -17,8 +17,14 @@ module Tapioca
|
|
17
17
|
TracePoint.trace(:class) do |tp|
|
18
18
|
unless tp.self.singleton_class?
|
19
19
|
key = name_of(tp.self)
|
20
|
+
file = tp.path
|
21
|
+
if file == "(eval)"
|
22
|
+
file = T.must(caller_locations)
|
23
|
+
.drop_while { |loc| loc.path == "(eval)" }
|
24
|
+
.first&.path
|
25
|
+
end
|
20
26
|
@class_files[key] ||= Set.new
|
21
|
-
@class_files[key] <<
|
27
|
+
@class_files[key] << file
|
22
28
|
end
|
23
29
|
end
|
24
30
|
|
data/lib/tapioca/gemfile.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "bundler"
|
5
|
+
require "logger"
|
6
|
+
require "yard-sorbet"
|
5
7
|
|
6
8
|
module Tapioca
|
7
9
|
class Gemfile
|
@@ -9,10 +11,7 @@ module Tapioca
|
|
9
11
|
|
10
12
|
Spec = T.type_alias do
|
11
13
|
T.any(
|
12
|
-
|
13
|
-
::Bundler::StubSpecification,
|
14
|
-
::Bundler::RemoteSpecification
|
15
|
-
),
|
14
|
+
::Bundler::StubSpecification,
|
16
15
|
::Gem::Specification
|
17
16
|
)
|
18
17
|
end
|
@@ -116,7 +115,9 @@ module Tapioca
|
|
116
115
|
sig { returns(T::Array[Pathname]) }
|
117
116
|
def files
|
118
117
|
if default_gem?
|
119
|
-
|
118
|
+
# `Bundler::RemoteSpecification` delegates missing methods to
|
119
|
+
# `Gem::Specification`, so `files` actually always exists on spec.
|
120
|
+
T.unsafe(@spec).files.map do |file|
|
120
121
|
ruby_lib_dir.join(file)
|
121
122
|
end
|
122
123
|
else
|
@@ -145,6 +146,11 @@ module Tapioca
|
|
145
146
|
end
|
146
147
|
end
|
147
148
|
|
149
|
+
sig { void }
|
150
|
+
def parse_yard_docs
|
151
|
+
files.each { |path| YARD.parse(path.to_s, [], Logger::Severity::FATAL) }
|
152
|
+
end
|
153
|
+
|
148
154
|
private
|
149
155
|
|
150
156
|
sig { returns(T::Boolean) }
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# TODO: Remove me when logging logic has been abstracted.
|
5
|
+
require "thor"
|
6
|
+
|
7
|
+
module Tapioca
|
8
|
+
module Generators
|
9
|
+
class Base
|
10
|
+
extend T::Sig
|
11
|
+
extend T::Helpers
|
12
|
+
|
13
|
+
class FileWriter < Thor
|
14
|
+
include Thor::Actions
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO: Remove me when logging logic has been abstracted
|
18
|
+
include Thor::Base
|
19
|
+
|
20
|
+
abstract!
|
21
|
+
|
22
|
+
sig { params(default_command: String, file_writer: Thor::Actions).void }
|
23
|
+
def initialize(default_command:, file_writer: FileWriter.new)
|
24
|
+
@file_writer = file_writer
|
25
|
+
@default_command = default_command
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { abstract.void }
|
29
|
+
def generate; end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# TODO: Remove me when logging logic has been abstracted
|
34
|
+
sig { params(message: String, color: T.any(Symbol, T::Array[Symbol])).void }
|
35
|
+
def say_error(message = "", *color)
|
36
|
+
force_new_line = (message.to_s !~ /( |\t)\Z/)
|
37
|
+
# NOTE: This is a hack. We're no longer subclassing from Thor::Shell::Color
|
38
|
+
# so we no longer have access to the prepare_message call.
|
39
|
+
# We should update this to remove this.
|
40
|
+
buffer = shell.send(:prepare_message, *T.unsafe([message, *T.unsafe(color)]))
|
41
|
+
buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
|
42
|
+
|
43
|
+
$stderr.print(buffer)
|
44
|
+
$stderr.flush
|
45
|
+
end
|
46
|
+
|
47
|
+
sig do
|
48
|
+
params(
|
49
|
+
path: T.any(String, Pathname),
|
50
|
+
content: String,
|
51
|
+
force: T::Boolean,
|
52
|
+
skip: T::Boolean,
|
53
|
+
verbose: T::Boolean
|
54
|
+
).void
|
55
|
+
end
|
56
|
+
def create_file(path, content, force: true, skip: false, verbose: true)
|
57
|
+
@file_writer.create_file(path, force: force, skip: skip, verbose: verbose) { content }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -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
|