tapioca 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +114 -23
  3. data/lib/tapioca/cli.rb +188 -65
  4. data/lib/tapioca/compilers/dsl/active_record_associations.rb +94 -8
  5. data/lib/tapioca/compilers/dsl/active_record_columns.rb +5 -4
  6. data/lib/tapioca/compilers/dsl/active_record_enum.rb +1 -1
  7. data/lib/tapioca/compilers/dsl/active_record_relations.rb +703 -0
  8. data/lib/tapioca/compilers/dsl/active_record_scope.rb +43 -13
  9. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +39 -33
  10. data/lib/tapioca/compilers/dsl/base.rb +26 -42
  11. data/lib/tapioca/compilers/dsl/extensions/frozen_record.rb +29 -0
  12. data/lib/tapioca/compilers/dsl/frozen_record.rb +37 -0
  13. data/lib/tapioca/compilers/dsl/helper/active_record_constants.rb +27 -0
  14. data/lib/tapioca/compilers/dsl/identity_cache.rb +0 -1
  15. data/lib/tapioca/compilers/dsl/param_helper.rb +52 -0
  16. data/lib/tapioca/compilers/dsl/rails_generators.rb +120 -0
  17. data/lib/tapioca/compilers/dsl_compiler.rb +34 -6
  18. data/lib/tapioca/compilers/sorbet.rb +2 -0
  19. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +51 -48
  20. data/lib/tapioca/executor.rb +79 -0
  21. data/lib/tapioca/gemfile.rb +28 -4
  22. data/lib/tapioca/generators/base.rb +11 -18
  23. data/lib/tapioca/generators/dsl.rb +33 -38
  24. data/lib/tapioca/generators/gem.rb +64 -34
  25. data/lib/tapioca/generators/init.rb +41 -16
  26. data/lib/tapioca/generators/todo.rb +6 -6
  27. data/lib/tapioca/helpers/cli_helper.rb +26 -0
  28. data/lib/tapioca/helpers/config_helper.rb +84 -0
  29. data/lib/tapioca/helpers/test/content.rb +51 -0
  30. data/lib/tapioca/helpers/test/isolation.rb +125 -0
  31. data/lib/tapioca/helpers/test/template.rb +34 -0
  32. data/lib/tapioca/internal.rb +3 -2
  33. data/lib/tapioca/rbi_ext/model.rb +13 -10
  34. data/lib/tapioca/reflection.rb +13 -0
  35. data/lib/tapioca/trackers/autoload.rb +70 -0
  36. data/lib/tapioca/trackers/constant_definition.rb +42 -0
  37. data/lib/tapioca/trackers/mixin.rb +78 -0
  38. data/lib/tapioca/trackers.rb +14 -0
  39. data/lib/tapioca/version.rb +1 -1
  40. data/lib/tapioca.rb +28 -2
  41. metadata +37 -13
  42. data/lib/tapioca/config.rb +0 -45
  43. data/lib/tapioca/config_builder.rb +0 -73
  44. data/lib/tapioca/constant_locator.rb +0 -34
@@ -7,7 +7,7 @@ module Tapioca
7
7
  sig do
8
8
  params(
9
9
  gem_names: T::Array[String],
10
- gem_excludes: T::Array[String],
10
+ exclude: T::Array[String],
11
11
  prerequire: T.nilable(String),
12
12
  postrequire: String,
13
13
  typed_overrides: T::Hash[String, String],
@@ -15,12 +15,14 @@ module Tapioca
15
15
  outpath: Pathname,
16
16
  file_header: T::Boolean,
17
17
  doc: T::Boolean,
18
- file_writer: Thor::Actions
18
+ include_exported_rbis: T::Boolean,
19
+ file_writer: Thor::Actions,
20
+ number_of_workers: T.nilable(Integer)
19
21
  ).void
20
22
  end
