tapioca 0.3.1 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +25 -1
  3. data/README.md +23 -2
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +8 -2
  6. data/lib/tapioca/cli.rb +32 -3
  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 +267 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +393 -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 +165 -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 +92 -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/sorbet.rb +34 -0
  26. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +171 -26
  27. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +1 -20
  28. data/lib/tapioca/compilers/todos_compiler.rb +32 -0
  29. data/lib/tapioca/config.rb +14 -6
  30. data/lib/tapioca/config_builder.rb +22 -9
  31. data/lib/tapioca/constant_locator.rb +1 -0
  32. data/lib/tapioca/core_ext/class.rb +23 -0
  33. data/lib/tapioca/gemfile.rb +32 -9
  34. data/lib/tapioca/generator.rb +231 -23
  35. data/lib/tapioca/loader.rb +30 -9
  36. data/lib/tapioca/version.rb +1 -1
  37. metadata +32 -39
@@ -2,16 +2,12 @@
2
2
  # typed: true
3
3
 
4
4
  require 'json'
5
- require 'pathname'
6
5
  require 'tempfile'
7
- require 'shellwords'
8
6
 
9
7
  module Tapioca
10
8
  module Compilers
11
9
  module SymbolTable
12
10
  module SymbolLoader
13
- SORBET = Pathname.new(Gem::Specification.find_by_name("sorbet-static").full_gem_path) / "libexec" / "sorbet"
14
-
15
11
  class << self
16
12
  extend(T::Sig)
17
13
 
@@ -53,22 +49,7 @@ module Tapioca
53
49
  end
54
50
 
55
51
  def symbol_table_json_from(input, table_type: "symbol-table-json")
56
- IO.popen(
57
- [
58
- sorbet_path,
59
- # We don't want to pick up any sorbet/config files in cwd
60
- "--no-config",
61
- "--print=#{table_type}",
62
- "--quiet",
63
- input,
64
- ].join(' '),
65
- err: "/dev/null"
66
- ).read
67
- end
68
-
69
- sig { returns(String) }
70
- def sorbet_path
71
- SORBET.to_s.shellescape
52
+ Tapioca::Compilers::Sorbet.run("--no-config", "--print=#{table_type}", input)
72
53
  end
73
54
  end
74
55
 
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ # typed: strong
3
+
4
+ module Tapioca
5
+ module Compilers
6
+ # Taken from https://github.com/sorbet/sorbet/blob/master/gems/sorbet/lib/todo-rbi.rb
7
+ class TodosCompiler
8
+ extend(T::Sig)
9
+
10
+ sig do
11
+ returns(String)
12
+ end
13
+ def compile
14
+ list_todos.each_line.map do |line|
15
+ next if line.include?("<") || line.include?("class_of")
16
+ "module #{line.strip.gsub('T.untyped::', '')}; end"
17
+ end.compact.join("\n")
18
+ end
19
+
20
+ private
21
+
22
+ sig { returns(String) }
23
+ def list_todos
24
+ Tapioca::Compilers::Sorbet.run(
25
+ "--print=missing-constants",
26
+ "--stdout-hup-hack",
27
+ "--no-error-count"
28
+ ).strip
29
+ end
30
+ end
31
+ end
32
+ end
@@ -11,20 +11,28 @@ module Tapioca
11
11
  const(:generate_command, String)
12
12
  const(:exclude, T::Array[String])
13
13
  const(:typed_overrides, T::Hash[String, String])
14
+ const(:todos_path, String)
15
+ const(:generators, T::Array[String])
14
16
 
15
17
  sig { returns(Pathname) }
16
18
  def outpath
17
- @outpath ||= T.let(Pathname.new(outdir), T.nilable(Pathname))
18
- T.must(@outpath)
19
+ @outpath = T.let(@outpath, T.nilable(Pathname))
20
+ @outpath ||= Pathname.new(outdir)
19
21
  end
20
22
 
21
23
  private_class_method :new
22
24
 
