tapioca 0.4.0 → 0.4.1

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +25 -1
  3. data/README.md +12 -0
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +2 -0
  6. data/lib/tapioca/cli.rb +24 -2
  7. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +129 -0
  8. data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
  9. data/lib/tapioca/compilers/dsl/active_record_associations.rb +285 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +379 -0
  11. data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
  12. data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +213 -0
  13. data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
  14. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +170 -0
  15. data/lib/tapioca/compilers/dsl/active_resource.rb +140 -0
  16. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +126 -0
  17. data/lib/tapioca/compilers/dsl/base.rb +163 -0
  18. data/lib/tapioca/compilers/dsl/frozen_record.rb +96 -0
  19. data/lib/tapioca/compilers/dsl/protobuf.rb +144 -0
  20. data/lib/tapioca/compilers/dsl/smart_properties.rb +173 -0
  21. data/lib/tapioca/compilers/dsl/state_machines.rb +378 -0
  22. data/lib/tapioca/compilers/dsl/url_helpers.rb +83 -0
  23. data/lib/tapioca/compilers/dsl_compiler.rb +121 -0
  24. data/lib/tapioca/compilers/requires_compiler.rb +67 -0
  25. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +141 -24
  26. data/lib/tapioca/config.rb +11 -6
  27. data/lib/tapioca/config_builder.rb +19 -9
  28. data/lib/tapioca/constant_locator.rb +1 -0
  29. data/lib/tapioca/core_ext/class.rb +23 -0
  30. data/lib/tapioca/generator.rb +187 -21
  31. data/lib/tapioca/loader.rb +20 -9
  32. data/lib/tapioca/sorbet_config_parser.rb +77 -0
  33. data/lib/tapioca/version.rb +1 -1
  34. metadata +29 -51
@@ -12,6 +12,7 @@ module Tapioca
12
12
  const(:exclude, T::Array[String])
13
13
  const(:typed_overrides, T::Hash[String, String])
14
14
  const(:todos_path, String)
15
+ const(:generators, T::Array[String])
15
16
 
16
17
  sig { returns(Pathname) }
17
18
  def outpath
@@ -21,17 +22,21 @@ module Tapioca
21
22
 
22
23
  private_class_method :new
23
24
 
24
- CONFIG_FILE_PATH = "sorbet/tapioca/config.yml"
25
- SORBET_CONFIG = "sorbet/config"
25
+ SORBET_PATH = T.let("sorbet", String)
26
+ SORBET_CONFIG = T.let("#{SORBET_PATH}/config", String)
27
+ TAPIOCA_PATH = T.let("#{SORBET_PATH}/tapioca", String)
28
+ TAPIOCA_CONFIG = T.let("#{TAPIOCA_PATH}/config.yml", String)
29
+
30
+ DEFAULT_POSTREQUIRE = T.let("#{TAPIOCA_PATH}/require.rb", String)
31
+ DEFAULT_RBIDIR = T.let("#{SORBET_PATH}/rbi", String)
32
+ DEFAULT_DSLDIR = T.let("#{DEFAULT_RBIDIR}/dsl", String)
33
+ DEFAULT_GEMDIR = T.let("#{DEFAULT_RBIDIR}/gems", String)
34
+ DEFAULT_TODOSPATH = T.let("#{DEFAULT_RBIDIR}/todo.rbi", String)
26
35
 
27
- DEFAULT_POSTREQUIRE = "sorbet/tapioca/require.rb"
28
- DEFAULT_RBIDIR = "sorbet/rbi"
29
- DEFAULT_OUTDIR = T.let("#{DEFAULT_RBIDIR}/gems", String)
30
36
  DEFAULT_OVERRIDES = T.let({
31
37
  # ActiveSupport overrides some core methods with different signatures
32
38
  # so we generate a typed: false RBI for it to suppress errors
33
39
  "activesupport" => "false",
34
40
  }.freeze, T::Hash[String, String])
35
- DEFAULT_TODOSPATH = T.let("#{DEFAULT_RBIDIR}/todo.rbi", String)
36
41
  end
37
42
  end
@@ -8,10 +8,10 @@ module Tapioca
8
8
  class << self
9
9
  extend(T::Sig)
10
10
 
11
- sig { params(options: T::Hash[String, T.untyped]).returns(Config) }
12
- def from_options(options)
11
+ sig { params(command: Symbol, options: T::Hash[String, T.untyped]).returns(Config) }
12
+ def from_options(command, options)
13
13
  Config.from_hash(
14
- merge_options(default_options, config_options, options)
14
+ merge_options(default_options(command), config_options, options)
15
15
  )
16
16
  end
17
17
 
@@ -19,16 +19,25 @@ module Tapioca
19
19
 
20
20
  sig { returns(T::Hash[String, T.untyped]) }
