tapioca 0.3.0 → 0.4.3

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 (38) 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 +31 -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 +387 -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 +223 -35
  27. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +3 -17
  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/sorbet_config_parser.rb +77 -0
  37. data/lib/tapioca/version.rb +1 -1
  38. metadata +31 -51
@@ -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
 
@@ -33,7 +29,7 @@ module Tapioca
33
29
  file.write(Array(paths).join("\n"))
34
30
  file.flush
35
31
 
36
- symbol_table_json_from("@#{file.path}")
32
+ symbol_table_json_from("@#{file.path.shellescape}")
37
33
  end, T.nilable(String))
38
34
 
39
35
  return Set.new if output.nil? || output.empty?
@@ -44,7 +40,7 @@ module Tapioca
44
40
 
45
41
  def ignored_symbols
46
42
  unless @ignored_symbols
47
- output = symbol_table_json_from("''", table_type: "symbol-table-full-json")
43
+ output = symbol_table_json_from("-e ''", table_type: "symbol-table-full-json")
48
44
  json = JSON.parse(output)
49
45
  @ignored_symbols = SymbolTableParser.parse(json)
50
46
  end
@@ -53,17 +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,
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
- ].shelljoin,
65
- err: "/dev/null"
66
- ).read
52
+ Tapioca::Compilers::Sorbet.run("--no-config", "--print=#{table_type}", input)
67
53
  end
68
54
  end
69
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