tapioca 0.2.7 → 0.4.1

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