23
- CONFIG_FILE_PATH = "sorbet/tapioca/config.yml"
24
- 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)
25
35
 
26
- DEFAULT_POSTREQUIRE = "sorbet/tapioca/require.rb"
27
- DEFAULT_OUTDIR = "sorbet/rbi/gems"
28
36
  DEFAULT_OVERRIDES = T.let({
29
37
  # ActiveSupport overrides some core methods with different signatures
30
38
  # so we generate a typed: false RBI for it to suppress errors
@@ -1,15 +1,17 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'yaml'
5
+
4
6
  module Tapioca
5
7
  class ConfigBuilder
6
8
  class << self
7
9
  extend(T::Sig)
8
10
 
9
- sig { params(options: T::Hash[String, T.untyped]).returns(Config) }
10
- def from_options(options)
11
+ sig { params(command: Symbol, options: T::Hash[String, T.untyped]).returns(Config) }
12
+ def from_options(command, options)
11
13
  Config.from_hash(
12
- merge_options(default_options, config_options, options)
14
+ merge_options(default_options(command), config_options, options)
13
15
  )
14
16
  end
15
17
 
@@ -17,16 +19,25 @@ module Tapioca
17
19
 
18
20
  sig { returns(T::Hash[String, T.untyped]) }
19
21
  def config_options
20
- if File.exist?(Config::CONFIG_FILE_PATH)
21
- YAML.load_file(Config::CONFIG_FILE_PATH, fallback: {})
22
+ if File.exist?(Config::TAPIOCA_CONFIG)
23
+ YAML.load_file(Config::TAPIOCA_CONFIG, fallback: {})
22
24
  else
23
25
  {}
24
26
  end
25
27
  end
26
28
 
27
- sig { returns(T::Hash[String, T.untyped]) }
28
- def default_options
29
- 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)
30
41
  end
31
42
 
32
43
  sig { returns(String) }
@@ -53,10 +64,12 @@ module Tapioca
53
64
 
54
65
  DEFAULT_OPTIONS = T.let({
55
66
  "postrequire" => Config::DEFAULT_POSTREQUIRE,
56
- "outdir" => Config::DEFAULT_OUTDIR,
67
+ "outdir" => nil,
57
68
  "generate_command" => default_command,
58
69
  "exclude" => [],
59
70
  "typed_overrides" => Config::DEFAULT_OVERRIDES,
71
+ "todos_path" => Config::DEFAULT_TODOSPATH,
72
+ "generators" => [],
60
73
  }.freeze, T::Hash[String, T.untyped])
61
74
  end
62
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
@@ -28,7 +28,7 @@ module Tapioca
28
28
  sig { returns(T::Array[Gem]) }
29
29
  def dependencies
30
30
  @dependencies ||= begin
31
- specs = definition.specs.to_a
31
+ specs = definition.locked_gems.specs.to_a
32
32
 
33
33
  definition
34
34
  .resolve
@@ -79,17 +79,18 @@ module Tapioca
79
79
  extend(T::Sig)
80
80
 
81
81
  IGNORED_GEMS = T.let(%w{
82
- sorbet sorbet-static sorbet-runtime tapioca
82
+ sorbet sorbet-static sorbet-runtime
83
83
  }.freeze, T::Array[String])
84
84
 
85
85
  sig { returns(String) }
86
- attr_reader :full_gem_path
86
+ attr_reader :full_gem_path, :version
87
87
 
88
88
  sig { params(spec: Spec).void }
89
89
  def initialize(spec)
90
90
  @spec = T.let(spec, Tapioca::Gemfile::Spec)
91
91
  real_gem_path = to_realpath(@spec.full_gem_path)
92
92
  @full_gem_path = T.let(real_gem_path, String)
93
+ @version = T.let(version_string, String)
93
94
  end
94
95
 
95
96
  sig { params(gemfile_dir: String).returns(T::Boolean) }
@@ -109,11 +110,6 @@ module Tapioca
109
110
  @spec.name
110
111
  end
111
112
 
112
- sig { returns(::Gem::Version) }
113
- def version
114
- @spec.version
115
- end
116
-
117
113
  sig { returns(String) }
118
114
  def rbi_file_name
119
115
  "#{name}@#{version}.rbi"
@@ -121,11 +117,38 @@ module Tapioca
121
117
 
122
118
  sig { params(path: String).returns(T::Boolean) }
123
119
  def contains_path?(path)
124
- to_realpath(path).start_with?(full_gem_path)
120
+ to_realpath(path).start_with?(full_gem_path) || has_parent_gemspec?(path)
125
121
  end
126
122
 
127
123
  private
128
124
 
125
+ sig { returns(String) }
126
+ def version_string
127
+ version = @spec.version.to_s
128
+ version += "-#{@spec.source.revision}" if Bundler::Source::Git === @spec.source
129
+ version
130
+ end
131
+
132
+ sig { params(path: String).returns(T::Boolean) }
133
+ def has_parent_gemspec?(path)
134
+ # For some Git installed gems the location of the loaded file can
135
+ # be different from the gem path as indicated by the spec file
136
+ #
137
+ # To compensate for these cases, we walk up the directory hierarchy
138
+ # from the given file and try to match a <gem-name.gemspec> file in
139
+ # one of those folders to see if the path really belongs in the given gem
140
+ # or not.
141
+ return false unless Bundler::Source::Git === @spec.source
142
+ parent = Pathname.new(path)
143
+
144
+ until parent.root?
145
+ parent = parent.parent.expand_path
146
+ return true if parent.join("#{name}.gemspec").file?
147
+ end
148
+
149
+ false
150
+ end
151
+
129
152
  sig { params(path: T.any(String, Pathname)).returns(String) }
130
153
  def to_realpath(path)
131
154
  path_string = path.to_s
@@ -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,105 @@ 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
+
83
+ sig { void }
84
+ def build_todos
85
+ todos_path = config.todos_path
86
+ compiler = Compilers::TodosCompiler.new
87
+ name = set_color(todos_path, :yellow, :bold)
88
+ say("Compiling #{name}, this may take a few seconds... ")
89
+
90
+ # Clean all existing unresolved constants before regenerating the list
91
+ # so Sorbet won't grab them as already resolved.
92
+ File.delete(todos_path) if File.exist?(todos_path)
93
+
94
+ rbi_string = compiler.compile
95
+ if rbi_string.empty?
96
+ say("Nothing to do", :green)
97
+ return
98
+ end
99
+
100
+ content = String.new
101
+ content << rbi_header(
102
+ config.generate_command,
103
+ reason: "unresolved constants",
104
+ strictness: "false"
105
+ )
106
+ content << rbi_string
107
+ content << "\n"
108
+
109
+ outdir = File.dirname(todos_path)
110
+ FileUtils.mkdir_p(outdir)
111
+ File.write(todos_path, content)
112
+
113
+ say("Done", :green)
114
+
115
+ say("All unresolved constants have been written to #{name}.", [:green, :bold])
116
+ say("Please review changes and commit them.", [:green, :bold])
117
+ end
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
+
47
146
  sig { void }
48
147
  def sync_rbis_with_gemfile
49
148
  anything_done = [
@@ -63,6 +162,11 @@ module Tapioca
63
162
 
64
163
  private
65
164
 
165
+ EMPTY_RBI_COMMENT = <<~CONTENT
166
+ # THIS IS AN EMPTY RBI FILE.
167
+ # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
168
+ CONTENT
169
+
66
170
  sig { returns(Gemfile) }
67
171
  def bundle
68
172
  @bundle ||= Gemfile.new
@@ -81,15 +185,83 @@ module Tapioca
81
185
  sig { void }
82
186
  def require_gem_file
83
187
  say("Requiring all gems to prepare for compiling... ")
84
- loader.load_bundle(config.prerequire, config.postrequire)
188
+ begin
189
+ loader.load_bundle(config.prerequire, config.postrequire)
190
+ rescue LoadError => e
191
+ explain_failed_require(config.postrequire, e)
192
+ exit(1)
193
+ end
85
194
  say(" Done", :green)
86
195
  puts
87
196
  end
88
197
 
198
+ sig { params(file: String, error: LoadError).void }
199
+ def explain_failed_require(file, error)
200
+ say_error("\n\nLoadError: #{error}", :bold, :red)
201
+ say_error("\nTapioca could not load all the gems required by your application.", :yellow)
202
+ say_error("If you populated ", :yellow)
203
+ say_error("#{file} ", :bold, :blue)
204
+ say_error("with ", :yellow)
205
+ say_error("tapioca require", :bold, :blue)
206
+ say_error("you should probably review it and remove the faulty line.", :yellow)
207
+ end
208
+
209
+ sig do
210
+ params(
211
+ message: String,
212
+ color: T.any(Symbol, T::Array[Symbol]),
213
+ ).void
214
+ end
215
+ def say_error(message = "", *color)
216
+ force_new_line = (message.to_s !~ /( |\t)\Z/)
217
+ buffer = prepare_message(*T.unsafe([message, *T.unsafe(color)]))
218
+ buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
219
+
220
+ stderr.print(buffer)
221
+ stderr.flush
222
+ end
223
+
224
+ sig { params(eager_load: T::Boolean).void }
225
+ def load_application(eager_load:)
226
+ say("Loading Rails application... ")
227
+
228
+ loader.load_rails(
229
+ environment_load: true,
230
+ eager_load: eager_load
231
+ )
232
+
233
+ say("Done", :green)
234
+ end
235
+
236
+ sig { void }
237
+ def load_dsl_generators
238
+ say("Loading DSL generator classes... ")
239
+
240
+ Dir.glob([
241
+ "#{__dir__}/compilers/dsl/*.rb",
242
+ "#{Config::TAPIOCA_PATH}/generators/**/*.rb",
243
+ ]).each do |generator|
244
+ require File.expand_path(generator)
245
+ end
246
+
247
+ say("Done", :green)
248
+ end
249
+
250
+ sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
251
+ def constantize(constant_names)
252
+ constant_names.map do |name|
253
+ begin
254
+ name.constantize
255
+ rescue NameError
256
+ nil
257
+ end
258
+ end.compact
259
+ end
260
+
89
261
  sig { returns(T::Hash[String, String]) }
90
262
  def existing_rbis
91
263
  @existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s)
92
- .map { |f| f.basename(".*").to_s.split('@') }
264
+ .map { |f| T.cast(f.basename(".*").to_s.split('@', 2), [String, String]) }
93
265
  .to_h
94
266
  end
95
267
 
@@ -102,22 +274,22 @@ module Tapioca
102
274
  end
103
275
 
104
276
  sig { params(gem_name: String, version: String).returns(Pathname) }
105
- def rbi_filename(gem_name, version)
277
+ def gem_rbi_filename(gem_name, version)
106
278
  config.outpath / "#{gem_name}@#{version}.rbi"
107
279
  end
108
280
 
109
281
  sig { params(gem_name: String).returns(Pathname) }
110
282
  def existing_rbi(gem_name)
111
- rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
283
+ gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
112
284
  end
113
285
 
114
286
  sig { params(gem_name: String).returns(Pathname) }
115
287
  def expected_rbi(gem_name)
116
- rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
288
+ gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
117
289
  end
118
290
 
119
291
  sig { params(gem_name: String).returns(T::Boolean) }
120
- def rbi_exists?(gem_name)
292
+ def gem_rbi_exists?(gem_name)
121
293
  existing_rbis.key?(gem_name)
122
294
  end
123
295
 
@@ -195,13 +367,13 @@ module Tapioca
195
367
  gems.each do |gem_name|
196
368
  filename = expected_rbi(gem_name)
197
369
 
198
- if rbi_exists?(gem_name)
370
+ if gem_rbi_exists?(gem_name)
199
371
  old_filename = existing_rbi(gem_name)
200
372
  move(old_filename, filename) unless old_filename == filename
201
373
  end
202
374
 
203
375
  gem = T.must(bundle.gem(gem_name))
204
- compile_rbi(gem)
376
+ compile_gem_rbi(gem)
205
377
  add(filename)
206
378
 
207
379
  puts
@@ -233,37 +405,73 @@ module Tapioca
233
405
  end
234
406
  end
235
407
 
236
- sig { params(command: String, typed_sigil: String).returns(String) }
237
- def rbi_header(command, typed_sigil)
238
- <<~HEAD
239
- # This file is autogenerated. Do not edit it by hand. Regenerate it with:
240
- # #{command}
408
+ sig { params(command: String, reason: T.nilable(String), strictness: T.nilable(String)).returns(String) }
409
+ def rbi_header(command, reason: nil, strictness: nil)
410
+ statement = <<~HEAD
411
+ # DO NOT EDIT MANUALLY
412
+ # This is an autogenerated file for #{reason}.
413
+ # Please instead update this file by running `#{command}`.
414
+ HEAD
241
415
 
242
- # typed: #{typed_sigil}
416
+ sigil = <<~SIGIL if strictness
417
+ # typed: #{strictness}
418
+ SIGIL
243
419
 
244
- HEAD
420
+ [statement, sigil].compact.join("\n").strip.concat("\n\n")
245
421
  end
246
422
 
247
423
  sig { params(gem: Gemfile::Gem).void }
248
- def compile_rbi(gem)
424
+ def compile_gem_rbi(gem)
249
425
  compiler = Compilers::SymbolTableCompiler.new
250
426
  gem_name = set_color(gem.name, :yellow, :bold)
251
427
  say("Compiling #{gem_name}, this may take a few seconds... ")
252
428
 
253
- typed_sigil = config.typed_overrides[gem.name] || "true"
254
-
255
- content = compiler.compile(gem)
256
- content.prepend(rbi_header(config.generate_command, typed_sigil))
429
+ strictness = config.typed_overrides[gem.name] || "true"
430
+ rbi_body_content = compiler.compile(gem)
431
+ content = String.new
432
+ content << rbi_header(
433
+ config.generate_command,
434
+ reason: "types exported from the `#{gem.name}` gem",
435
+ strictness: strictness
436
+ )
257
437
 
258
438
  FileUtils.mkdir_p(config.outdir)
259
439
  filename = config.outpath / gem.rbi_file_name
260
- File.write(filename.to_s, content)
261
440
 
262
- say("Done", :green)
441
+ if rbi_body_content.strip.empty?
442
+ content << EMPTY_RBI_COMMENT
443
+ say("Done (empty output)", :yellow)
444
+ else
445
+ content << rbi_body_content
446
+ say("Done", :green)
447
+ end
448
+ File.write(filename.to_s, content)
263
449
 
264
450
  Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
265
451
  remove(file) unless file.basename.to_s == gem.rbi_file_name
266
452
  end
267
453
  end
454
+
455
+ sig { params(constant: Module, contents: String).void }
456
+ def compile_dsl_rbi(constant, contents)
457
+ return if contents.nil?
458
+
459
+ command = format(config.generate_command, constant.name)
460
+ constant_name = Module.instance_method(:name).bind(constant).call
461
+ rbi_name = constant_name.underscore + ".rbi"
462
+ filename = config.outpath / rbi_name
463
+
464
+ out = String.new
465
+ out << rbi_header(
466
+ command,
467
+ reason: "dynamic methods in `#{constant.name}`"
468
+ )
469
+ out << contents
470
+
471
+ FileUtils.mkdir_p(File.dirname(filename))
472
+ File.write(filename, out)
473
+ say("Wrote: ", [:green])
474
+ say(filename)
475
+ end
268
476
  end
269
477
  end