tapioca 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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 }