21
23
  def initialize(
22
24
  gem_names:,
23
- gem_excludes:,
25
+ exclude:,
24
26
  prerequire:,
25
27
  postrequire:,
26
28
  typed_overrides:,
@@ -28,15 +30,18 @@ module Tapioca
28
30
  outpath:,
29
31
  file_header:,
30
32
  doc:,
31
- file_writer: FileWriter.new
33
+ include_exported_rbis:,
34
+ file_writer: FileWriter.new,
35
+ number_of_workers: nil
32
36
  )
33
37
  @gem_names = gem_names
34
- @gem_excludes = gem_excludes
38
+ @exclude = exclude
35
39
  @prerequire = prerequire
36
40
  @postrequire = postrequire
37
41
  @typed_overrides = typed_overrides
38
42
  @outpath = outpath
39
43
  @file_header = file_header
44
+ @number_of_workers = number_of_workers
40
45
 
41
46
  super(default_command: default_command, file_writer: file_writer)
42
47
 
@@ -45,24 +50,32 @@ module Tapioca
45
50
  @existing_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
46
51
  @expected_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
47
52
  @doc = T.let(doc, T::Boolean)
53
+ @include_exported_rbis = include_exported_rbis
48
54
  end
49
55
 
50
56
  sig { override.void }
51
57
  def generate
52
58
  require_gem_file
53
59
 
54
- gems_to_generate(@gem_names)
55
- .reject { |gem| @gem_excludes.include?(gem.name) }
56
- .each do |gem|
57
- say("Processing '#{gem.name}' gem:", :green)
58
- shell.indent do
59
- compile_gem_rbi(gem)
60
- puts
61
- end
60
+ gem_queue = gems_to_generate(@gem_names).reject { |gem| @exclude.include?(gem.name) }
61
+ anything_done = [
62
+ perform_removals,
63
+ gem_queue.any?,
64
+ ].any?
65
+
66
+ Executor.new(gem_queue, number_of_workers: @number_of_workers).run_in_parallel do |gem|
67
+ shell.indent do
68
+ compile_gem_rbi(gem)
69
+ puts
62
70
  end
71
+ end
63
72
 
64
- say("All operations performed in working directory.", [:green, :bold])
65
- say("Please review changes and commit them.", [:green, :bold])
73
+ if anything_done
74
+ say("All operations performed in working directory.", [:green, :bold])
75
+ say("Please review changes and commit them.", [:green, :bold])
76
+ else
77
+ say("No operations performed, all RBIs are up-to-date.", [:green, :bold])
78
+ end
66
79
  end
67
80
 
68
81
  sig { params(should_verify: T::Boolean).void }
@@ -110,6 +123,9 @@ module Tapioca
110
123
  explain_failed_require(@postrequire, e)
111
124
  exit(1)
112
125
  end
126
+
127
+ Tapioca::Trackers::Autoload.eager_load_all!
128
+
113
129
  say(" Done", :green)
114
130
  unless bundle.missing_specs.empty?
115
131
  say(" completed with missing specs: ")
@@ -135,7 +151,6 @@ module Tapioca
135
151
  sig { params(gem: Gemfile::GemSpec).void }
136
152
  def compile_gem_rbi(gem)
137
153
  gem_name = set_color(gem.name, :yellow, :bold)
138
- say("Compiling #{gem_name}, this may take a few seconds... ")
139
154
 
140
155
  rbi = RBI::File.new(strictness: @typed_overrides[gem.name] || "true")
