tapioca 0.2.7 → 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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +27 -1
  3. data/README.md +21 -2
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +15 -9
  6. data/lib/tapioca/cli.rb +41 -12
  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/sorbet.rb +34 -0
  26. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +209 -49
  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 +42 -0
  30. data/lib/tapioca/config_builder.rb +75 -0
  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 +14 -1
  34. data/lib/tapioca/generator.rb +235 -67
  35. data/lib/tapioca/loader.rb +20 -9
  36. data/lib/tapioca/sorbet_config_parser.rb +77 -0
  37. data/lib/tapioca/version.rb +1 -1
  38. metadata +35 -66
@@ -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
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ class Config < T::Struct
6
+ extend(T::Sig)
7
+
8
+ const(:outdir, String)
9
+ const(:prerequire, T.nilable(String))
10
+ const(:postrequire, String)
11
+ const(:generate_command, String)
12
+ const(:exclude, T::Array[String])
13
+ const(:typed_overrides, T::Hash[String, String])
14
+ const(:todos_path, String)
15
+ const(:generators, T::Array[String])
16
+
17
+ sig { returns(Pathname) }
18
+ def outpath
19
+ @outpath = T.let(@outpath, T.nilable(Pathname))
20
+ @outpath ||= Pathname.new(outdir)
21
+ end
22
+
23
+ private_class_method :new
24
+
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)
35
+
36
+ DEFAULT_OVERRIDES = T.let({
37
+ # ActiveSupport overrides some core methods with different signatures
38
+ # so we generate a typed: false RBI for it to suppress errors
39
+ "activesupport" => "false",
40
+ }.freeze, T::Hash[String, String])
41
+ end
42
+ end
@@ -0,0 +1,75 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'yaml'
5
+
6
+ module Tapioca
7
+ class ConfigBuilder
8
+ class << self
9
+ extend(T::Sig)
10
+
11
+ sig { params(command: Symbol, options: T::Hash[String, T.untyped]).returns(Config) }
12
+ def from_options(command, options)
13
+ Config.from_hash(
14
+ merge_options(default_options(command), config_options, options)
15
+ )
16
+ end
17
+
18
+ private
19
+
20
+ sig { returns(T::Hash[String, T.untyped]) }
21
+ def config_options
22
+ if File.exist?(Config::TAPIOCA_CONFIG)
23
+ YAML.load_file(Config::TAPIOCA_CONFIG, fallback: {})
24
+ else
25
+ {}
26
+ end
27
+ end
28
+
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)
41
+ end
42
+
43
+ sig { returns(String) }
44
+ def default_command
45
+ command = File.basename($PROGRAM_NAME)
46
+ args = ARGV.join(" ")
47
+
48
+ "#{command} #{args}".strip
49
+ end
50
+
51
+ sig { params(options: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
52
+ def merge_options(*options)
53
+ options.each_with_object({}) do |option, result|
54
+ result.merge!(option) do |_, this_val, other_val|
55
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
56
+ this_val.merge(other_val)
57
+ else
58
+ other_val
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ DEFAULT_OPTIONS = T.let({
66
+ "postrequire" => Config::DEFAULT_POSTREQUIRE,
67
+ "outdir" => nil,
68
+ "generate_command" => default_command,
69
+ "exclude" => [],
70
+ "typed_overrides" => Config::DEFAULT_OVERRIDES,
71
+ "todos_path" => Config::DEFAULT_TODOSPATH,
72
+ "generators" => [],
73
+ }.freeze, T::Hash[String, T.untyped])
74
+ end
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
@@ -88,7 +88,8 @@ module Tapioca
88
88
  sig { params(spec: Spec).void }
89
89
  def initialize(spec)
90
90
  @spec = T.let(spec, Tapioca::Gemfile::Spec)
91
- @full_gem_path = T.let(@spec.full_gem_path.to_s, String)
91
+ real_gem_path = to_realpath(@spec.full_gem_path)
92
+ @full_gem_path = T.let(real_gem_path, String)
92
93
  end
93
94
 
94
95
  sig { params(gemfile_dir: String).returns(T::Boolean) }
@@ -118,8 +119,20 @@ module Tapioca
118
119
  "#{name}@#{version}.rbi"
119
120
  end
120
121
 
122
+ sig { params(path: String).returns(T::Boolean) }
123
+ def contains_path?(path)
124
+ to_realpath(path).start_with?(full_gem_path)
125
+ end
126
+
121
127
  private
122
128
 
129
+ sig { params(path: T.any(String, Pathname)).returns(String) }
130
+ def to_realpath(path)
131
+ path_string = path.to_s
132
+ path_string = File.realpath(path_string) if File.exist?(path_string)
133
+ path_string
134
+ end
135
+
123
136
  sig { returns(T::Boolean) }
124
137
  def gem_ignored?
125
138
  IGNORED_GEMS.include?(name)
@@ -8,41 +8,16 @@ module Tapioca
8
8
  class Generator < ::Thor::Shell::Color
9
9
  extend(T::Sig)
10
10
 
11
- SORBET_CONFIG = "sorbet/config"
12
- DEFAULT_POSTREQUIRE = "sorbet/tapioca/require.rb"
13
- DEFAULT_OUTDIR = "sorbet/rbi/gems"
14
- DEFAULT_OVERRIDES = T.let({
15
- # ActiveSupport overrides some core methods with different signatures
16
- # so we generate a typed: false RBI for it to suppress errors
17
- "activesupport" => "false",
18
- }.freeze, T::Hash[String, String])
19
-
20
- sig { returns(Pathname) }
21
- attr_reader :outdir
22
- sig { returns(T.nilable(String)) }
23
- attr_reader :prerequire
24
- sig { returns(T.nilable(String)) }
25
- attr_reader :postrequire
26
- sig { returns(String) }
27
- attr_reader :command
28
- sig { returns(T::Hash[String, String]) }
29
- attr_reader :typed_overrides
11
+ sig { returns(Config) }
12
+ attr_reader :config
30
13
 
31
14
  sig do
32
15
  params(
33
- outdir: T.nilable(String),
34
- prerequire: T.nilable(String),
35
- postrequire: T.nilable(String),
36
- command: T.nilable(String),
37
- typed_overrides: T.nilable(T::Hash[String, String])
16
+ config: Config
38
17
  ).void
39
18
  end
40
- def initialize(outdir: nil, prerequire: nil, postrequire: nil, command: nil, typed_overrides: nil)
41
- @outdir = T.let(Pathname.new(outdir || DEFAULT_OUTDIR), Pathname)
42
- @prerequire = T.let(prerequire, T.nilable(String))
43
- @postrequire = T.let(postrequire || DEFAULT_POSTREQUIRE, T.nilable(String))
44
- @command = T.let(command || default_command, String)
45
- @typed_overrides = T.let(typed_overrides || {}, T::Hash[String, String])
19
+ def initialize(config)
20
+ @config = config
46
21
  @bundle = T.let(nil, T.nilable(Gemfile))
47
22
  @loader = T.let(nil, T.nilable(Loader))
48
23
  @compiler = T.let(nil, T.nilable(Compilers::SymbolTableCompiler))
@@ -55,14 +30,115 @@ module Tapioca
55
30
  def build_gem_rbis(gem_names)
56
31
  require_gem_file
57
32
 
58
- gems_to_generate(gem_names).map do |gem|
59
- say("Processing '#{gem.name}' gem:", :green)
60
- indent do
61
- compile_rbi(gem)
62
- puts
33
+ gems_to_generate(gem_names)
34
+ .reject { |gem| config.exclude.include?(gem.name) }
35
+ .each do |gem|
36
+ say("Processing '#{gem.name}' gem:", :green)
37
+ indent do
38
+ compile_gem_rbi(gem)
39
+ puts
40
+ end
63
41
  end
42
+
43
+ say("All operations performed in working directory.", [:green, :bold])
44
+ say("Please review changes and commit them.", [:green, :bold])
45
+ end
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
64
98
  end
65
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
+
66
142
  say("All operations performed in working directory.", [:green, :bold])
67
143
  say("Please review changes and commit them.", [:green, :bold])
68
144
  end
@@ -86,14 +162,6 @@ module Tapioca
86
162
 
87
163
  private
88
164
 
89
- sig { returns(String) }
90
- def default_command
91
- command = File.basename($PROGRAM_NAME)
92
- args = ARGV.join(" ")
93
-
94
- "#{command} #{args}"
95
- end
96
-
97
165
  sig { returns(Gemfile) }
98
166
  def bundle
99
167
  @bundle ||= Gemfile.new
@@ -112,42 +180,111 @@ module Tapioca
112
180
  sig { void }
113
181
  def require_gem_file
114
182
  say("Requiring all gems to prepare for compiling... ")
115
- loader.load_bundle(prerequire, 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
116
189
  say(" Done", :green)
117
190
  puts
118
191
  end
119
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
+
120
256
  sig { returns(T::Hash[String, String]) }
121
257
  def existing_rbis
122
- @existing_rbis ||= Pathname.glob((Pathname.new(outdir) / "*@*.rbi").to_s)
123
- .map { |f| f.basename(".*").to_s.split('@') }
258
+ @existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s)
259
+ .map { |f| T.cast(f.basename(".*").to_s.split('@', 2), [String, String]) }
124
260
  .to_h
125
261
  end
126
262
 
127
263
  sig { returns(T::Hash[String, String]) }
128
264
  def expected_rbis
129
265
  @expected_rbis ||= bundle.dependencies
266
+ .reject { |gem| config.exclude.include?(gem.name) }
130
267
  .map { |gem| [gem.name, gem.version.to_s] }
131
268
  .to_h
132
269
  end
133
270
 
134
271
  sig { params(gem_name: String, version: String).returns(Pathname) }
135
- def rbi_filename(gem_name, version)
136
- outdir / "#{gem_name}@#{version}.rbi"
272
+ def gem_rbi_filename(gem_name, version)
273
+ config.outpath / "#{gem_name}@#{version}.rbi"
137
274
  end
138
275
 
139
276
  sig { params(gem_name: String).returns(Pathname) }
140
277
  def existing_rbi(gem_name)
141
- rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
278
+ gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
142
279
  end
143
280
 
144
281
  sig { params(gem_name: String).returns(Pathname) }
145
282
  def expected_rbi(gem_name)
146
- rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
283
+ gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
147
284
  end
148
285
 
149
286
  sig { params(gem_name: String).returns(T::Boolean) }
150
- def rbi_exists?(gem_name)
287
+ def gem_rbi_exists?(gem_name)
151
288
  existing_rbis.key?(gem_name)
152
289
  end
153
290
 
@@ -225,13 +362,13 @@ module Tapioca
225
362
  gems.each do |gem_name|
226
363
  filename = expected_rbi(gem_name)
227
364
 
228
- if rbi_exists?(gem_name)
365
+ if gem_rbi_exists?(gem_name)
229
366
  old_filename = existing_rbi(gem_name)
230
367
  move(old_filename, filename) unless old_filename == filename
231
368
  end
232
369
 
233
370
  gem = T.must(bundle.gem(gem_name))
234
- compile_rbi(gem)
371
+ compile_gem_rbi(gem)
235
372
  add(filename)
236
373
 
237
374
  puts
@@ -263,37 +400,68 @@ module Tapioca
263
400
  end
264
401
  end
265
402
 
266
- sig { params(command: String, typed_sigil: String).returns(String) }
267
- def rbi_header(command, typed_sigil)
268
- <<~HEAD
269
- # This file is autogenerated. Do not edit it by hand. Regenerate it with:
270
- # #{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
271
410
 
272
- # typed: #{typed_sigil}
411
+ sigil = <<~SIGIL if strictness
412
+ # typed: #{strictness}
413
+ SIGIL
273
414
 
274
- HEAD
415
+ [statement, sigil].compact.join("\n").strip.concat("\n\n")
275
416
  end
276
417
 
277
418
  sig { params(gem: Gemfile::Gem).void }
278
- def compile_rbi(gem)
419
+ def compile_gem_rbi(gem)
279
420
  compiler = Compilers::SymbolTableCompiler.new
280
421
  gem_name = set_color(gem.name, :yellow, :bold)
281
422
  say("Compiling #{gem_name}, this may take a few seconds... ")
282
423
 
283
- typed_sigil = typed_overrides[gem.name] || DEFAULT_OVERRIDES[gem.name] || "true"
424
+ strictness = config.typed_overrides[gem.name] || "true"
284
425
 
285
- content = compiler.compile(gem)
286
- content.prepend(rbi_header(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)
287
433
 
288
- FileUtils.mkdir_p(outdir)
289
- filename = outdir / gem.rbi_file_name
434
+ FileUtils.mkdir_p(config.outdir)
435
+ filename = config.outpath / gem.rbi_file_name
290
436
  File.write(filename.to_s, content)
291
437
 
292
438
  say("Done", :green)
293
439
 
294
- Pathname.glob((outdir / "#{gem.name}@*.rbi").to_s) do |file|
440
+ Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
295
441
  remove(file) unless file.basename.to_s == gem.rbi_file_name
296
442
  end
297
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
298
466
  end
299
467
  end