21
21
  def config_options
22
- if File.exist?(Config::CONFIG_FILE_PATH)
23
- YAML.load_file(Config::CONFIG_FILE_PATH, fallback: {})
22
+ if File.exist?(Config::TAPIOCA_CONFIG)
23
+ YAML.load_file(Config::TAPIOCA_CONFIG, fallback: {})
24
24
  else
25
25
  {}
26
26
  end
27
27
  end
28
28
 
29
- sig { returns(T::Hash[String, T.untyped]) }
30
- def default_options
31
- DEFAULT_OPTIONS
29
+ sig { params(command: Symbol).returns(T::Hash[String, T.untyped]) }
30
+ def default_options(command)
31
+ default_outdir = case command
32
+ when :sync, :generate
33
+ Config::DEFAULT_GEMDIR
34
+ when :dsl
35
+ Config::DEFAULT_DSLDIR
36
+ else
37
+ Config::SORBET_PATH
38
+ end
39
+
40
+ DEFAULT_OPTIONS.merge("outdir" => default_outdir)
32
41
  end
33
42
 
34
43
  sig { returns(String) }
@@ -55,11 +64,12 @@ module Tapioca
55
64
 
56
65
  DEFAULT_OPTIONS = T.let({
57
66
  "postrequire" => Config::DEFAULT_POSTREQUIRE,
58
- "outdir" => Config::DEFAULT_OUTDIR,
67
+ "outdir" => nil,
59
68
  "generate_command" => default_command,
60
69
  "exclude" => [],
61
70
  "typed_overrides" => Config::DEFAULT_OVERRIDES,
62
71
  "todos_path" => Config::DEFAULT_TODOSPATH,
72
+ "generators" => [],
63
73
  }.freeze, T::Hash[String, T.untyped])
64
74
  end
65
75
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'set'
@@ -0,0 +1,23 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ class Class
5
+ # Returns an array with all classes that are < than its receiver.
6
+ #
7
+ # class C; end
8
+ # C.descendants # => []
9
+ #
10
+ # class B < C; end
11
+ # C.descendants # => [B]
12
+ #
13
+ # class A < B; end
14
+ # C.descendants # => [B, A]
15
+ #
16
+ # class D < C; end
17
+ # C.descendants # => [B, A, D]
18
+ def descendants
19
+ ObjectSpace.each_object(singleton_class).reject do |k|
20
+ k.singleton_class? || k == self
21
+ end
22
+ end
23
+ end
@@ -35,7 +35,7 @@ module Tapioca
35
35
  .each do |gem|
36
36
  say("Processing '#{gem.name}' gem:", :green)
37
37
  indent do
38
- compile_rbi(gem)
38
+ compile_gem_rbi(gem)
39
39
  puts
40
40
  end
41
41
  end
@@ -44,6 +44,42 @@ module Tapioca
44
44
  say("Please review changes and commit them.", [:green, :bold])
45
45
  end
46
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 << rbi_header(
66
+ config.generate_command,
67
+ reason: "explicit gem requires",
68
+ strictness: "false"
69
+ )
70
+ content << rb_string
71
+
72
+ outdir = File.dirname(requires_path)
73
+ FileUtils.mkdir_p(outdir)
74
+ File.write(requires_path, content)
75
+
76
+ say("Done", :green)
77
+
78
+ say("All requires from this application have been written to #{name}.", [:green, :bold])
79
+ cmd = set_color("tapioca sync", :yellow, :bold)
80
+ say("Please review changes and commit them, then run #{cmd}.", [:green, :bold])
81
+ end
82
+
47
83
  sig { void }
48
84
  def build_todos
49
85
  todos_path = config.todos_path
@@ -62,7 +98,11 @@ module Tapioca
62
98
  end
63
99
 
64
100
  content = String.new
65
- content << rbi_header(config.generate_command, "false")
101
+ content << rbi_header(
102
+ config.generate_command,
103
+ reason: "unresolved constants",
104
+ strictness: "false"
105
+ )
66
106
  content << rbi_string
67
107
  content << "\n"
68
108
 
@@ -76,6 +116,33 @@ module Tapioca
76
116
  say("Please review changes and commit them.", [:green, :bold])
77
117
  end
78
118
 
119
+ sig { params(requested_constants: T::Array[String]).void }
120
+ def build_dsl(requested_constants)
121
+ load_application(eager_load: requested_constants.empty?)
122
+ load_dsl_generators
123
+
124
+ say("Compiling DSL RBI files...")
125
+ say("")
126
+
127
+ compiler = Compilers::DslCompiler.new(
128
+ requested_constants: constantize(requested_constants),
129
+ requested_generators: config.generators,
130
+ error_handler: ->(error) {
131
+ say_error(error, :bold, :red)
132
+ }
133
+ )
134
+
135
+ compiler.run do |constant, contents|
136
+ compile_dsl_rbi(constant, contents)
137
+ end
138
+
139
+ say("")
140
+ say("Done", :green)
141
+
142
+ say("All operations performed in working directory.", [:green, :bold])
143
+ say("Please review changes and commit them.", [:green, :bold])
144
+ end
145
+
79
146
  sig { void }