141
156
  rbi.set_file_header(
@@ -146,17 +161,19 @@ module Tapioca
146
161
 
147
162
  Compilers::SymbolTableCompiler.new.compile(gem, rbi, 0, @doc)
148
163
 
164
+ merge_with_exported_rbi(gem, rbi) if @include_exported_rbis
165
+
149
166
  if rbi.empty?
150
167
  rbi.set_empty_body_content
151
- say("Done (empty output)", :yellow)
168
+ say("Compiled #{gem_name} (empty output)", :yellow)
152
169
  else
153
- say("Done", :green)
170
+ say("Compiled #{gem_name}", :green)
154
171
  end
155
172
 
156
173
  create_file(@outpath / gem.rbi_file_name, rbi.transformed_string)
157
174
 
158
175
  T.unsafe(Pathname).glob((@outpath / "#{gem.name}@*.rbi").to_s) do |file|
159
- remove(file) unless file.basename.to_s == gem.rbi_file_name
176
+ remove_file(file) unless file.basename.to_s == gem.rbi_file_name
160
177
  end
161
178
  end
162
179
 
@@ -192,7 +209,7 @@ module Tapioca
192
209
  else
193
210
  gems.each do |removed|
194
211
  filename = existing_rbi(removed)
195
- remove(filename)
212
+ remove_file(filename)
196
213
  end
197
214
 
198
215
  anything_done = true
@@ -219,7 +236,7 @@ module Tapioca
219
236
  else
220
237
  require_gem_file
221
238
 
222
- gems.each do |gem_name|
239
+ Executor.new(gems, number_of_workers: @number_of_workers).run_in_parallel do |gem_name|
223
240
  filename = expected_rbi(gem_name)
224
241
 
225
242
  if gem_rbi_exists?(gem_name)
@@ -252,13 +269,6 @@ module Tapioca
252
269
  say_error("you should probably review it and remove the faulty line.", :yellow)
253
270
  end
254
271
 
255
- sig { params(filename: Pathname).void }
256
- def remove(filename)
257
- return unless filename.exist?
258
- say("-- Removing: #{filename}")
259
- filename.unlink
260
- end
261
-
262
272
  sig { returns(T::Array[String]) }
263
273
  def removed_rbis
264
274
  (existing_rbis.keys - expected_rbis.keys).sort
@@ -292,7 +302,7 @@ module Tapioca
292
302
  say("Nothing to do, all RBIs are up-to-date.")
293
303
  else
294
304
  say("RBI files are out-of-date. In your development environment, please run:", :green)
295
- say(" `#{Config::DEFAULT_COMMAND} #{command}`", [:green, :bold])
305
+ say(" `#{@default_command} #{command}`", [:green, :bold])
296
306
  say("Once it is complete, be sure to commit and push any changes", :green)
297
307
 
298
308
  say("")
@@ -322,7 +332,7 @@ module Tapioca
322
332
  sig { returns(T::Hash[String, String]) }
323
333
  def expected_rbis
324
334
  @expected_rbis ||= bundle.dependencies
325
- .reject { |gem| @gem_excludes.include?(gem.name) }
335
+ .reject { |gem| @exclude.include?(gem.name) }
326
336
  .map { |gem| [gem.name, gem.version.to_s] }
327
337
  .to_h
328
338
  end
@@ -334,11 +344,31 @@ module Tapioca
334
344
 
335
345
  sig { params(cause: Symbol, files: T::Array[String]).returns(String) }
336
346
  def build_error_for_files(cause, files)
337
- filenames = files.map do |file|
338
- @outpath / file
339
- end.join("\n - ")
347
+ " File(s) #{cause}:\n - #{files.join("\n - ")}"
348
+ end
349
+
350
+ sig { params(gem: Gemfile::GemSpec, file: RBI::File).void }
351
+ def merge_with_exported_rbi(gem, file)
352
+ return file unless gem.export_rbi_files?
353
+ tree = gem.exported_rbi_tree
354
+
355
+ unless tree.conflicts.empty?
356
+ say_error("\n\n RBIs exported by `#{gem.name}` contain conflicts and can't be used:", :yellow)
357
+
358
+ tree.conflicts.each do |conflict|
359
+ say_error("\n #{conflict}", :yellow)
360
+ say_error(" Found at:", :yellow)
361
+ say_error(" #{conflict.left.loc}", :yellow)
362
+ say_error(" #{conflict.right.loc}", :yellow)
363
+ end
364
+
365
+ return file
366
+ end
340
367
 
341
- " File(s) #{cause}:\n - #{filenames}"
368
+ file.root = RBI::Rewriters::Merge.merge_trees(file.root, tree, keep: RBI::Rewriters::Merge::Keep::LEFT)
369
+ rescue RBI::ParseError => e
370
+ say_error("\n\n RBIs exported by `#{gem.name}` contain errors and can't be used:", :yellow)
371
+ say_error("Cause: #{e.message} (#{e.location})")
342
372
  end
343
373
  end
344
374
  end
@@ -7,13 +7,21 @@ module Tapioca
7
7
  sig do
8
8
  params(
9
9
  sorbet_config: String,
10
+ tapioca_config: String,
10
11
  default_postrequire: String,
11
12
  default_command: String,
12
13
  file_writer: Thor::Actions
13
14
  ).void
14
15
  end
15
- def initialize(sorbet_config:, default_postrequire:, default_command:, file_writer: FileWriter.new)
16
+ def initialize(
17
+ sorbet_config:,
18
+ tapioca_config:,
19
+ default_postrequire:,
20
+ default_command:,
21
+ file_writer: FileWriter.new
22
+ )
16
23
  @sorbet_config = sorbet_config
24
+ @tapioca_config = tapioca_config
17
25
  @default_postrequire = default_postrequire
18
26
 
19
27
  super(default_command: default_command, file_writer: file_writer)
@@ -24,25 +32,41 @@ module Tapioca
24
32
 
25
33
  sig { override.void }
26
34
  def generate
27
- create_config
35
+ create_sorbet_config
36
+ create_tapioca_config
28
37
  create_post_require
29
- if File.exist?(@default_command)
30
- generate_binstub!
31
- else
32
- generate_binstub
33
- end
38
+ create_binstub
34
39
  end
35
40
 
36
41
  private
37
42
 
38
43
  sig { void }
39
- def create_config
44
+ def create_sorbet_config
40
45
  create_file(@sorbet_config, <<~CONTENT, skip: true, force: false)
41
46
  --dir
42
47
  .
43
48
  CONTENT
44
49
  end
45
50
 
51
+ sig { void }
52
+ def create_tapioca_config
53
+ create_file(@tapioca_config, <<~YAML, skip: true, force: false)
54
+ gem:
55
+ # Add your `gem` command parameters here:
56
+ #
57
+ # exclude:
58
+ # - gem_name
59
+ # doc: true
60
+ # workers: 5
61
+ dsl:
62
+ # Add your `dsl` command parameters here:
63
+ #
64
+ # exclude:
65
+ # - SomeGeneratorName
66
+ # workers: 5
67
+ YAML
68
+ end
69
+
46
70
  sig { void }
47
71
  def create_post_require
48
72
  create_file(@default_postrequire, <<~CONTENT, skip: true, force: false)
@@ -54,15 +78,16 @@ module Tapioca
54
78
  end
55
79
 
56
80
  sig { void }
57
- def generate_binstub!
58
- installer.generate_bundler_executable_stubs(spec, { force: true })
59
- say_status(:force, @default_command, :yellow)
60
- end
81
+ def create_binstub
82
+ force = File.exist?(@default_command)
61
83
 
62
- sig { void }
63
- def generate_binstub
64
- installer.generate_bundler_executable_stubs(spec)
65
- say_status(:create, @default_command, :green)
84
+ installer.generate_bundler_executable_stubs(spec, { force: force })
85
+
86
+ say_status(
87
+ force ? :force : :create,
88
+ @default_command,
89
+ force ? :yellow : :green
90
+ )
66
91
  end
67
92
 
68
93
  sig { returns(Bundler::Installer) }
@@ -6,14 +6,14 @@ module Tapioca
6
6
  class Todo < Base
7
7
  sig do
8
8
  params(
9
- todos_path: String,
9
+ todo_file: String,
10
10
  file_header: T::Boolean,
11
11
  default_command: String,
12
12
  file_writer: Thor::Actions
13
13
  ).void
14
14
  end
15
- def initialize(todos_path:, file_header:, default_command:, file_writer: FileWriter.new)
16
- @todos_path = todos_path
15
+ def initialize(todo_file:, file_header:, default_command:, file_writer: FileWriter.new)
16
+ @todo_file = todo_file
17
17
  @file_header = file_header
18
18
 
19
19
  super(default_command: default_command, file_writer: file_writer)
@@ -26,7 +26,7 @@ module Tapioca
26
26
 
27
27
  # Clean all existing unresolved constants before regenerating the list
28
28
  # so Sorbet won't grab them as already resolved.
29
- File.delete(@todos_path) if File.exist?(@todos_path)
29
+ File.delete(@todo_file) if File.exist?(@todo_file)
30
30
 
31
31
  rbi_string = compiler.compile
32
32
  if rbi_string.empty?
@@ -44,9 +44,9 @@ module Tapioca
44
44
  content << "\n"
45
45
 
46
46
  say("Done", :green)
47
- create_file(@todos_path, content, verbose: false)
47
+ create_file(@todo_file, content, verbose: false)
48
48
 
49
- name = set_color(@todos_path, :yellow, :bold)
49
+ name = set_color(@todo_file, :yellow, :bold)
50
50
  say("\nAll unresolved constants have been written to #{name}.", [:green, :bold])
51
51
  say("Please review changes and commit them.", [:green, :bold])
52
52
  end
@@ -0,0 +1,26 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "thor"
5
+
6
+ module Tapioca
7
+ module CliHelper
8
+ extend T::Sig
9
+ extend T::Helpers
10
+
11
+ requires_ancestor { Thor::Shell }
12
+
13
+ sig { params(message: String, color: T.any(Symbol, T::Array[Symbol])).void }
14
+ def say_error(message = "", *color)
15
+ force_new_line = (message.to_s !~ /( |\t)\Z/)
16
+ # NOTE: This is a hack. We're no longer subclassing from Thor::Shell::Color
17
+ # so we no longer have access to the prepare_message call.
18
+ # We should update this to remove this.
19
+ buffer = shell.send(:prepare_message, *T.unsafe([message, *T.unsafe(color)]))
20
+ buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
21
+
22
+ $stderr.print(buffer)
23
+ $stderr.flush
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,84 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "yaml"
5
+
6
+ module Tapioca
7
+ module ConfigHelper
8
+ extend T::Sig
9
+
10
+ sig { returns(String) }
11
+ attr_reader :command_name
12
+
13
+ sig { returns(Thor::CoreExt::HashWithIndifferentAccess) }
14
+ attr_reader :defaults
15
+
16
+ sig { params(args: T.untyped, local_options: T.untyped, config: T.untyped).void }
17
+ def initialize(args = [], local_options = {}, config = {})
18
+ # Store current command
19
+ command = config[:current_command]
20
+ command_options = config[:command_options]
21
+ @command_name = T.let(command.name, String)
22
+ @merged_options = T.let(nil, T.nilable(Thor::CoreExt::HashWithIndifferentAccess))
23
+ @defaults = T.let(Thor::CoreExt::HashWithIndifferentAccess.new, Thor::CoreExt::HashWithIndifferentAccess)
24
+
25
+ # Filter command options unless we are handling the help command.
26
+ # This is so that the defaults are printed
27
+ filter_defaults(command_options) unless command_name == "help"
28
+
29
+ super
30
+ end
31
+
32
+ sig { returns(Thor::CoreExt::HashWithIndifferentAccess) }
33
+ def options
34
+ @merged_options ||= begin
35
+ original_options = super
36
+ config_options = config_options(original_options)
37
+
38
+ merge_options(defaults, config_options, original_options)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ sig { params(options: T::Hash[Symbol, Thor::Option]).void }
45
+ def filter_defaults(options)
46
+ options.each do |key, option|
47
+ # Store the value of the current default in our defaults hash
48
+ defaults[key] = option.default
49
+ # Remove the default value from the option
50
+ option.instance_variable_set(:@default, nil)
51
+ end
52
+ end
53
+
54
+ sig { params(options: Thor::CoreExt::HashWithIndifferentAccess).returns(Thor::CoreExt::HashWithIndifferentAccess) }
55
+ def config_options(options)
56
+ config_file = options[:config]
57
+ config = {}
58
+
59
+ if File.exist?(config_file)
60
+ config = YAML.load_file(config_file, fallback: {})
61
+ end
62
+
63
+ Thor::CoreExt::HashWithIndifferentAccess.new(config[command_name] || {})
64
+ end
65
+
66
+ sig do
67
+ params(options: T.nilable(Thor::CoreExt::HashWithIndifferentAccess))
68
+ .returns(Thor::CoreExt::HashWithIndifferentAccess)
69
+ end
70
+ def merge_options(*options)
71
+ merged = options.each_with_object({}) do |option, result|
72
+ result.merge!(option || {}) do |_, this_val, other_val|
73
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
74
+ Thor::CoreExt::HashWithIndifferentAccess.new(this_val.merge(other_val))
75
+ else
76
+ other_val
77
+ end
78
+ end
79
+ end
80
+
81
+ Thor::CoreExt::HashWithIndifferentAccess.new(merged)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,51 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+
6
+ module Tapioca
7
+ module Helpers
8
+ module Test
9
+ module Content
10
+ extend T::Sig
11
+ extend T::Helpers
12
+
13
+ requires_ancestor { Kernel }
14
+
15
+ sig { void }
16
+ def teardown
17
+ super
18
+ remove_tmp_path
19
+ end
20
+
21
+ sig { params(args: String).returns(String) }
22
+ def tmp_path(*args)
23
+ @tmp_path = T.let(@tmp_path, T.nilable(String))
24
+ @tmp_path ||= Dir.mktmpdir
25
+ T.unsafe(File).join(@tmp_path, *args)
26
+ end
27
+
28
+ sig { void }
29
+ def remove_tmp_path
30
+ FileUtils.rm_rf(tmp_path)
31
+ end
32
+
33
+ sig { params(name: String, content: String, require_file: T::Boolean).returns(String) }
34
+ def add_ruby_file(name, content, require_file: true)
35
+ add_content_file(name, content).tap do |file_name|
36
+ Tapioca.silence_warnings { require(file_name) } if require_file
37
+ end
38
+ end
39
+
40
+ sig { params(name: String, content: String).returns(String) }
41
+ def add_content_file(name, content)
42
+ file_name = tmp_path("lib/#{name}")
43
+ raise ArgumentError, "a file named '#{name}' was already added; cannot overwrite." if File.exist?(file_name)
44
+ FileUtils.mkdir_p(File.dirname(file_name))
45
+ File.write(file_name, content)
46
+ file_name
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,125 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Helpers
6
+ module Test
7
+ # Copied from ActiveSupport::Testing::Isolation since we cannot require
8
+ # constants from ActiveSupport without polluting the global namespace.
9
+ module Isolation
10
+ extend T::Sig
11
+ require "thread"
12
+
13
+ sig { returns(T::Boolean) }
14
+ def self.forking_env?
15
+ !ENV["NO_FORK"] && Process.respond_to?(:fork)
16
+ end
17
+
18
+ def run
19
+ serialized = T.unsafe(self).run_in_isolation do
20
+ super
21
+ end
22
+
23
+ Marshal.load(serialized)
24
+ end
25
+
26
+ module Forking
27
+ extend T::Sig
28
+ include Kernel
29
+
30
+ sig { params(_blk: T.untyped).returns(String) }
31
+ def run_in_isolation(&_blk)
32
+ read, write = IO.pipe
33
+ read.binmode
34
+ write.binmode
35
+
36
+ this = T.cast(self, Minitest::Test)
37
+ pid = fork do
38
+ read.close
39
+ yield
40
+ begin
41
+ if this.error?
42
+ this.failures.map! do |e|
43
+ Marshal.dump(e)
44
+ e
45
+ rescue TypeError
46
+ ex = Exception.new(e.message)
47
+ ex.set_backtrace(e.backtrace)
48
+ Minitest::UnexpectedError.new(ex)
49
+ end
50
+ end
51
+ test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : this.dup
52
+ result = Marshal.dump(test_result)
53
+ end
54
+
55
+ write.puts [result].pack("m")
56
+ write.close
57
+ exit!(false)
58
+ end
59
+
60
+ write.close
61
+ result = read.read
62
+ read.close
63
+
64
+ Process.wait2(T.must(pid))
65
+ T.must(result).unpack1("m")
66
+ end
67
+ end
68
+
69
+ module Subprocess
70
+ extend T::Sig
71
+ include Kernel
72
+ ORIG_ARGV = T.let(ARGV.dup, T::Array[T.untyped]) unless defined?(ORIG_ARGV)
73
+
74
+ # Crazy H4X to get this working in windows / jruby with
75
+ # no forking.
76
+ sig { params(_blk: T.untyped).returns(String) }
77
+ def run_in_isolation(&_blk)
78
+ this = T.cast(self, Minitest::Test)
79
+ require "tempfile"
80
+
81
+ if ENV["ISOLATION_TEST"]
82
+ yield
83
+ test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : this.dup
84
+ File.open(T.must(ENV["ISOLATION_OUTPUT"]), "w") do |file|
85
+ file.puts [Marshal.dump(test_result)].pack("m")
86
+ end
87
+ exit!(false)
88
+ else
89
+ Tempfile.open("isolation") do |tmpfile|
90
+ env = {
91
+ "ISOLATION_TEST" => this.class.name,
92
+ "ISOLATION_OUTPUT" => tmpfile.path,
93
+ }
94
+
95
+ test_opts = "-n#{this.class.name}##{this.name}"
96
+
97
+ load_path_args = []
98
+ $-I.each do |p|
99
+ load_path_args << "-I"
100
+ load_path_args << File.expand_path(p)
101
+ end
102
+
103
+ child = IO.popen([env, Gem.ruby, *load_path_args, $PROGRAM_NAME, *ORIG_ARGV, test_opts])
104
+
105
+ begin
106
+ Process.wait(child.pid)
107
+ rescue Errno::ECHILD # The child process may exit before we wait
108
+ nil
109
+ end
110
+
111
+ return T.must(tmpfile.read).unpack1("m")
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ if forking_env?
118
+ include(Forking)
119
+ else
120
+ include(Subprocess)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "erb"
5
+
6
+ module Tapioca
7
+ module Helpers
8
+ module Test
9
+ module Template
10
+ include Kernel
11
+ extend T::Sig
12
+ ERB_SUPPORTS_KVARGS = T.let(
13
+ ::ERB.instance_method(:initialize).parameters.assoc(:key), T.nilable([Symbol, Symbol])
14
+ )
15
+
16
+ sig { params(selector: String).returns(T::Boolean) }
17
+ def ruby_version(selector)
18
+ Gem::Requirement.new(selector).satisfied_by?(Gem::Version.new(RUBY_VERSION))
19
+ end
20
+
21
+ sig { params(src: String).returns(String) }
22
+ def template(src)
23
+ erb = if ERB_SUPPORTS_KVARGS
24
+ ::ERB.new(src, trim_mode: ">")
25
+ else
26
+ ::ERB.new(src, nil, ">")
27
+ end
28
+
29
+ erb.result(binding)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -6,11 +6,12 @@ require "tapioca/loader"
6
6
  require "tapioca/sorbet_ext/generic_name_patch"
7
7
  require "tapioca/sorbet_ext/fixed_hash_patch"
8
8
  require "tapioca/generic_type_registry"
9
- require "tapioca/config"
10
- require "tapioca/config_builder"
9
+ require "tapioca/helpers/cli_helper"
10
+ require "tapioca/helpers/config_helper"
11
11
  require "tapioca/generators"
12
12
  require "tapioca/cli"
13
13
  require "tapioca/gemfile"
14
+ require "tapioca/executor"
14
15
  require "tapioca/compilers/sorbet"
15
16
  require "tapioca/compilers/requires_compiler"
16
17
  require "tapioca/compilers/symbol_table_compiler"