80
147
  def sync_rbis_with_gemfile
81
148
  anything_done = [
@@ -113,15 +180,83 @@ module Tapioca
113
180
  sig { void }
114
181
  def require_gem_file
115
182
  say("Requiring all gems to prepare for compiling... ")
116
- loader.load_bundle(config.prerequire, config.postrequire)
183
+ begin
184
+ loader.load_bundle(config.prerequire, config.postrequire)
185
+ rescue LoadError => e
186
+ explain_failed_require(config.postrequire, e)
187
+ exit(1)
188
+ end
117
189
  say(" Done", :green)
118
190
  puts
119
191
  end
120
192
 
193
+ sig { params(file: String, error: LoadError).void }
194
+ def explain_failed_require(file, error)
195
+ say_error("\n\nLoadError: #{error}", :bold, :red)
196
+ say_error("\nTapioca could not load all the gems required by your application.", :yellow)
197
+ say_error("If you populated ", :yellow)
198
+ say_error("#{file} ", :bold, :blue)
199
+ say_error("with ", :yellow)
200
+ say_error("tapioca require", :bold, :blue)
201
+ say_error("you should probably review it and remove the faulty line.", :yellow)
202
+ end
203
+
204
+ sig do
205
+ params(
206
+ message: String,
207
+ color: T.any(Symbol, T::Array[Symbol]),
208
+ ).void
209
+ end
210
+ def say_error(message = "", *color)
211
+ force_new_line = (message.to_s !~ /( |\t)\Z/)
212
+ buffer = prepare_message(*T.unsafe([message, *T.unsafe(color)]))
213
+ buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
214
+
215
+ stderr.print(buffer)
216
+ stderr.flush
217
+ end
218
+
219
+ sig { params(eager_load: T::Boolean).void }
220
+ def load_application(eager_load:)
221
+ say("Loading Rails application... ")
222
+
223
+ loader.load_rails(
224
+ environment_load: true,
225
+ eager_load: eager_load
226
+ )
227
+
228
+ say("Done", :green)
229
+ end
230
+
231
+ sig { void }
232
+ def load_dsl_generators
233
+ say("Loading DSL generator classes... ")
234
+
235
+ Dir.glob([
236
+ "#{__dir__}/compilers/dsl/*.rb",
237
+ "#{Config::TAPIOCA_PATH}/generators/**/*.rb",
238
+ ]).each do |generator|
239
+ require File.expand_path(generator)
240
+ end
241
+
242
+ say("Done", :green)
243
+ end
244
+
245
+ sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
246
+ def constantize(constant_names)
247
+ constant_names.map do |name|
248
+ begin
249
+ name.constantize
250
+ rescue NameError
251
+ nil
252
+ end
253
+ end.compact
254
+ end
255
+
121
256
  sig { returns(T::Hash[String, String]) }
122
257
  def existing_rbis
123
258
  @existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s)
124
- .map { |f| f.basename(".*").to_s.split('@') }
259
+ .map { |f| T.cast(f.basename(".*").to_s.split('@', 2), [String, String]) }
125
260
  .to_h
126
261
  end
127
262
 
@@ -134,22 +269,22 @@ module Tapioca
134
269
  end
135
270
 
136
271
  sig { params(gem_name: String, version: String).returns(Pathname) }
137
- def rbi_filename(gem_name, version)
272
+ def gem_rbi_filename(gem_name, version)
138
273
  config.outpath / "#{gem_name}@#{version}.rbi"
139
274
  end
140
275
 
141
276
  sig { params(gem_name: String).returns(Pathname) }
142
277
  def existing_rbi(gem_name)
143
- rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
278
+ gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
144
279
  end
145
280
 
146
281
  sig { params(gem_name: String).returns(Pathname) }
147
282
  def expected_rbi(gem_name)
148
- rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
283
+ gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
149
284
  end
150
285
 
151
286
  sig { params(gem_name: String).returns(T::Boolean) }
152
- def rbi_exists?(gem_name)
287
+ def gem_rbi_exists?(gem_name)
153
288
  existing_rbis.key?(gem_name)
154
289
  end
155
290
 
@@ -227,13 +362,13 @@ module Tapioca
227
362
  gems.each do |gem_name|
228
363
  filename = expected_rbi(gem_name)
229
364
 
230
- if rbi_exists?(gem_name)
365
+ if gem_rbi_exists?(gem_name)
231
366
  old_filename = existing_rbi(gem_name)
232
367
  move(old_filename, filename) unless old_filename == filename
233
368
  end
234
369
 
235
370
  gem = T.must(bundle.gem(gem_name))
236
- compile_rbi(gem)
371
+ compile_gem_rbi(gem)
237
372
  add(filename)
238
373
 
239
374
  puts
@@ -265,27 +400,36 @@ module Tapioca
265
400
  end
266
401
  end
267
402
 
268
- sig { params(command: String, typed_sigil: String).returns(String) }
269
- def rbi_header(command, typed_sigil)
270
- <<~HEAD
271
- # This file is autogenerated. Do not edit it by hand. Regenerate it with:
272
- # #{command}
403
+ sig { params(command: String, reason: T.nilable(String), strictness: T.nilable(String)).returns(String) }
404
+ def rbi_header(command, reason: nil, strictness: nil)
405
+ statement = <<~HEAD
406
+ # DO NOT EDIT MANUALLY
407
+ # This is an autogenerated file for #{reason}.
408
+ # Please instead update this file by running `#{command}`.
409
+ HEAD
273
410
 
274
- # typed: #{typed_sigil}
411
+ sigil = <<~SIGIL if strictness
412
+ # typed: #{strictness}
413
+ SIGIL
275
414
 
276
- HEAD
415
+ [statement, sigil].compact.join("\n").strip.concat("\n\n")
277
416
  end
278
417
 
279
418
  sig { params(gem: Gemfile::Gem).void }
280
- def compile_rbi(gem)
419
+ def compile_gem_rbi(gem)
281
420
  compiler = Compilers::SymbolTableCompiler.new
282
421
  gem_name = set_color(gem.name, :yellow, :bold)
283
422
  say("Compiling #{gem_name}, this may take a few seconds... ")
284
423
 
285
- typed_sigil = config.typed_overrides[gem.name] || "true"
424
+ strictness = config.typed_overrides[gem.name] || "true"
286
425
 
287
- content = compiler.compile(gem)
288
- content.prepend(rbi_header(config.generate_command, typed_sigil))
426
+ content = String.new
427
+ content << rbi_header(
428
+ config.generate_command,
429
+ reason: "types exported from the `#{gem.name}` gem",
430
+ strictness: strictness
431
+ )
432
+ content << compiler.compile(gem)
289
433
 
290
434
  FileUtils.mkdir_p(config.outdir)
291
435
  filename = config.outpath / gem.rbi_file_name
@@ -297,5 +441,27 @@ module Tapioca
297
441
  remove(file) unless file.basename.to_s == gem.rbi_file_name
298
442
  end
299
443
  end
444
+
445
+ sig { params(constant: Module, contents: String).void }
446
+ def compile_dsl_rbi(constant, contents)
447
+ return if contents.nil?
448
+
449
+ command = format(config.generate_command, constant.name)
450
+ constant_name = Module.instance_method(:name).bind(constant).call
451
+ rbi_name = constant_name.underscore + ".rbi"
452
+ filename = config.outpath / rbi_name
453
+
454
+ out = String.new
455
+ out << rbi_header(
456
+ command,
457
+ reason: "dynamic methods in `#{constant.name}`"
458
+ )
459
+ out << contents
460
+
461
+ FileUtils.mkdir_p(File.dirname(filename))
462
+ File.write(filename, out)
463
+ say("Wrote: ", [:green])
464
+ say(filename)
465
+ end
300
466
  end
301
467
  end
@@ -24,6 +24,24 @@ module Tapioca
24
24
  load_rails_engines
25
25
  end
26
26
 
27
+ sig { params(environment_load: T::Boolean, eager_load: T::Boolean).void }
28
+ def load_rails(environment_load: false, eager_load: false)
29
+ return unless File.exist?("config/application.rb")
30
+
31
+ safe_require("rails")
32
+
33
+ silence_deprecations
34
+
35
+ safe_require("rails/generators/test_case")
36
+ if environment_load
37
+ safe_require("./config/environment")
38
+ else
39
+ safe_require("./config/application")
40
+ end
41
+
42
+ eager_load_rails_app if eager_load
43
+ end
44
+
27
45
  private
28
46
 
29
47
  sig { returns(Tapioca::Gemfile) }
@@ -80,15 +98,8 @@ module Tapioca
80
98
  end
81
99
 
82
100
  sig { void }
83
- def load_rails
84
- return unless File.exist?("config/application.rb")
85
-
86
- safe_require("rails")
87
-
88
- silence_deprecations
89
-
90
- safe_require("rails/generators/test_case")
91
- safe_require("./config/application")
101
+ def eager_load_rails_app
102
+ Object.const_get("Rails").autoloaders.each(&:eager_load)
92
103
  end
93
104
 
94
105
  sig { void }